The Hidden Cost of getComputedStyle in Your Animation Loop
A portfolio that stuttered while scrolling taught me a lesson about forced synchronous layout — and why your 60fps animation might be quietly destroying scroll performance.
Scrolling on my own portfolio would occasionally stutter or feel stuck. No obvious culprit — until I profiled it and found two classic performance anti-patterns hiding in plain sight.
Anti-pattern 1: reading styles every frame
A particle background read the theme's accent color inside its requestAnimationFrame loop:
const draw = () => {
// Runs ~60 times per second 👇
const accent = getComputedStyle(document.documentElement)
.getPropertyValue("--accent-primary");
// ...
requestAnimationFrame(draw);
};
getComputedStyle forces a synchronous style recalculation. Doing it 60 times a second on the main thread competes directly with scrolling, which also needs the main thread. The fix: read it once, cache it, and only refresh when it actually changes.
let rgb = parseAccent();
const observer = new MutationObserver(() => (rgb = parseAccent()));
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});
Anti-pattern 2: layout reads on every mouse event
A custom cursor called getComputedStyle(target).cursor on every mouseover to detect interactive elements. Same problem, different trigger. Replacing it with a cheap selector test removed the jank entirely:
if (target.closest('a, button, input, [role="button"]')) {
setHovering(true);
}
Anti-pattern 3: a draggable element that eats scroll
A framer-motion carousel with drag="x" was capturing vertical scroll gestures that happened to start on the card. The one-line fix is dragDirectionLock, which only claims a gesture once it's clearly horizontal:
<motion.div drag="x" dragDirectionLock dragElastic={0.2} />
The lesson
"Sometimes scrolling doesn't work" is almost never a scroll bug. It's the main thread being held hostage by forced synchronous layout — or a gesture handler that's too greedy. Profile before you guess, and treat getComputedStyle inside any hot path as a red flag.