/1.js Secret
Last active
December 16, 2024 20:53
-
Star
(387)
You must be signed in to star a gist -
Fork
(29)
You must be signed in to fork a gist
-
-
Save jakearchibald/cb03f15670817001b1157e62a076fe95 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export function animationInterval(ms, signal, callback) { | |
// Prefer currentTime, as it'll better sync animtions queued in the | |
// same frame, but if it isn't supported, performance.now() is fine. | |
const start = document.timeline ? document.timeline.currentTime : performance.now(); | |
function frame(time) { | |
if (signal.aborted) return; | |
callback(time); | |
scheduleFrame(time); | |
} | |
function scheduleFrame(time) { | |
const elapsed = time - start; | |
const roundedElapsed = Math.round(elapsed / ms) * ms; | |
const targetNext = start + roundedElapsed + ms; | |
const delay = targetNext - performance.now(); | |
setTimeout(() => requestAnimationFrame(frame), delay); | |
} | |
scheduleFrame(start); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Usage | |
import { animationInterval } from './1.js'; | |
const controller = new AbortController(); | |
// Create an animation callback every second: | |
animationInterval(1000, controller.signal, time => { | |
console.log('tick!', time); | |
}); | |
// And to stop it: | |
controller.abort(); |
If your thread is blocked, then both setTimeout
and requestAnimationFrame
callbacks will be delayed. Avoid running long tasks on the UI thread.
I came up with a similar algo that compensates clock skew as it goes. It will compensate end to end for slight delays in setTimeout
, requestAnimationFrame
, and of course callback
executions. So if any of these skew the clock, it will tell the next run to execute a bit earlier. As a bonus, I also made it compatible with useEffect
for react users.
type ExecutionControl = { stopped?: true; animationId?: number; timeoutId?: NodeJS.Timeout };
export function interval(cb: () => void, ms: number, exeCtl: ExecutionControl = {}) {
if (exeCtl.stopped) return;
const start = performance.now();
exeCtl.animationId = requestAnimationFrame(() => {
exeCtl.timeoutId !== undefined && cb();
const end = performance.now();
const diff = Math.round(end - start);
const timeout = Math.max(0, ms - diff);
exeCtl.timeoutId = setTimeout(() => {
return interval(cb, ms, exeCtl);
}, timeout);
});
return () => {
exeCtl.stopped = true;
exeCtl.timeoutId && clearTimeout(exeCtl.timeoutId);
exeCtl.animationId && cancelAnimationFrame(exeCtl.animationId);
};
}
Fwiw, the OP solution isn't incompatible with useEffect
.
useEffect(() => {
const controller = new AbortController();
animationInterval(1000, controller.signal, () => {
// Runs every second
});
return () => controller.abort();
}, [...whatever]);
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Now the time obtained each time
requestAnimationFrame(frame)
is executed is accurate, but due to the use ofsetTimeout
, the main thread will still be blocked and the number of seconds will jump.Suppose there is a time-consuming synchronization task:
The actual performance is as follows:
Assuming there is a time-consuming task, it seems that the only way is to put this time-consuming task into a Worker or adopt a time-slicing solution to divide it into several small tasks so that the main thread has time to update the UI, right?
@jakearchibald