Portfolio/Writing

Daksh Nauni

Full-Stack & Mobile Engineer building web platforms and production mobile apps end to end.

Navigation

  • Home
  • Projects
  • Blog
  • Contact

© 2026 Daksh Nauni. All rights reserved.

FrontendFixNextJS

Why Your Marquee Stutters and How to Fix Its Timing

Learn why marquee animations stutter across refresh rates and after dropped frames, plus how elapsed time, pixel-based speed, and visibility handling fix them.

Daksh Nauni·September 12, 2026·10 min read
Fix Marquee Stutter with Frame Rate Independence

Intro


Never thought I would be writing a blog on something like this but I wanted the smoothest marquee ever on every kind of device my site opens in.

You build a scrolling marquee for testimonials, logos, or a ticker using `requestAnimationFrame`, a browser feature that runs code before the next screen update. It looks smooth on your machine. Then someone opens it on a screen that refreshes 120 times per second, a laptop under load, or a tab that was recently in the background. The marquee changes speed, stutters, or suddenly jumps forward.


Nothing crashed, and React may not be the problem. The issue is often a mismatch between three things:

  1. How movement is measured
  2. How much real time passed between frames
  3. What should happen after an unusually long pause


Those are related problems, but they need different fixes.
Before looking at the code, it helps to define three terms:

  • A frame is one image drawn on the screen during an animation.
  • The frame rate is how many frames the screen draws each second. Hz means updates per second, so a 60 Hz display can draw up to 60 frames per second, while a 120 Hz display can draw up to 120.
  • Delta time, usually named delta, is the amount of time that passed since the previous frame.Sometimes, this is the actual issue so I am not saying that these are wrong guesses in every situation. But often the real bug is in the math driving the animation loop itself.

In my case, I noticed the jitter on a 120fps monitor. I don’t even have one, my friend told me about this issue. “Daksh, the brands marquee stutters on my desktop of 120Hz”

I wondered why cause I picked the marquee code from a popular component lib in the first place. Well since I went down the rabbit hole, this blog exists.


Why fixed movement per frame causes marquee stutter

The examples use `useAnimationFrame`, a Motion library function that runs your code once per frame. The variable `baseX` stores the marquee's current horizontal position.
The simplest animation loop moves an element by the same amount every time the browser renders a frame:

javascript
const speedPerFrame = 0.5;

useAnimationFrame(() => {
  baseX.set(baseX.get() - speedPerFrame);
});



This code ties speed to the number of rendered frames instead of elapsed time.
At 60 frames per second, the marquee moves about 30 pixels per second.

At 120 frames per second, it moves about 60 pixels per second.

If the browser drops frames, it moves more slowly because fewer updates occur.


This is called frame-rate-dependent motion because the animation's speed depends on how many frames the browser draws. That is the real timing bug.


A common fix: scale movement using delta time

Once you know that fixed movement per frame is unreliable, the natural next step is to adjust the movement based on how long the frame took.
At 60 frames per second, each frame has about 16.67 milliseconds available. Many animation loops use that duration as a reference. A frame that takes 33.34 milliseconds receives twice the normal movement because it represents roughly twice as much elapsed time:

javascript
const speedAt60Fps = 0.5;

useAnimationFrame((_time, delta) => {
  const moveBy = speedAt60Fps * (delta / 16.67);
  baseX.set(baseX.get() - moveBy);
});

This calculation is valid. It accounts for elapsed time by comparing `delta` with the duration of a typical 60 Hz frame.
It is mathematically equivalent to expressing the same speed in pixels per second:

javascript
const pixelsPerSecond = speedAt60Fps * (1000 / 16.67);
const moveBy = pixelsPerSecond * (delta / 1000);

Both versions produce nearly the same movement. Dividing by `1000` is not a special fix for stuttering, and dividing by `16.67` does not create compounding error.
Pixels per second is still usually the clearer way to describe the speed. A value such as `30` clearly means 30 pixels every second, regardless of the display refresh rate.


Percentage-based speed is a separate problem

A marquee often stores its position as a percentage and wraps between values such as `-50%` and `0%`. That can make the visible pixel speed depend on the rendered width of the track.
For example, moving by one percent on a 2,000 pixel track covers twice the physical distance of moving by one percent on a 1,000 pixel track. Font loading, changes to the number or width of cards, and screen-width layout rules called responsive breakpoints can therefore change the perceived speed.
This does not make percentage-based motion frame-rate dependent. Instead, it makes the speed layout dependent, which means the visible speed changes when the element's size changes.
Using pixels per second and wrapping by the measured width of one repeated segment gives the animation a consistent physical speed.

A frame-rate-independent marquee

Assume the marquee contains two identical copies of the same content. The variable `loopWidth` stores the measured pixel width of one copy.
The example also uses `wrap(min, max, value)`. A wrap helper keeps a number inside a range. When the marquee moves past one end, the helper moves its position back to the other end so the repeated content appears continuous.
The animation can now calculate movement from the elapsed time reported by Motion:

javascript
const pixelsPerSecond = 30;

useAnimationFrame((_time, delta) => {
  const elapsedSeconds = delta / 1000;
  const nextX = baseX.get() - pixelsPerSecond * elapsedSeconds;

  baseX.set(wrap(-loopWidth, 0, nextX));
});

The units now line up: pixels/second * seconds = pixels

A 60 Hz display receives smaller, frequent updates. A 120 Hz display receives even smaller and more frequent updates. If the browser misses a normal frame, the next update includes the extra time that passed.


ResizeObserver is a browser tool that reports when an element changes size. Use it, or a similar React measurement hook, to update loopWidth when the repeated segment changes size. Wrap after one complete segment, not after the full width of both duplicated segments.

Why the marquee can still jump after a long pause

Delta-time movement keeps the speed correct, but it does not guarantee that every frame will look smooth.


The main thread is the part of the browser that runs most JavaScript and handles much of the page's work. If other work blocks it for 150 milliseconds, the next frame receives a large delta.


Moving the full distance for those 150 milliseconds keeps the marquee aligned with real time, but the user sees one large visual step. The same issue can appear when a background tab becomes active again because browsers commonly pause or slow down animation callbacks while a page is hidden.

You need to choose the behavior that suits the animation.

Option 1: Clamp unusually large delta values

For a decorative marquee, smooth-looking movement is often more important than catching up with real time:

javascript
const pixelsPerSecond = 30;
const maxDelta = 50;

useAnimationFrame((_time, delta) => {
  const elapsedSeconds = Math.min(delta, maxDelta) / 1000;
  const nextX = baseX.get() - pixelsPerSecond * elapsedSeconds;

  baseX.set(wrap(-loopWidth, 0, nextX));
});

Clamping means limiting a number to a chosen maximum.

In this example, any `delta` greater than 50 milliseconds is treated as 50 milliseconds.
Clamping discards some elapsed time after a long pause, so the marquee does not jump to catch up. That tradeoff is usually acceptable for an endlessly repeating decorative element.


Do not use this approach when the animation must remain synchronized, which means kept at the same point in time, with audio, video, a timer, or another real-time source.

Option 2: Pause while the page is hidden

The Page Visibility API is a browser feature that tells your code whether the page is currently visible. Use it directly, or use a visibility hook such as Motion's usePageInView, to stop updating the marquee while the page is hidden. Resume from the same position when the page becomes visible again.
This avoids spending work on a hidden animation and makes the intended pause behavior explicit.

Option 3: Calculate position from one trusted clock

If real-time synchronization matters, choose one clock as the trusted source of time. Calculate the current position from that clock instead of repeatedly adding movement to the previous position. The animation may still appear to jump after a long blocked frame, but its position will stay matched with the clock.

Other causes of a choppy marquee

Correct timing cannot fix every rendering problem. Browsers have a later rendering stage called compositing, where already-drawn parts of a page can be moved together without recalculating the entire layout.

If the speed calculation is sound, check for these issues:

  • Animating layout properties such as `left` instead of `transform`. Browsers can often move transforms during compositing, which requires less layout work.
  • Re-rendering React state on every animation frame
  • Images or fonts changing the track width after the loop starts
  • Two repeated segments with different widths or gaps- Incorrect wrap boundaries that create a visible seam
  • Expensive main-thread work that repeatedly misses frame deadlines. A frame deadline is the short amount of time available before the browser must draw the next frame.
  • Applying filters, shadows, or effects that force the browser to redraw many pixels on a large moving surface


Tools such as `will-change` can help in specific cases, but they do not repair incorrect timing or mismatched loop geometry.

Why this matters beyond marquees

The same distinction applies to custom cursors, canvas animations, parallax effects where page layers move at different speeds, drag inertia that keeps an item moving after release, and game loops:

  • Fixed distance per rendered frame is refresh-rate dependent.
  • Velocity multiplied by elapsed time is frame-rate independent.
  • A very large elapsed-time value may require clamping, pausing, or synchronization logic.
  • Layout-relative units can make perceived speed depend on element size.


These are separate decisions. Treating them separately makes animation bugs much easier to diagnose.

Takeaway

If a marquee animation stutters or changes speed, start by checking whether movement is based on frame count or elapsed time. Expressing velocity in pixels per second makes the behavior easier to understand and keeps physical speed independent of track width.
Then decide how long pauses should behave. Delta time preserves the correct speed, but a large delta can still produce a visible jump. Clamp it for decorative motion, pause while the page is hidden, or calculate position from one trusted clock when synchronization matters.
Frame-rate-independent math is the foundation. Smooth behavior also requires sensible pause handling, stable measurements, correct wrap boundaries, and inexpensive rendering.

References

  1. Window: requestAnimationFrame() on MDN
  2. Motion useAnimationFrame documentation
  3. Motion usePageInView documentation
Daksh Nauni

Daksh Nauni

I am a software engineer who enjoys building mobile apps, web platforms, and backend systems. This blog is where I share what I learn while building real projects, from debugging weird errors to shipping features that actually matter.

View portfolioContact

Work with me

Building an app or web product?

I work with startups and small teams to ship polished, performant web and mobile products. If you have something worth building, I'd love to hear about it.

View Portfolio

or Get in touch

On this page

  • Intro
  • Why fixed movement per frame causes marquee stutter
  • A common fix: scale movement using delta time
  • Percentage-based speed is a separate problem
  • A frame-rate-independent marquee
  • Why the marquee can still jump after a long pause
  • Option 1: Clamp unusually large delta values
  • Option 2: Pause while the page is hidden
  • Option 3: Calculate position from one trusted clock
  • Other causes of a choppy marquee
  • Why this matters beyond marquees
  • Takeaway
  • References

Related Articles

Getx with GoRouter without Bindings in Flutter
Flutter

Getx and GoRouter without Bindings in Flutter Web

Use GoRouter with GetX without Bindings in Flutter Web. Learn how to inject controllers, pass route params, avoid duplicates, and support deep linking safely.

April 13, 20263 min read
Fixing Next JS Heap Memory Exhaustion in next dev
FixErrorNextJS

Fixing Next JS Heap Memory Exhaustion in next dev

Next.js crashed with “JavaScript heap out of memory” during next dev. The cause was Turbopack caching and Tailwind v4 scanning. This post explains the root cause and the simple fixes that stopped it.

April 10, 20262 min read