Skip to content

My Site Stuttered on Scroll — It Was Not the Hosting, It Was the CSS

by Lucas Almeida 5 min read

DigitalRadar loaded fast. Time to first content was good, the server responded well, every image was already WebP. And yet the experience was bad: scrolling stuttered, the custom cursor dragged behind the mouse, and hovering over cards felt like a heavy site.

My first suspect was the hosting. My second was WordPress. Neither had anything to do with it — the problem lived entirely in the browser, in four CSS decisions and one JavaScript decision.

Why measuring “speed” does not find this

The tools we reach for by reflex — PageSpeed, GTmetrix — mostly measure loading: how long until the page appears and becomes usable. They are good at that, and that is exactly where my site was already fine.

What I had was a runtime problem: the cost of drawing each frame after the page had loaded. That does not show up in a 0-to-100 score. It shows up in the DevTools Performance panel, when you record a few seconds of scrolling and watch the rendering bar blow past the 16-millisecond-per-frame budget.

The four culprits

1. A full-screen grain overlay with mix-blend-mode

The theme had a subtle texture covering the whole site, for a filmic look:

.dr-body::before {
  position: fixed;
  inset: 0;
  mix-blend-mode: soft-light;   /* ← the problem */
}

This is the worst of the four. A fixed element covering the viewport with mix-blend-mode forces the browser to recompose the entire page every frame, because the blend result depends on everything underneath — and what is underneath changes with every scrolled pixel. Removing the layer killed half the jank.

2. A custom cursor with blend and an infinite loop

The glow following the mouse had two compounding faults: it used mix-blend-mode and it ran a permanent requestAnimationFrame, recalculating position even while the mouse sat still.

The fix was dropping the blend, switching positioning to translate3d, isolating the element with contain and — most importantly — making the loop stop on its own. The interpolation runs while the distance between cursor and target still matters; below half a pixel, the loop ends:

if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) {
  running = false;
  return;          // nothing moving = nothing to draw
}
requestAnimationFrame(step);

3. filter: blur() over large areas

There were decorative glows using filter: blur(30px) to blur(40px) in the hero, the CTA and the newsletter block. Blur is expensive: the browser has to render the layer and then process it.

I replaced all of them with radial gradients, which are soft by construction and cost nothing:

/* before */
background: var(--dr-neon); filter: blur(40px);

/* after */
background: radial-gradient(circle, rgba(0,180,255,.35), transparent 70%);

There was even a filter: blur(.3px) on an animated element — a tenth of a pixel of blur, invisible in practice, forcing filter cost on every frame.

4. Blend on an animated element

The same mistake as item 1, at smaller scale: an animated scan line using mix-blend-mode. The blend went, the animation stayed.

How to find this on your own site

Two routes in Chrome DevTools, both under a minute:

  1. Rendering → Paint flashing. Open the three-dot menu → More tools → Rendering and tick “Paint flashing”. Scroll. Areas flashing green are being repainted. If the whole viewport flashes while scrolling, you have a layer problem — most likely a fixed element with a blend or a filter.
  2. Performance → record 5 seconds of scrolling. Look at the Rendering and Painting tracks. Long, repeated bars mean per-frame cost, not load cost.

To hunt the suspects straight in the code, one search over your CSS gets you most of the way:

grep -rn "mix-blend-mode|backdrop-filter|filter: *blur" assets/css/

The rules I adopted

  • Never use mix-blend-mode, backdrop-filter or filter: blur() on an element that is position: fixed, animated in a loop, or receiving a transform on scroll. backdrop-filter on a short fixed header is acceptable; on a full-screen layer, it is not.
  • No requestAnimationFrame running forever. Always gate it on “something is moving” and end it when motion stops.
  • Animate only opacity and transform — the two properties the browser resolves without redoing layout or paint.
  • will-change only during the animation. Leaving it in static CSS reserves GPU memory permanently.
  • Test in Performance before adding a new effect, not after the site feels slow.

The practical result: scrolling became clean and the cursor started tracking the mouse. Not a line of PHP changed, and no hosting plan was swapped.

Frequently asked questions

Why did PageSpeed not flag this?

Because it mostly measures loading. Scroll stutter is per-frame cost after the page has already loaded, and that appears in the DevTools Performance panel, not in the score.

Is mix-blend-mode always bad?

No. The problem is the combination: a blend on a full-screen, fixed or animated element. On a small static icon the cost is irrelevant.

How do I know whether it is my site or the visitor’s computer?

Use CPU throttling in DevTools (Performance → CPU → 4x or 6x slowdown). If it stutters throttled, it will stutter on a mid-range phone, which is most of your traffic.

Would better hosting fix it?

Not in this case. A faster server delivers the HTML sooner, but the visitor’s browser is what draws each frame. Compositing problems travel with the CSS.

At DigitalRadar, we write up what broke on our own site and how it was fixed. Stay on the radar.

Lucas Almeida
DigitalRadar Newsroom

Desenvolvedor e fundador da Elion Estúdio, Lucas Almeida é especialista em tecnologias emergentes, games e análise de mercado. Com experiência em SEO e growth marketing, foca em conteúdo aprofundado que conecta o universo da tecnologia com o dia a dia das pessoas.

Reported from the sources cited and verified before publishing. See our Editorial Policy.

Leave a comment

Your email address will not be published. Required fields are marked *