Suggest an editImprove this articleRefine the answer for “CSS will-change property”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**`will-change`** is a CSS performance hint that tells the browser which properties an element will animate, so it can promote that element to a GPU compositor layer before the animation starts. ```css .card:hover { will-change: transform; transform: scale(1.05); } ``` **Key rule:** add it just before the animation starts (via `:hover` or JS), remove it with `will-change: auto` after. Applying it globally creates hundreds of GPU layers and degrades performance instead of helping it.Shown above the full answer for quick recall.Answer (EN)Image**`will-change`** tells the browser which CSS properties an element will animate, so it can promote that element to a GPU compositor layer before the animation starts. ## Theory ### TL;DR - Think of it as preordering at a restaurant: the kitchen starts cooking before you arrive, not after you sit down - Without it: the browser repaints on the CPU each frame, eating into the 16ms budget at 60fps - With it: GPU composites `transform` and `opacity` off the main thread, dropping per-frame latency below 5ms - Add it just before animation starts via `:hover` or JS; remove with `will-change: auto` when done - Works only for compositor-fast properties: `transform`, `opacity`, `filter`, `scroll-position` ### Quick example ```css .card { transition: transform 0.3s ease; /* No will-change: CPU repaints every frame */ } .card:hover { will-change: transform; /* Browser creates a GPU layer on hover */ transform: scale(1.05); /* GPU composites this — main thread stays free */ } /* Open DevTools > Layers panel: one new layer appears on hover, gone after */ ``` When `.card:hover` fires, the browser sees `will-change` before the transition begins. That is enough notice to allocate a compositor layer. The scale animation then runs entirely off the main thread. ### How layer promotion actually works By default, every paint happens on the main thread. A `transform` or `opacity` change without a compositor layer means the CPU recalculates pixels, uploads them to GPU memory, and blends. At 60fps you have 16ms per frame. A busy main thread eats that fast. `will-change` changes that flow. In Chrome's Blink engine, `will-change: transform` triggers a `cc::Layer` allocation. The GPU process rasterizes the element into tiles once, then composites every frame without touching layout or paint. Per-frame cost drops from 50ms+ to under 5ms for transforms and opacity. Each layer costs GPU memory, typically a few MB depending on element size. Fifty layers means 200MB+ on mobile. Chrome keeps a soft budget of around 20 active layers per stacking context and auto-demotes extras after roughly 250ms idle (Chrome 110+). Safari demotes faster, around 60ms. ### When to use - **Hover animations:** set `will-change` on `:hover` so the GPU layer exists when the transition fires - **JS-driven animations:** `element.style.willChange = 'transform'` one [requestAnimationFrame](/questions/request-animation-frame) call before starting the animation - **Modals:** set on open, remove with `will-change: auto` in the `transitionend` handler - **Scroll-driven effects:** only on elements currently in the viewport, never on an entire list at once ### Difference from `transform: translateZ(0)` `transform: translateZ(0)` forces a GPU layer immediately and holds it permanently. It predates `will-change` and was the standard hack before 2015. `will-change` allocates lazily and can be cleaned up with `will-change: auto`. For static elements that never animate, `translateZ(0)` backfires by holding a layer forever. For controlled animations, `will-change` is the better choice because the layer lifecycle is yours to manage. ### Common mistakes **Applying to everything on page load** ```css /* 100 cards = 100+ GPU layers = 500MB RAM, scroll jank */ .card { will-change: transform; } ``` I audited a product grid like this once. Every `.product-card` had `will-change: transform` in the global stylesheet. DevTools Layers showed 80+ active compositor layers and scroll frame rate was stuck at 30fps. Removing the global rule and adding `will-change` on `mouseenter` brought it back to a steady 60fps. ```javascript el.addEventListener('mouseenter', () => { el.style.willChange = 'transform'; }); el.addEventListener('transitionend', () => { el.style.willChange = 'auto'; }); ``` **Forgetting to reset after animation** ```css /* Layer stays allocated even after animation ends */ .animate { will-change: transform; animation: spin 2s; } ``` On iOS Safari this causes a 30% performance drop on subsequent scrolling (WebKit bug 148037). Always reset: call `el.style.willChange = 'auto'` in the `animationend` handler. **Using it on non-composited properties** ```css /* will-change: width gives no GPU compositing benefit */ .bad { will-change: width; transition: width 1s; } ``` Only `transform`, `opacity`, `filter`, and `scroll-position` get compositor promotion. `width`, `height`, `color`, and `margin` do not. The browser simply ignores the hint for those. If you need a width-change effect, fake it with `transform: scaleX()`. **Setting it on scroll lists without limits** ```css /* 50 list items * will-change = memory spike on mid-range Android */ .scroll-list > * { will-change: transform; } ``` Chrome 120+ auto-demotes extras after 250ms idle, but the allocation spike itself causes jank. Apply `will-change` only to items near the viewport via JS. ### Real-world usage - GSAP 3.12+: `gsap.set(el, { willChange: 'transform' })` before any tween - React Spring v9+: `useWillChange` hook pattern for parallax modals (react-spring issue #782) - Framer Motion: manages `will-change` internally for spring animations - Ionic 7: sets `will-change` on modal overlays for iOS Safari PWA smoothness - Chrome DevTools: Gmail compose animation shows `will-change` usage in the Layers panel ### Follow-up questions **Q:** Which CSS properties actually get GPU compositor promotion from `will-change`? **A:** `transform`, `opacity`, `filter`, and `scroll-position`. Properties like `width`, `height`, `top`, `left`, and `color` do not trigger promotion. The browser ignores `will-change` for those. **Q:** What is the difference between `will-change: transform` and `transform: translateZ(0)`? **A:** `translateZ(0)` creates a GPU layer immediately and keeps it permanently. `will-change` allocates lazily and cleans up with `will-change: auto`. For animations you control, `will-change` is cleaner because you own the layer lifecycle. **Q:** How many layers can Chrome handle before performance degrades? **A:** Around 20 active layers per stacking context. Chrome 110+ auto-demotes extras after 250ms idle. Safari does it faster, around 60ms. You can check memory pressure with `performance.measureUserAgentSpecificMemory()`. **Q:** Why does `will-change: auto` not free the GPU layer immediately? **A:** Budget pooling has a minimum idle delay of around 128ms. The browser batches demotions to prevent thrashing. Check the Layers panel in DevTools after triggering `will-change: auto` to confirm the demotion. **Q:** You have a scrollable list of 100 animated cards, all with `will-change: transform`. Mobile users report scroll lag. How do you fix it without `IntersectionObserver`? **A:** Virtualize the list with `react-window` so only 6-10 DOM nodes exist at once. Apply `will-change` to visible items only via a `ResizeObserver` throttle. This cuts active layers from 100 to 3-5. Another option: apply `will-change` on `mouseenter` and remove on `transitionend`, so the layer exists for roughly 300ms per card maximum. ## Examples ### Basic: hover card with GPU compositing ```css .card { width: 200px; padding: 20px; background: #f0f0f0; transition: transform 0.3s ease, box-shadow 0.3s ease; } /* will-change is set BEFORE the transition fires */ .card:hover { will-change: transform; transform: translateY(-4px); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); } ``` The GPU layer gets allocated when `:hover` matches, before the [CSS transition](/questions/css-transitions) starts. The `translateY` animation runs fully on the compositor. Check DevTools Layers panel: one extra layer on hover, gone on mouse-out. ### Intermediate: modal with JS lifecycle management ```javascript const modal = document.querySelector('.modal'); function openModal() { // Allocate GPU layer one frame before animation starts modal.style.willChange = 'transform, opacity'; requestAnimationFrame(() => { modal.classList.add('modal--visible'); }); } function closeModal() { modal.addEventListener('transitionend', () => { // Recycle the layer after animation completes modal.style.willChange = 'auto'; }, { once: true }); modal.classList.remove('modal--visible'); } ``` ```css .modal { opacity: 0; transform: scale(0.92); transition: transform 0.25s ease, opacity 0.25s ease; } .modal--visible { opacity: 1; transform: scale(1); } ``` Setting `will-change` before `requestAnimationFrame` gives the browser one full frame to allocate the layer. The `{ once: true }` option removes the `transitionend` listener automatically, preventing stacked handlers on repeated open/close cycles. ### Advanced: viewport-scoped will-change for long lists ```javascript function manageWillChange(container) { const items = Array.from(container.querySelectorAll('.item')); function update() { items.forEach(item => { const { top, bottom } = item.getBoundingClientRect(); const inViewport = bottom > 0 && top < window.innerHeight; // Promote items near the viewport, demote everything else item.style.willChange = inViewport ? 'transform, opacity' : 'auto'; }); } let ticking = false; window.addEventListener('scroll', () => { if (!ticking) { requestAnimationFrame(() => { update(); ticking = false; }); ticking = true; } }); update(); // Run once on mount } ``` At any scroll position, at most 5-8 layers are active regardless of list length. The `requestAnimationFrame` throttle caps updates at 60 times per second. Chrome 120+ would auto-demote off-screen items after 250ms anyway, but explicit management produces the same behavior in Safari and Firefox.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.