Властивість CSS will-change
will-change повідомляє браузер про те, які CSS-властивості елемента зміняться, щоб він міг виділити GPU compositor layer ще до початку анімації.
Теорія
TL;DR
- Як замовлення страви наперед: кухня готує до твого приходу, а не після
- Без нього: браузер перемальовує через CPU на кожному кадрі, з'їдаючи 16ms бюджету
- З ним: GPU компонує
transformіopacityпоза головним потоком, затримка падає нижче 5ms - Додавай за 1-2 кадри до початку анімації через
:hoverабо JS; прибирай черезwill-change: autoпісля - Працює тільки для compositor-fast властивостей:
transform,opacity,filter,scroll-position
Швидкий приклад
.card {
transition: transform 0.3s ease;
/* Без will-change: CPU перемальовує кожен кадр */
}
.card:hover {
will-change: transform; /* Браузер створює GPU-шар при hover */
transform: scale(1.05); /* GPU компонує — головний потік вільний */
}
/* DevTools > Layers: новий шар з'являється при hover і зникає після */Коли спрацьовує .card:hover, браузер бачить will-change ще до початку transition. Цього достатньо, щоб виділити compositor layer. Анімація масштабу проходить повністю поза головним потоком.
Як насправді працює layer promotion
За замовчуванням браузер малює все на головному потоці. Кожна зміна transform або opacity без compositor layer запускає repaint: CPU перераховує пікселі, відправляє на GPU, де відбувається blending. При 60fps є всього 16ms на кадр. Завантажений головний потік легко з'їдає цей бюджет.
will-change дозволяє браузеру обійти цей потік. В движку Blink Chrome, отримавши will-change: transform, виділяє cc::Layer. GPU-процес растеризує елемент в тайли один раз і компонує кожен кадр, не торкаючись layout і paint. Витрати на кадр падають з 50ms+ до менш ніж 5ms для transform і opacity.
Але кожен шар займає GPU-пам'ять, зазвичай кілька MB залежно від розміру елемента. 50 шарів на мобільному це вже 200MB+. Chrome тримає soft budget приблизно на 20 активних шарів на stacking context і автоматично прибирає зайві через 250ms простою (Chrome 110+). Safari прибирає швидше, приблизно за 60ms.
Коли використовувати
- Hover-анімації: встановлюй
will-changeна:hover, щоб GPU-шар вже існував коли запускається transition - Анімації через JS:
element.style.willChange = 'transform'за один requestAnimationFrame до старту анімації - Модалки: встановлюй при відкритті, прибирай через
will-change: autoв обробникуtransitionend - Scroll-driven ефекти: тільки для елементів у viewport, не для всього списку одразу
Різниця з transform: translateZ(0)
transform: translateZ(0) відразу і назавжди створює GPU-шар. Цей підхід існував до появи will-change і був поширеним хаком до 2015 року. will-change виділяє шар ліниво і прибирається через will-change: auto. Для статичних елементів, які не анімуються, translateZ(0) може навпаки зашкодити, тримаючи шар вічно. Для керованих анімацій will-change краще, бо ти сам контролюєш lifecycle шару.
Типові помилки
Додавати до всього при завантаженні сторінки
/* 100 карток = 100+ GPU шарів = 500MB RAM, лагаючий скрол */
.card { will-change: transform; }Я одного разу аудитив продуктову сітку саме з такою проблемою. Кожна .product-card мала will-change: transform в глобальному стилі, DevTools Layers показував 80+ активних compositor шарів, а frame rate при скролі застряг на 30fps. Прибрав глобальне правило і додав will-change на mouseenter — скрол повернувся до стабільних 60fps.
el.addEventListener('mouseenter', () => {
el.style.willChange = 'transform';
});
el.addEventListener('transitionend', () => {
el.style.willChange = 'auto';
});Не прибирати після анімації
/* Шар залишається виділеним навіть після завершення анімації */
.animate { will-change: transform; animation: spin 2s; }На iOS Safari це дає 30% падіння продуктивності при наступному скролі (WebKit bug 148037). Завжди скидай: el.style.willChange = 'auto' в обробнику animationend.
Використовувати для non-composited властивостей
/* will-change: width не дає жодної GPU-оптимізації */
.bad { will-change: width; transition: width 1s; }Compositor promotion отримують тільки transform, opacity, filter і scroll-position. Властивості width, height, color, margin не переміщуються на GPU. Браузер просто ігнорує підказку для них. Якщо потрібна анімація ширини, імітуй її через transform: scaleX().
Встановлювати для великих списків без обмежень
/* 50 елементів * will-change = пік пам'яті на середньому Android */
.scroll-list > * { will-change: transform; }Chrome 120+ автоматично прибере зайві шари через 250ms простою, але сам пік виділення пам'яті вже викличе jank. Застосовуй will-change тільки до елементів поблизу viewport через JS.
Де це зустрічається
- GSAP 3.12+:
gsap.set(el, { willChange: 'transform' })перед будь-яким tween - React Spring v9+: патерн з
useWillChangeхуком для parallax-модалок (react-spring issue #782) - Framer Motion: автоматично керує
will-changeдля spring анімацій - Ionic 7:
will-changeна modal overlays для плавності в iOS Safari PWA - Chrome DevTools: у DevTools Layers видно
will-changeв анімації Gmail compose-вікна
Follow-up питання
Q: Для яких CSS-властивостей will-change реально дає GPU compositor promotion?
A: transform, opacity, filter і scroll-position. Властивості width, height, top, left, color promotion не отримують. Для них браузер просто ігнорує підказку.
Q: Яка різниця між will-change: transform і transform: translateZ(0)?
A: translateZ(0) відразу і назавжди створює GPU-шар. will-change виділяє ліниво і прибирається через will-change: auto. Для анімацій, якими ти керуєш, will-change кращий, бо lifecycle шару в твоїх руках.
Q: Скільки will-change шарів Chrome тримає до початку деградації?
A: Приблизно 20 активних шарів на stacking context. Chrome 110+ автоматично прибирає зайві через 250ms простою. Safari робить це швидше, за 60ms. Тиск на пам'ять можна перевірити через performance.measureUserAgentSpecificMemory().
Q: Чому will-change: auto не відразу звільняє GPU-шар?
A: Браузер батчує демоції з мінімальною затримкою близько 128ms, щоб не викликати thrashing. Для перевірки дивись на Layers panel у DevTools після встановлення will-change: auto.
Q: У тебе список зі 100 картками, у всіх will-change: transform і hover-анімації. Мобільні користувачі скаржаться на лаги скролу. Як виправити без IntersectionObserver?
A: Віртуалізуй список через react-window, щоб у DOM було тільки 6-10 вузлів. Застосовуй will-change тільки до видимих елементів через ResizeObserver з throttle. Це скорочує активні шари з 100 до 3-5. Другий варіант: встановлювати will-change в mouseenter і прибирати в transitionend, щоб шар існував максимум 300ms на картку.
Приклади
Базовий: hover-картка з GPU compositing
.card {
width: 200px;
padding: 20px;
background: #f0f0f0;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
/* will-change встановлюється ДО початку transition */
.card:hover {
will-change: transform;
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}Браузер виділяє GPU-шар у момент спрацювання :hover, ще до початку CSS transition. Анімація translateY проходить повністю на compositor. Відкрий DevTools Layers: при hover з'явиться один зайвий шар, при відведенні миші зникне.
Середній: модалка з lifecycle управлінням через JS
const modal = document.querySelector('.modal');
function openModal() {
// Виділяємо GPU-шар за один кадр до початку анімації
modal.style.willChange = 'transform, opacity';
requestAnimationFrame(() => {
modal.classList.add('modal--visible');
});
}
function closeModal() {
modal.addEventListener('transitionend', () => {
// Повертаємо шар після завершення анімації
modal.style.willChange = 'auto';
}, { once: true });
modal.classList.remove('modal--visible');
}.modal {
opacity: 0;
transform: scale(0.92);
transition: transform 0.25s ease, opacity 0.25s ease;
}
.modal--visible {
opacity: 1;
transform: scale(1);
}Встановлення will-change перед requestAnimationFrame дає браузеру один повний кадр на виділення шару. Опція { once: true } прибирає слухач автоматично і уникає накопичення обробників при повторних відкриттях.
Просунутий: viewport-scoped will-change для довгих списків
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 елементи поблизу viewport, demote решту
item.style.willChange = inViewport ? 'transform, opacity' : 'auto';
});
}
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => { update(); ticking = false; });
ticking = true;
}
});
update(); // Викликаємо одразу при монтуванні
}Незалежно від довжини списку, в будь-який момент скролу активних шарів буде не більше 5-8. requestAnimationFrame обмежує частоту оновлень до 60 разів на секунду. Chrome 120+ і сам би прибрав позаекранні шари через 250ms, але явне керування дає однакову поведінку в Safari і Firefox.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.