Lindsay Edwards

Own the RAF loop and honour reduced motion

On this page

I added a smooth-scroll library to a page. It felt nice for about half a session, and then scrolling started behaving like it was being pushed twice as hard as I moved. Doubled, janky, slightly seasick.

Nothing in my code was scrolling anything. The library was. Twice.

Two loops where there should be one#

The library drives its own requestAnimationFrame loop. Every frame it reads your intent and nudges the scroll position. One loop, smooth scroll. Fine.

The trouble was teardown. I set the library up in an effect, and StrictMode double-mounts effects in development. My cleanup was not cancelling the exact frame handle the library had started, and it was not calling the library’s own destroy method.

So the first mount started a loop, cleanup did not fully stop it, and the second mount started another. Two loops, both moving the page, both convinced they were the only one. That is your doubled scroll.

For anyone actually reading the page, every flick of the wheel overshot where they were aiming. That is the kind of small, constant friction that makes people give up on a site without ever quite knowing why.

A library that owns an animation loop owns a resource you have to hand back. Forget, and it keeps running without you.

Disciplined teardown#

The fix was to make cleanup do two specific things.

Cancel the exact frame handle the library gave me, not a fresh one, not a guess. And call the library’s destroy method so it releases whatever else it is holding.

Done properly, the first mount’s loop is fully gone before the second mount starts. One loop again, and the doubling disappears.

The lesson generalises. Any library running its own loop needs you to cancel the specific handle and destroy the specific instance. Half a teardown is worse than none, because it leaves an orphan running with no reference you can reach.

Motion is a preference, not a default#

There was a second thing I had skipped, and it matters more than the jank.

Some people set their system to reduce motion. Smooth-scroll hijacking is exactly the kind of thing they are opting out of. So the effect has to check whether reduced motion is requested, and if it is, bail out entirely and never touch scroll.

Not a smaller animation. No animation. The native scroll they asked for.

Gating a motion library behind that preference is not a nice-to-have. If someone tells the browser they get motion sick, overriding their scroll is the wrong answer no matter how smooth it feels to me.

One more host trap#

This page was not on Vercel, and the framework had image optimisation that quietly assumes Vercel’s infrastructure. On another host it does not just underperform, it fails, because the machinery behind it is not there.

So I disabled that feature. It was assuming a host I was not on.

Cancel the handle, honour the preference#

Any library that runs its own animation loop needs disciplined teardown: cancel the exact handle, destroy the instance. Always gate motion behind prefers-reduced-motion. And disable framework features that assume a host you are not running on.

The doubled scroll taught me the first lesson. The second one I should not have needed teaching.

Keep reading