Core Web Vitals explained: what each metric measures and how to move it
Three numbers, each measuring a different way a page can feel bad: slow to appear, slow to respond, and unstable while you use it.
Web animations stay smooth when they only change transform and opacity, which the compositor can handle without recalculating layout or repainting. Animating width, height, top or left forces layout on every frame, which is the usual cause of stutter on mid-range mobile devices.
Transform and opacity, because the compositor can apply them without recalculating layout or repainting pixels.
| Property | Triggers | Cost |
|---|---|---|
| transform | Composite only | Cheap |
| opacity | Composite only | Cheap |
| background-color | Paint | Moderate |
| box-shadow | Paint | Moderate to expensive |
| width / height | Layout, paint, composite | Expensive |
| top / left | Layout, paint, composite | Expensive |
| margin / padding | Layout, paint, composite | Expensive |
/* Forces layout on every frame */
.card:hover { width: 320px; top: -4px; }
/* Compositor handles it — same visual result */
.card { transition: transform 200ms ease-out; }
.card:hover { transform: translateY(-4px) scale(1.05); }// Bad: read after write, in a loop — forces synchronous layout each iteration
for (const el of items) {
el.style.height = "auto";
const height = el.offsetHeight; // forces layout, every time
el.style.height = height + "px";
}
// Better: batch all reads, then all writes
const heights = items.map(el => el.offsetHeight);
items.forEach((el, i) => { el.style.height = heights[i] + "px"; });Reading a geometry property forces the browser to flush pending style changes and recalculate layout. Doing that inside a loop turns one layout pass into hundreds, which is why some animations are fine with ten items and unusable with a hundred.
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}Some users experience nausea or vestibular symptoms from parallax and large motion. The preference is set at the operating system level, and honouring it is a WCAG requirement rather than a nicety.
CSS animations on transform and opacity can run off the main thread, so they keep going even when JavaScript is busy. A JavaScript animation using the Web Animations API on the same properties performs comparably; one using requestAnimationFrame to set layout properties does not.
Sparingly, and shortly before the animation starts rather than permanently in a stylesheet. It promotes an element to its own compositor layer, which costs memory. Applied to many elements it makes performance worse, not better.
Not inherently — good ones animate the right properties. The cost is bundle size, so import only what you use and avoid loading a large motion library globally for one hero animation.
60fps, meaning under 16.7ms per frame including all other page work. On high-refresh displays the browser may target higher, but 60 is the practical bar and consistency matters more than the peak.
ROVQIX Engineering
Engineering team, ROVQIX
The ROVQIX engineering team builds and maintains web platforms, APIs and infrastructure for clients across SaaS, ecommerce and enterprise. These notes come out of real production work — deploys, incidents, migrations and audits.
ROVQIXdesigns and builds production web platforms — Next.js front ends, Node.js APIs and the infrastructure behind them. Tell us what you're building and we'll scope it with you.
Three numbers, each measuring a different way a page can feel bad: slow to appear, slow to respond, and unstable while you use it.
LCP is not one number, it is four phases. Optimising the wrong one is why so much performance work produces no measurable change.
A landing page is one argument made in the right order. Most fail on the first line.
No spam. Just the occasional case study and craft breakdown.