Hardware-Accelerated Properties
Part of Core CSS Animation Fundamentals, this topic covers the browser rendering phases that determine whether an animation runs on the GPU compositor thread or falls back to the main thread — and the practical audit workflow to keep every frame cheap.
What hardware acceleration means for CSS animation
When the browser renders a frame it moves through four phases in order: style recalculation, layout, paint, and composite. Most animated CSS properties touch at least layout or paint, which executes on the main thread and competes with JavaScript. Hardware-accelerated properties skip directly to the composite phase: the GPU receives updated transformation matrices and blends pre-rasterized textures independently of anything running on the main thread.
transform (including translate, rotate, scale, and their shorthand forms) and opacity are the only CSS properties guaranteed to run on the compositor thread across all major browsers. Everything else — background-color, width, box-shadow, border-radius when animated — triggers paint or layout and resets the frame budget clock.
Execution model: compositor vs main thread
The compositor thread operates independently of the main JavaScript thread. Once an element is promoted to its own compositor layer, the browser uploads a texture of that element to GPU memory. Subsequent transform or opacity changes send only a new matrix or alpha value to the GPU — the texture is never repainted.
Layer promotion happens in two ways. The browser promotes elements automatically when it detects they need to be isolated (for example, elements with position: fixed or active CSS animations on transform/opacity). Developers can also hint at promotion with will-change: transform or will-change: opacity, which causes the browser to create the layer before the animation starts and avoids the one-frame promotion cost.
Understanding how timing functions and easing curves interact with these matrices is critical: the compositor interpolates keyframe values between frames using the easing function baked into the animation-timing-function. Mismatched or overly complex curves do not cause paint, but they can exceed the per-frame interpolation budget on low-powered devices.
Property and compositing tier reference
| Property | Compositing tier | Rendering phase triggered | Notes |
|---|---|---|---|
transform |
Composite | None | Preferred for all motion and sizing effects |
opacity |
Composite | None | Safe for fade in/out without repaint |
filter (Chromium) |
Composite (partial) | Paint in other browsers | Verify per-browser; do not rely on it cross-browser |
background-color |
Paint | Paint | Use opacity + layered pseudo-elements instead |
box-shadow |
Paint | Paint | Animate opacity of a separate shadow element |
width / height |
Layout | Layout + Paint | Replace with transform: scale() where possible |
top / left |
Layout | Layout + Paint | Replace with transform: translate() |
clip-path |
Paint | Paint | No compositor promotion; minimise animated complexity |
border-radius |
Paint | Paint | Prefer static border-radius with animated transform |
Annotated code examples
Composite-only transition with lifecycle management
/* Intent: scale an interactive card without touching layout or paint. */
.card {
/* will-change pre-allocates a compositor layer before the user hovers */
will-change: transform, opacity;
transition:
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s ease;
}
.card:hover,
.card.is-active {
transform: scale(1.04);
opacity: 1;
}
/* Remove will-change once the element is no longer interactive */
.card.is-complete {
will-change: auto;
}
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
will-change: auto;
}
.card:hover,
.card.is-active {
transform: none;
}
}
Rendering Impact: Composite — transform and opacity never touch layout or paint. will-change: auto frees the GPU texture after completion.
Replacing layout-triggering properties with transform equivalents
/* Intent: slide an off-canvas panel into view without triggering layout. */
/* Anti-pattern: animates left, causes layout recalculation on every frame */
.panel-bad {
position: fixed;
left: -320px;
transition: left 0.35s ease-in-out;
}
.panel-bad.is-open {
left: 0;
}
/* Correct pattern: animates transform, compositor-only */
.panel {
position: fixed;
transform: translateX(-320px);
will-change: transform;
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.panel.is-open {
transform: translateX(0);
}
/* Reset will-change after the transition ends — wire via transitionend event */
.panel.is-settled {
will-change: auto;
}
@media (prefers-reduced-motion: reduce) {
.panel {
transition: none;
will-change: auto;
transform: none;
}
}
Rendering Impact: Composite (correct pattern) vs Layout (anti-pattern). The transform version skips layout and paint entirely.
PerformanceObserver compositor bypass monitor
/* Intent: surface main-thread contention during animation for CI/CD regression tests. */
const monitorCompositing = () => {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (entry.entryType === 'layout-shift' || entry.entryType === 'longtask') {
console.warn(
`Compositor bypass detected: ${entry.entryType}`,
`duration: ${entry.duration?.toFixed(2) ?? 'n/a'}ms`
);
}
});
});
// longtask: JS blocking the main thread >50 ms
// layout-shift: unexpected layout changes that indicate non-composite animation
observer.observe({ entryTypes: ['longtask', 'layout-shift'] });
return () => observer.disconnect();
};
// Attach before animation starts, disconnect in the animationend callback
const stop = monitorCompositing();
element.addEventListener('animationend', stop, { once: true });
Rendering Impact: Main thread (observation only) — this does not alter compositing behaviour, only surfaces when the compositor is bypassed.
Inline SVG: browser rendering phase decision tree
DevTools audit workflow
Auditing compositor behaviour requires a specific sequence in Chrome DevTools. The workflow below applies to any animation that might be causing jank.
-
Open the Rendering panel. In Chrome DevTools, press
Ctrl+Shift+P(orCmd+Shift+Pon Mac) and type “Rendering” to open the drawer. Enable both Layer borders (draws orange borders around compositor layers) and FPS meter. -
Trigger the animation. Hover, click, or script the element that should animate. Watch for the orange border to appear — its presence confirms compositor promotion. If no border appears around the animating element, it is not on its own layer.
-
Record a Performance trace. Switch to the Performance tab, hit Record, trigger the animation, then stop recording. Expand the Main thread flame chart and look for purple Layout or green Paint bars firing during the animation. Any layout or paint in the main thread during an animation is a signal to investigate.
-
Inspect the Layers panel. Open the Layers panel (three-dot menu → More tools → Layers). Select the animated element in the layer list — the right panel shows the layer’s memory footprint. Layers over 4MB on mobile should prompt a review of whether
will-changeis applied too broadly. -
Check for layer explosion. Excessive
will-changedeclarations can create hundreds of unnecessary layers. In the Layers panel, look for elements that are promoted but never animate. Removewill-changefrom any element where it is speculative rather than immediately-needed. -
Validate in Firefox. Open Firefox Developer Tools → Performance → Record. Firefox’s flame chart labels compositor-thread work separately from main-thread work — this is the quickest way to confirm cross-browser behaviour. Pay particular attention to
Rasterizeentries, which indicate unexpected paint promotion.
Failure modes and fixes
Problem: Animating top or left positions causes visible frame drops on scroll.
Root cause: top and left are layout properties. Every frame change triggers a full layout recalculation, which blocks the main thread and competes with scroll event processing.
Fix: Replace with transform: translate(x, y). The visual result is identical but the compositor handles it without touching layout. For keyframe-based movement, express all positional keyframes as transform values.
Problem: will-change applied globally via a utility class causes degraded scroll performance on mid-range Android devices.
Root cause: Each will-change declaration forces the browser to allocate a GPU texture immediately, regardless of whether the element is currently animating. On devices with limited GPU memory, texture upload stalls block rendering.
Fix: Add will-change only in the state immediately preceding animation — via a .is-about-to-animate class added in JavaScript — and remove it in the transitionend or animationend handler.
Problem: A React component toggling className to trigger a CSS animation produces a one-frame flicker before the animation starts.
Root cause: React’s reconciliation writes the new class name synchronously, but the browser paints the intermediate frame before the compositor layer is promoted.
Fix: Wrap the class toggle in requestAnimationFrame to defer style application by one frame, giving the browser time to complete the current paint before promoting the element. See animation state management for full patterns on synchronising JavaScript state with CSS animation lifecycle events.
Problem: backface-visibility: hidden causes unexpected blank frames on Safari when applied to a carousel.
Root cause: backface-visibility: hidden forces layer creation even when the element is not animated. On Safari, overlapping promoted layers can occlude each other depending on stacking context order.
Fix: Remove backface-visibility: hidden from static elements. Apply it only to elements that genuinely undergo 3D rotation, and verify the stacking context order with the WebKit layer inspector.
Problem: Animating box-shadow to indicate focus state causes continuous paint on every frame.
Root cause: box-shadow is a paint property. Any change to its values triggers a repaint of the element and any elements it overlaps.
Fix: Use a ::after pseudo-element with the shadow pre-rendered, and animate only its opacity between 0 and 1. This moves the visual effect to a composite operation while keeping the shadow’s appearance identical.
Accessibility and reduced-motion
Hardware-acceleration optimisation and motion accessibility are separate concerns that must both be addressed. A compositor-only animation is still a motion effect that can trigger vestibular disorders. Every animation on this page must include a prefers-reduced-motion rule that either eliminates the motion or reduces it to a simple opacity fade.
The most maintainable pattern is a global reset at the top of the animation stylesheet:
@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;
will-change: auto !important;
}
}
This resets all transitions and animations to near-instant while retaining any JavaScript state changes that depend on transitionend or animationend events firing. The will-change: auto rule also frees compositor layers that would otherwise persist unnecessarily for users who have disabled motion.
For interactions where the animation communicates state change (a button expanding to show a form), preserve the end state of the animation for reduced-motion users — do not simply remove the effect entirely.
Common pitfalls
- Applying
will-changeto static elements or via a broad CSS selector, causing GPU memory exhaustion on devices with less than 2 GB RAM. - Animating
widthorheightdirectly instead of usingtransform: scale(), forcing a layout recalculation on every frame. - Leaving
will-changeactive after animation completion, causing the browser to retain a separate compositor layer and increasing memory pressure during scroll. - Relying on
transform: translateZ(0)as a universal “GPU hack” without verifying promotion in DevTools — complex stacking contexts can prevent layer isolation even with this hint applied. - Applying
backface-visibility: hiddento all elements in a 3D transform context, which forces layer creation regardless of whether back faces are ever visible. - Animating
clip-pathfor motion effects when atransform-based approach would produce the same visual result at lower rendering cost.
FAQ
Does transform: translateZ(0) always guarantee hardware acceleration?
No. It triggers layer promotion only when the browser’s compositor can allocate a dedicated texture. Elements with overflow: hidden, complex filter stacks, or nested stacking contexts may still trigger paint or layout fallbacks. Always verify with DevTools layer borders rather than assuming promotion has occurred.
When should will-change be removed from an element?
Immediately after the animation completes or the interactive state resolves. Persistent will-change forces the browser to maintain a separate compositor layer and its associated GPU texture, increasing memory overhead and potentially degrading scroll performance on low-end devices. Wire removal to the animationend or transitionend event.
How do I debug jank in hardware-accelerated animations?
Use the Chrome Performance tab to isolate Compositor versus Main thread activity. If the main thread shows Layout or Paint spikes during the animation interval, the animation is likely touching non-composite properties or triggering synchronous DOM reads. Switch to transform/opacity and defer any state reads to requestIdleCallback.
Can filter: blur() run on the compositor thread?
Animating filter in Chromium-based browsers now runs on the compositor for a subset of filter functions including blur, brightness, contrast, and opacity. However, compositor promotion for filter is not guaranteed in Firefox or Safari. Verify in each browser before depending on it for performance-critical animations.
Does clip-path animation trigger layout or paint?
Animating clip-path triggers paint but not layout in modern browsers. It does not run on the compositor thread, so jank is possible on paint-heavy frames. For motion effects where clip-path is not semantically required, a transform-based approach delivers better frame consistency.
Related
- Core CSS Animation Fundamentals — parent section covering the full rendering pipeline
- Avoiding layout thrashing in CSS animations — deep-dive into synchronous DOM read/write patterns
- Timing functions and easing curves — cubic-bezier optimisation aligned with GPU interpolation limits
- Keyframe architecture and state mapping — structuring keyframes to avoid layer invalidation
- Compositor-only property optimisation — performance budgeting strategies that build on these fundamentals