Investigating Scroll Jank: SVG feTurbulence & GPU Rasterization at high FPS
The Context
Recently, I wanted to add a subtle grainy noise texture to the background of the site. The most elegant, dependency-free way to do this seemed to be using an inline SVG filter with <feTurbulence>. It's a neat trick: you define the filter, apply it via CSS, and you get a nice procedural noise effect without loading any external image assets.
It looked great on my external.
The Problem
When I tested and started scrolling, the performance completely tanked. The scrolling felt heavy, stuttery.
I fired up the Chrome DevTools Performance tab to record a scrolling session. The flame chart was a mess. The main thread wasn't heavily blocked by JS, but there were massive green blocks indicating huge Paint and Rasterization times. The GPU was sweating.
The "Why"
Here is what's happening under the hood:
<feTurbulence>is highly computationally expensive. Generating procedural Perlin/fractal noise requires a lot of math per pixel.- When you scroll, depending on how the browser optimizes the rendering pipeline, elements might need to be repainted or re-rasterized if they aren't properly promoted to their own compositor layer, or if the scrolling forces a repaint of the background context.
- At 120 FPS, the browser has less than ~8.3ms per frame to do the math, composite, and push the pixels. The GPU just couldn't keep up with rasterizing the SVG filter over a large area (like a full-page background) that fast.
Failed Attempts
I initially thought I could just force the browser to throw the noise layer onto the GPU and leave it alone:
Tried adding will-change: transform.
Tried the classic transform: translateZ(0).
Tried pointer-events: none to ensure no hit-testing was triggering recalculations.
While this created a dedicated composite layer, the browser still struggled with the initial rasterization of the massive SVG area, and occasionally re-rasterized it during fast scrolls, causing dropped frames (jank).
The Solution: Keep It Stupid Simple (KISS)
I realized I was over-engineering a static effect. A live SVG filter is overkill for static noise.
The fix:
- I used the SVG
feTurbulencefilter once in a separate sandbox to generate the noise I liked. - Took a screenshot/export of that noise as a tiny 256x256
webp(orpng) tile. - Applied it using standard CSS:
```css
.noise-bg {
background-image: url('/assets/noise.webp');
background-repeat: repeat;
opacity: 0.4; / Tweak for subtlety */
}
The Result:
Silky smooth 120 FPS scrolling. The browser's compositor handles repeating a tiny static image tile effortlessly. GPU rasterization time dropped to near zero.
Takeaway:
Browser APIs like SVG filters and CSS backdrop-filter are incredibly powerful, but their render cost is often hidden until you hit high-refresh-rate screens or lower-end devices. Sometimes, the "dumb" solution (a tiled static image) is objectively better for UX than the mathematically elegant one.