Все, что вам нужно знать о CSS-свойстве will-change

Вступление

Если вы когда-либо замечали «подёргивание» в браузерах на движке WebKit в процессе выполнения определённых CSS-операций, особенно трансформаций и анимации, то, скорее всего, вы уже слышали термин «аппаратное ускорение».

Центральный процессор, графический процессор и аппаратное ускорение

Если коротко, то аппаратное ускорение значит, что графический процессор помогает браузеру отрисовывать страницу путем выполнения некоторых сложных задач, вместо того, чтобы свалить это всё на центральный процессор. Когда CSS-операция получает аппаратное ускорение, обычно происходит скачок скорости, отрисовка страницы выполняется быстрее.

Центральный процессор размещён на материнской плате компьютера. Он занимается выполнением всех процессов и является мозгом компьютера. Графический процессор размещён на графической карте компьютера и отвечает за обработку и отрисовку графики. Больше того, графический процессор создан специально для выполнения сложных математических и геометрических вычислений, требуемых для отрисовки графики. Следовательно, если переложить груз некоторых операций на графический процессор, можно получить существенное повышение быстродействия и снизить загрузку центрального процессора на мобильных устройствах.

Аппаратное ускорение (или же ускорение графики) основывается на иерархической модели, используемой браузером при отрисовке страницы. Когда с элементом на странице проводятся определённые операции (такие как 3D-трансформации), элемент перемещается на свой собственный «слой», где он может отрисовываться независимо от остальной страницы и быть выведен на экран позже. Это изолирует отрисовку содержимого так, что остальную часть страницы не нужно отрисовывать повторно если трансформация элемента — это единственное изменение между фреймами. Это часто положительно влияет на скорость отрисовки. Здесь также следует отметить, что только 3D-трансформации могут занимать отдельный слой, с 2D-трансформациями этого не происходит.

Ускорение графики при CSS-анимации, трансформациях и переходах не происходит автоматически, они выполняются довольно медленным движком визуализации браузера. Тем не менее, некоторые браузеры поддерживают возможность аппаратного ускорения при использовании определённых CSS-свойств для обеспечения лучшего быстродействия при отрисовке. Например, свойство opacity является одним из немногих CSS-свойств, которые могут быть основательно ускорены, так как графическому процессору нетрудно его обработать. Собственно, когда вы выполняете манипуляции с прозрачностью через CSS-переход или анимацию, браузеру хватает ума переложить выполнение этой задачи на графический процессор, что его значительно ускоряет. Из всех возможностей CSS, изменение прозрачности является наиболее быстрым и у вас не должно с ней возникнуть никаких проблем. Ещё одной операцией, которая обычно подлежит аппаратному ускорению, является 3D-трансформация на основе CSS.

Старый подход: приём translateZ() (или translate3d())

Уже довольно давно, чтобы обмануть браузер и заставить его применить аппаратное ускорение для операций анимации и трансформаций, используется так называемый приём translateZ() (или translate3d()). Это происходит путём добавления простой 3D-трансформации для элемента, который не будет трансформироваться в трёхмерном пространстве. Например, чтобы получить аппаратное ускорение для двухмерной анимации элемента, нужно добавить это простое правило:

transform: translate3d(0, 0, 0);

В результате аппаратного ускорения операции создается так называемый слой компоновки, на котором происходит загрузка и компоновка графическим процессором. Однако принудительное создание такого слоя не всегда является подходящим решением для некоторых узких мест в быстродействии на странице. Приёмы создания слоев могут увеличить скорость страницы, однако определённой ценой: они потребляют оперативную память системы и ресурсы графического процессора (довольно ограниченные на мобильных устройствах) и в большом количестве могут привести к негативных последствиям (особенно на мобильных устройствах). Использовать их нужно с умом и следует предварительно убедиться, что аппаратное ускорение операции действительно положительно повлияет на быстродействие страницы, что узкое место в быстродействии не является следствием выполнения какой-либо другой операции на странице.

Чтобы избежать применения приёмов по созданию слоёв, было введено новое CSS-свойство, позволяющее заранее предупредить браузер о потенциальных изменениях элемента, тем самым давая ему возможность заранее оптимизировать выполнение этих изменений, выполнив потенциально затратные приготовления к операциям вроде анимации до того, как они будут начаты. Это новое свойство — will-change.

Новый подход: чудесное свойство will-change

Свойство will-change позволяет заранее проинформировать браузер об изменениях, которые возможно будут применены к элементу, так что он может провести оптимизацию до того, как они понадобятся, таким образом позволяя избежать нетривиальных затрат на запуск операций, негативно влияющих на способность страницы к быстрому реагированию. Элементы смогут изменяться и отрисовываться быстрее, страница получит возможность аккуратно обновиться, и работать с ней станет приятнее.

Например, при применении к элементу 3D-трансформации в CSS, для этого элемента и его содержимого может быть выделен новый слой, как мы упоминали ранее, до того как он будет отрисован на экране. Однако, настройка элемента в новом слое является относительно затратной операцией, которая может отложить начало анимации трансформации на заметную долю секунды, что приводит к заметному «подёргиванию».

Чтобы избежать задержки, можно сообщить браузеру об изменениях до того, как они произойдут. В таком случае у него будет некоторое время чтобы к ним приготовиться. Так что когда они произойдут, слой для элемента будет готов, анимация может быть выполнена, элемент — отрисован, а страница обновлена очень быстро.

Использовать will-change, чтобы сообщить браузеру о предстоящей трансформации очень просто, нужно только добавить следующее правило для элемента, который будет трансформирован:

will-change: transform;

Можно также сообщить браузеру о своем намерении изменить позицию элемента относительно прокрутки (расположение элемента в видимой части прокручиваемого окна и то, какая его часть является видимой в этом окне), его содержимое или же одно и больше значений для его CSS-свойств путем указания названия свойств, которые будут изменены. Если вы планируете изменить несколько значений или параметров элемента, можно указать список значений через запятую. Например, если элемент будет анимирован и передвинут (изменится его позиция), об этом можно сообщить браузеру следующим образом:

will-change: transform, opacity;

Если сообщить браузеру о том, что именно вы собираетесь изменить, он сможет наилучшим образом оптимизировать эти изменения. Очевидно это лучший способ добиться увеличения скорости страницы без использования хитростей и принудительного создания слоёв, которые могут вообще не пригодиться.

Влияет ли will-change на элемент как то ещё?

Ответ: и да, и нет — всё зависит от свойств, которые вы указываете. Если какое-либо значение свойства, отличное начального, привело бы к созданию стекового контекста для элемента, то указание этого свойства в will-change создаст его.

Например, если для свойств clip-path и opacity установлены значения, отличные от значений по умолчанию, это приведет к созданию стекового контекста для элемента, к которому они применены. Следовательно, использование одного из этих свойств или обеих в качестве значений для will-change создаст стековый контекст до того, как изменение собственно произойдёт. То же касается других свойств, создающих стековый контекст для элемента.

Кроме того, некоторые свойства могут привести к созданию содержащего блока для элементов с фиксированным позиционированием. Например, при трансформации элемента создаётся содержащий блок для всех его дочерних элементов с прописанным позиционированием, даже тех, для которых указано position: fixed. Если свойство ведёт к созданию содержащего блока, указание его в качестве значения для will-change также ведёт к генерации содержащего блока для элементов с фиксированным позиционированием.

Кроме вышеперечисленного свойство will-change никак прямым образом не влияет на элемент, для которого применяется. Оно всего лишь является подсказкой по отрисовке для браузера, позволяющим ему подготовиться к выполнению изменений, которые ожидаются для этого элемента. У него нет прямого влияния на элемент кроме создания стекового контекста и блока-обёртки в ситуациях, описанных выше.

Использование will-change: что стоит и не стоит делать

Зная о возможностях will-change, возникает соблазн предположить: «Пусть браузер просто оптимизирует ВСЁ!». На первый взгляд логично, да? Кто бы не хотел чтобы для всех запланированных им изменений проводилась предварительная оптимизация и затем можно было бы просто при первом желании провести эти изменения без проблем?

При всей мощности и величии will-change, для него действует то же правило, что и для всех источников силы: с большей силой приходит бо́льшая ответственность. will-change следует использовать с умом, иначе результатом могут стать удары по быстродействию, которые могут привести к полному падению страницы.

Как и у любых подсказок по быстродействию, у will-change есть свои побочные эффекты, которые нельзя обнаружить напрямую (ведь по сути это всего лишь способ скрытого общения с браузером), так что свойство может оказаться довольно коварным. Вот некоторые моменты, о которых стоит помнить, когда используете это свойство, чтобы убедиться, что вы делаете это с максимальной пользой и в то же время без потенциального вреда от его неправильного употребления.

Не используйте will-change для объявления изменений слишком большого количества свойств или элементов

Как я уже упоминал, может показаться очень соблазнительным просто приказать браузеру провести оптимизацию для потенциальных изменений всех свойств всех элементов; так что на первый взгляд добавление следующего правила в стили может показаться логичным:

*,
*::before,
*::after {
    will-change: all;
}

Хотя такое правило и выглядит привлекательно (именно так поначалу оно выглядело для меня и казалось вполне логичным), на самом деле оно может принести большой вред, к тому же является недопустимым. Кроме того что ключевое слово all является недопустимым значением для will-change (о допустимых и недопустимых значениях мы поговорим дальше), такое общее правило не принесло бы никакой пользы. Видите ли, браузер итак уже пытается провести все возможные меры по оптимизации в силу своих возможностей (помните мы говорили о прозрачности и 3D-трансформациях?), так что явное указание предпринять такие меры ничего в общем не меняет и никак не помогает. Собственно говоря, это даже может существенно навредить, так как некоторые более значимые мероприятия по оптимизации, которые могут быть привязаны к will-change, в результате ведут к большому потреблению ресурсов и если их использовать слишком активно таким вот образом, это приведёт к замедлению или падению страницы.

Другими словами, принуждение браузера к отслеживанию всех изменений, которые могут произойти или не произойти — это плохая идея, несущая больше вреда чем пользы. Не делайте так.

Дайте браузеру достаточно времени

Свойство will-change (перевод с англ. — «изменится» — прим. переводчика) получило такое название по очевидной причине: оно предназначено для информирования об изменениях которые произойдут, а не об изменениях которые уже происходят. Используя will-change, мы просим браузер провести подготовку к осуществлению изменений, о которых мы объявляем, и для того чтобы это произошло, браузеру нужно определённое количество времени для проведения такой подготовки, чтобы когда изменения будут осуществлены, оптимизация была применена без отлагательств.

Прописывать will-change для элемента непосредственно перед тем, как он будет изменён, практически бесполезно. (Вполне может быть, что это даже хуже, чем если не прописать его вообще. Вы можете понести затраты на создание нового слоя тогда как то, для чего производится анимация, не соответствует требованиям для создания нового слоя!). Например, если изменение происходит при наведении курсора, то это:

.element:hover {
    will-change: transform;
    transition: transform 2s;
    transform: rotate(30deg) scale(1.5);
}

…говорит браузеру, что нужно подготовиться к изменению, которое уже происходит, что само по себе бесполезно и даже в некотором роде противоречит концепции, что лежит в основе will-change. Вместо этого вам нужно найти способ предусмотреть хотя бы слегка наперёд что что-то будет изменено и установить will-change заранее.

Например, если элемент будет изменен при клике, то установка will-change при наведении курсора на этот элемент даёт браузеру достаточно времени, чтобы подготовиться к этому изменению. Времени между наведением курсора на элемент и собственно кликом по нему достаточно для браузера, чтобы подготовиться, так как человек реагирует относительно медленно. Это даёт браузеру около 200 мс перед тем, как изменение произойдёт.

.element {
    /* правила */
   transition: transform 1s ease-out;
}
.element:hover {
    will-change: transform;
}
.element:active {
    transform: rotateY(180deg);
}

Но что если вы рассчитываете, что изменение должно произойти при наведении курсора, а не при клике по элементу? Тогда, как уже говорилось, запись, представленная выше, будет бесполезной. В таком случае часто всё же возможно найти какой-либо способ предсказать действие до его осуществления. Например, наведение на родительский элемент изменяемого элемента должно дать нам достаточную фору:

.element {
    transition: opacity .3s linear;
}
/* объявление изменений для элемента, когда мышка наводится на его родительский элемент */
.ancestor:hover .element {
    will-change: opacity;
}
/* применение изменения, когда мышь наведена на элемент */
.element:hover {
    opacity: .5;
}

Тем не менее, наведение на родительский элемент не всегда является гарантией того, что с дочерним элементом будет проведено взаимодействие, так что можно к примеру установить will-change когда в вашем приложении станет активной область просмотра или когда элемент находится в видимой части области просмотра, что повышает его шансы на взаимодействие.

Удаление will-change после проведения изменений

Подготовка, проводимая браузером для изменений, которые должны случиться, обычно является затратной и, как мы упоминали ранее, может занять большое количество ресурсов. Как правило, браузер пытается как можно быстрее удалить все следы подготовки к изменениям и вернуться к нормальному режиму работы. Однако, will-change отменяет такое поведение, сохраняя следы подготовки на более длительное время, чем это сделал бы браузер в противном случае.

В связи с этим, не забывайте удалять will-change после того, как произойдет изменение указанных в нем свойств, чтобы браузер мог освободить зарезервированные ресурсы.

Если will-change прописано в стилях, его невозможно удалить, поэтому почти всегда рекомендуется устанавливать и удалять его с помощью JavaScript. С помощью скриптов, можно сообщить браузеру об изменениях и затем удалить will-change после завершения этих изменений с помощью соответствующего обработчика событий. Например, так же как мы делали в предыдущей части, можно перехватить событие наведения на элемент (или его родительский элемент) и установить will-change для mouseenter. Если для вашего элемента выполняется анимация, можно перехватить событие завершения анимации с помощью события DOM animationEnd и удалить will-change при срабатывании animationEnd.

// Грубый обобщенный пример
// Получаем доступ к элементу, который будет анимирован при клике по нему, например
var el = document.getElementById('element');
 
// Устанавливаем will-change при наведении на элемент
el.addEventListener('mouseenter', hintBrowser);
el.addEventListener('animationEnd', removeHint);
 
function hintBrowser() {
    // Свойства, для которых будут выполнены изменения в блоке
    // описания анимации, к которым можно подготовиться
    this.style.willChange = 'transform, opacity';
}
 
function removeHint() {
    this.style.willChange = 'auto';
}

Крейг Баклер (Craig Buckler) написал статью о перехвате событий CSS-анимации в JavaScript, на которую вам стоит взглянуть если этот процесс вам незнаком. Кроме того, на сайте CSS-Tricks есть статья об управлении CSS-анимацией и переходами, на которую также неплохо было бы взглянуть.

Используйте will-change в стилях умеренно

Как мы увидели в предыдущей части, will-change может быть использовано, чтобы подсказать браузеру изменения, которые произойдут с элементом через несколько миллисекунд. Это один из тех случаев, когда объявление will-change в стилях целесообразно. Хотя и рекомендуется устанавливать и удалять will-change с помощью JavaScript, существуют ситуации, в которых объявление этого свойства в стилях (и его последующее сохранение) уместно.

Одним из примеров является установка will-change для небольшого количества элементов, с которыми пользователь взаимодействует периодически и которые должны отвечать на действия пользователя мгновенно. Ограниченное количество элементов означает, что подготовка браузера не будет чересчур обременительной и следовательно не принесёт существенного вреда. Например, как при трансформации выезжающей боковой панели по запросу пользователя. Подойдёт следующее правило:

.sidebar {
    will-change: transform;
}

Еще один пример — использование will-change для элемента, который изменяется практически постоянно, например для элемента, который реагирует на движение курсора мыши пользователя и двигается вместе с ним. В таком случае, достаточно указать значение will-change в стилях, так как оно достоверно указывает на то, что элемент будет регулярно или постоянно изменяться и оптимизация этих изменений должна сохраняться.

.annoying-element-stuck-to-the-mouse-cursor {
    will-change: left, top;
}

Значения свойства will-change

Свойство will-change принимает одно из четырёх возможных значений: auto, scroll-position, contents и <custom-ident>.

Значение <custom-ident> используется для указания имени (или имён) одного или больше свойств, для которых ожидаются изменения. Несколько свойств разделяются запятыми. Примеры will-change с указанными именами свойств:

will-change: transform;
will-change: opacity;
will-change: top, left, bottom, right;

В дополнение к ключевым словам, которые обычно исключаются для <custom-ident>, значение <custom-ident> не принимает также ключевые слова will-change, none, all, auto, scroll-position и contents. Итак, как мы отметили в начале статьи, объявление will-change: all недопустимо и будет проигнорировано браузером.

Значение auto не указывает никакого конкретного свойства, то есть браузер не будет осуществлять никакой особенной подготовки кроме той, которую выполняет обычно.

Значение scroll-position обозначает, что в ближайшее время вы ожидаете изменение позиции элемента относительно прокрутки. Это значение является полезным, так как при его применении браузер готовится и отрисовывает содержимое помимо того, который виден в прокручиваемом окне. Зачастую браузеры отрисовывают только содержимое отображаемое в прокручиваемом окне и некоторую часть содержимого, которое находится за его границами, с целью добиться баланса между экономией памяти и времени на отрисовке и красивой прокруткой. Когда указано will-change: scroll-position, браузер уделит больше внимания подготовке отрисовки плавной длительной и/или быстрой прокрутки.

Значение contents обозначает, что ожидается изменение содержимого элемента. Браузеры обычно кэшируют отрисовку элементов на некоторое время, так как большинство содержимого изменяется не так уж часто или же изменяется только его размещение. Это значение воспринимается браузером как сигнал к тому, что для элемента не стоит осуществлять полноценное кэширование или же кэширование элемента должно избегаться вообще, так как если его содержимое постоянно меняется, хранение этого содержимого в кэше будет бесполезной тратой времени, так что браузер просто прекращает кэширование и отрисовывает элемент с ноля при каждом изменении его содержимого.

Как упоминалось выше, указание некоторых свойств для will-change не даст никакого результата, так как браузер никак не оптимизирует процесс их изменения. Указывать эти свойства безопасно, просто это не даст никакого результата. Другие свойства могут привести к созданию стекового контекста (opacity, clip-path, и т.д.) и блоков-обёрток.

Поддержка браузеров

На момент написания статьи свойство will-change поддерживали Chrome Canary 36+, Opera Developer 23+ и Firefox Nightly. Кроме того, его поддержку предлагают добавить в стабильные версии браузеров. Судя по всему оно скоро будет поддерживаться всеми современными браузерами.

Заключение

Свойство will-change — это инструмент, который поможет нам писать оптимизированный и сверхбыстрый код без применения хаков, и наделять производительность определенных CSS-операции большей значимостью. Однако, как всегда, с большей силой приходит бо́льшая ответственность, will-change является одним из тех свойств, которые не следует воспринимать легкомысленно, но нужно использовать с умом. В этом месте я процитирую Таба Аткинса (Tab Atkins Jr.), редактора спецификации will-change:

Прописывайте will-change только для тех свойств, которые действительно будут изменены и для элементов над которыми эти изменения буду произведены. И удаляйте его после завершения изменений.

Спасибо, что прочитали эту статью!

Огромное спасибо Полу Льюису (Paul Lewis), что просмотрел эту статью и поделился своими впечатлениями, Табу Аткинсу за поддержку и ответы, а также Брюсу Лоусону (Bruce Lawson) и Матиасу Байненсу (Mathias Bynens) за рецензирование статьи.

Логотип компании «Одноклассники»

Статья переведена благодаря спонсорской поддержке компании «Одноклассники».