setTimeout(fn, 16) approximates 60fps and is wrong in both directions: it keeps firing in a background tab, burning battery on frames nobody sees, and it drifts out of phase with the browser’s paint cycle so frames get dropped.
function step() {
el.style.transform = 'translateX(' + x + 'px)';
x += 2;
if (x < 400) { requestAnimationFrame(step); }
}
requestAnimationFrame(step);
requestAnimationFrame runs immediately before the next paint, so the work lands in the right place in the frame, and the browser suspends it entirely when the tab is hidden. Read layout properties before writing them within the callback, not after — interleaving reads and writes forces a synchronous reflow per iteration and gives back everything you gained.