Understand the four phases of browser rendering so you know exactly which CSS properties are expensive and how will-change works under the hood.
Open this lesson in KodokonThe browser turns your CSS into pixels through a four-phase pipeline: Style (computing values), Layout (geometry, also known as reflow), Paint (rasterizing into pixels, the repaint) and Composite (assembling layers on the GPU). The golden rule: the earlier the phase you touch, the bigger the bill, because every subsequent phase gets replayed. Changing width or top triggers layout + paint + composite. Changing color or box-shadow skips layout but repaints. Only transform and opacity can be handled by the compositor thread alone: the animation stays smooth even when the main thread is blocked by JavaScript.
.slide-bad {
position: absolute;
transition: left 0.3s ease;
}
.slide-bad:hover { left: 120px; }
.slide-good {
transition: transform 0.3s ease;
}
.slide-good:hover { transform: translateX(120px); }The browser is lazy: it batches your style writes and only recomputes layout at the next refresh. But some JavaScript reads - offsetHeight, getBoundingClientRect(), scrollTop, getComputedStyle() on a geometric property - demand up-to-date values. If a write is pending, the browser must recompute immediately and synchronously: that is a forced reflow. Alternating reads and writes inside a loop produces layout thrashing: one full reflow per iteration instead of a single one for the whole batch.
const items = document.querySelectorAll('.item');
items.forEach((item) => {
const h = item.offsetHeight;
item.style.height = h * 2 + 'px';
});
const heights = [...items].map((i) => i.offsetHeight);
items.forEach((item, index) => {
item.style.height = heights[index] * 2 + 'px';
});Compositing slices the page into layers that are rasterized independently and assembled by the GPU. will-change: transform asks for the element to be promoted to its own layer before the animation, avoiding the costly rasterization on the first frame. A spec subtlety that is often overlooked: will-change must produce the same side effects as the property it announces. So will-change: transform creates a stacking context and turns the element into a containing block for its position: fixed descendants - exactly like a real transform. A simple performance hint can therefore break your stacking order or your fixed elements.
const panel = document.querySelector('.panel');
panel.addEventListener('pointerenter', () => {
panel.style.willChange = 'transform';
});
panel.addEventListener('transitionend', () => {
panel.style.willChange = 'auto';
});