Appearance
Delay and Interval Utilities
Promise-based timing utilities: frame-accurate delay using requestAnimationFrame, random delays for staggered animations, and cancelable frame-synced intervals.
When to Use
- Waiting a precise duration in async code
- Animations that need frame-accurate timing
- Staggering visual effects with random delays
- Replacing
setIntervalwith frame-synced, cancelable intervals
The Patterns
Simple delay
A promise-based wrapper around setTimeout. Works in any environment:
javascript
await delay(1000);
console.log("1 second elapsed");Implementing delay()
Wraps setTimeout in a promise so delays can be used with await:
javascript
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}Frame-accurate delay
Uses requestAnimationFrame for timing instead of setTimeout, yielding control to the browser between checks:
javascript
await delay(100);
console.log("100ms elapsed");Why not setTimeout? setTimeout is not frame-synced and can drift. This approach ties delays to the rendering loop, making it ideal for animations.
Implementing frame-accurate delay()
Loops on requestAnimationFrame until the target duration elapses, keeping timing synchronized with the display refresh cycle:
javascript
async function nextFrame() {
return new Promise(requestAnimationFrame);
}
async function delay(ms = 0) {
const startTime = performance.now();
ms = Number(ms);
while (elapsed() < ms) {
await nextFrame();
}
return ms;
function elapsed() {
return performance.now() - startTime;
}
}Random delay
Stagger effects by picking a random duration within a range:
javascript
for (const element of elements) {
await randomDelay(50, 150);
element.classList.add("visible");
}Implementing randomDelay()
Picks a random target duration within the given range and waits using frame-based timing:
javascript
async function randomDelay(min, max) {
const startTime = performance.now();
const target = random(min, max);
while (elapsed() < target) {
await nextFrame();
}
return target;
function elapsed() {
return performance.now() - startTime;
}
}Simple interval
An async iterable wrapper around setInterval using Repeater. Yields elapsed time at each tick. Cancel by breaking out of the loop:
javascript
for await (const elapsed of interval(1000)) {
console.log(`${elapsed}ms since start`);
if (elapsed > 5000) break;
}Implementing interval() with Repeater
Wraps setInterval in a Repeater async iterable that yields elapsed time on each tick:
javascript
import { Repeater } from "@repeaterjs/repeater";
function interval(ms) {
const start = performance.now();
return new Repeater(async (push, stop) => {
const id = setInterval(() => push(elapsed()), ms);
await stop;
clearInterval(id);
});
function elapsed() {
return performance.now() - start;
}
}Frame-based cancelable interval
Replace setInterval with a frame-synced interval. Returns an object with a cancel() method:
javascript
const timer = interval(1000, (time) => {
console.log("tick", time);
});
// Stop after 5 seconds
setTimeout(() => timer.cancel(), 5000);Implementing frame-based interval()
Schedules callbacks aligned to animation frames and returns a { cancel() } handle for cleanup:
javascript
function interval(ms, callback) {
const start = document.timeline.currentTime;
let cancelled = false;
function frame(time) {
if (cancelled) 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);
return { cancel() { cancelled = true; } };
}Key details
performance.now()provides sub-millisecond precisionrequestAnimationFrameties execution to the display refresh rate (~16ms at 60fps)document.timeline.currentTimegives a stable reference for elapsed-time calculations- Returning a
{ cancel() }object keeps the API simple — no need to set up anAbortControllerfor basic interval cancellation
Trade-offs
| Approach | Pros | Cons |
|---|---|---|
Frame-based delay | Frame-synced, smooth | Higher CPU overhead |
setTimeout promise | Simple, low overhead | Not frame-synced, drifts |
Frame-based interval | Precise, cancelable | More complex, browser-only |
setInterval | Simple, universal | Drifts over time, poor cancel |