- PVSM.RU - https://www.pvsm.ru -
Думаю, все уже знают, что современные браузеры умеют рисовать некоторые части страницы на GPU. Особенно это заметно на анимациях. Например, анимация, сделанная с помощью CSS-свойства transform выглядит гораздо приятнее и плавнее, чем анимация, сделанная через top/left. Однако на вопрос «как правильно делать анимации на GPU?» обычно отвечают что-то вроде «используй transform: translateZ(0) или will-change: transform». Эти свойства уже стали чем-то вроде zoom: 1 для IE6 (если вы понимаете, о чём я ;) для подготовки слоя для анимации на GPU или композиции (compositing), как это предпочитают называть разработчики браузеров.
Однако очень часто анимации, которые красиво и плавно работали на простых демках, вдруг неожиданно начинают тормозить на готовом сайте, вызывают различные визуальные артефакты или, того хуже, приводят к крэшу браузера. Почему так происходит? Как с этим бороться? Давайте попробуем разобраться в этой статье.
Самое важное, что хочется сказать прежде, чем мы приступим к изучению деталей GPU-композиции: это всё один огромный хак. Вы не найдёте в спецификациях W3C [1] (по крайней мере пока) описания процесса GPU-композиции, способов явного переноса элемента на отдельный слой или даже самого режима композиции. Это всего лишь способ ускорить некоторые типовые задачи и каждый разработчик браузеров делает это по-своему. Всё, что вы прочитаете в этой статье — ни в коем случае не официальное объяснение, а результаты экспериментов и наблюдений, приправленных здравым смыслом и знаниями о работе некоторых подсистем браузера. Что-то может оказаться неправдой, а что-то изменится со временем — я вас предупредил!
Чтобы правильно подготовить страницу к GPU-анимации, очень важно не столько следовать советам, найденным интернете или в этой статье, сколько понимать, как это работает внутри браузера.
Предположим, у нас есть страница с элементами A и B, у которых указано position: absolute и разный z-index. Браузер отрисует всю страницу на CPU, отправит полученное изображение на GPU, а оттуда оно попадёт к нам на экран.
<style>
#a, #b {
position: absolute;
}
#a {
left: 30px;
top: 30px;
z-index: 2;
}
#b {
z-index: 1;
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>

Мы решили анимировать перемещение элемента A через CSS-свойство left c помощью CSS Animations:
<style>
#a, #b {
position: absolute;
}
#a {
left: 10px;
top: 10px;
z-index: 2;
animation: move 1s linear;
}
#b {
left: 50px;
top: 50px;
z-index: 1;
}
@keyframes move {
from { left: 30px; }
to { left: 100px; }
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>

В этом случае на каждый кадр анимации браузер со стороны CPU пересчитывает геометрию элементов (reflow), отрисовывает новое изображение с актуальным состоянием страницы (repaint), так же отправляет его на GPU, после чего оно отображается на экране. Мы знаем, что repaint — довольно дорогая операция, однако все современные браузеры достаточно умны, чтобы перерисовывать не всё изображение целиком, а только изменившиеся части. И делают они это достаточно быстро, но анимациям всё равно не хватает плавности.
Пересчёт геометрии и перерисовка, хоть и частичная, всей страницы на каждый шаг анимации: выглядит как очень трудоёмкая операция, особенно на больших и сложных сайтах. Гораздо эффективнее было бы один раз нарисовать два изображения: элемент A и саму страницу без элемента A, а потом просто перемещать эти изображения друг относительно друга. Иначе говоря, нужно делать композицию закэшированных изображений элементов. И именно с такой задачей лучше всего справляется GPU. Более того, он умеет это делать с субпиксельной точностью, что и придаёт ту самую плавность анимации.
Чтобы применить оптимизацию с композицией, браузер должен быть уверен, что анимируемые CSS-свойства:
Со стороны может показаться, что свойства top и left вместе с position: absolute/fixed не зависят от внешних факторов, но это не так. Например, свойство left может принимать значения в процентах, которые зависят от размера .offsetParent, а также единицы em, vh и т.д., которые зависят от окружения. Поэтому именно CSS-свойства transform и opacity подходят под описание.
Переделаем нашу анимацию: вместо left будем анимировать transform:
<style>
#a, #b {
position: absolute;
}
#a {
left: 10px;
top: 10px;
z-index: 2;
animation: move 1s linear;
}
#b {
left: 50px;
top: 50px;
z-index: 1;
}
@keyframes move {
from { transform: translateX(0); }
to { left: translateX(70px); }
}
</style>
<div id="#a">A</div>
<div id="#b">B</div>
Обратите внимание на код. Мы декларативно описали всю анимацию: её начало, конец, длительность и т.д. А это позволяет браузеру ещё до начала анимации определить, какие именно CSS-свойства элемента будут меняться. Увидев, что среди этих свойств нет тех, что влияют на reflow/repaint, браузер может применить оптимизацию с композицией: нарисовать два изображения и передать их GPU.

Преимущества такой оптимизации:
Казалось бы, всё просто и понятно, какие могут возникнуть проблемы? Давайте рассмотрим, что на самом деле делает такая оптимизация.
Возможно, для некоторых это будет открытием, но GPU является отдельным компьютером. Да-да, неотъемлемая часть всех современных устройств на самом деле является самостоятельной подсистемой со своими процессорами, памятью и способами обработки информации. И браузер, как и любая другая программа или игра, вынужден общаться с GPU именно как с отдельным устройством.
Чтобы лучше понять это, просто вспомните AJAX. Например, вам нужно зарегистрировать пользователя по данным, которые он ввёл в форме авторизации. Вы не можете сказать удалённому серверу «эй, возьми данные из вот этих полей и ещё вон ту переменную и сохрани их в базе», потому что у сервера нет доступа к памяти браузера. Вместо этого вы собираете необходимые данные со страницы в некую полезную нагрузку с простым форматом данных (например, JSON) и отправляете её на сервер.
То же самое происходит и во время композиции. Так как GPU, по сути, является удалённым сервером, браузер со стороны CPU вынужден сначала подготовить специальную полезную нагрузку, а затем отправить её на устройство. Да, GPU находится совсем рядом c CPU, однако если 2 секунды на отправку и получение ответа через AJAX зачастую являются вполне приемлемыми, то лишние 3–5 миллисекунд на передачу данных на GPU могут серьёзно ухудшить качество анимации.
Что из себя представляет полезная нагрузка для GPU? Как правило, это изображения слоёв и дополнительные инструкции, которые определяют размеры слоёв, их расположение друг относительно друга, инструкции для анимации и т.д. Вот как приблизительно выглядит процесс создания нагрузки и её передачи на GPU:
Таким образом, каждый раз, когда вы добавляете магическое transform: translateZ(0) или will-change: transform элементу, вы запускаете весь это процесс. Вы уже знаете, что repaint является достаточно ресурсоёмкой задачей. Но в данном случае всё ещё хуже: довольно часто браузер не может применить инкрементальный repaint и перерисовать только изменившуюся часть. Он должен заново отрисовать те части, которые были скрыты новым слоем:

Давайте вернёмся к нашему примеру с элементами A и B. Ранее мы анимировали элемент A, который находится поверх всех остальных элементов на странице. В результате получили композицию из двух слоёв: слой с A и слой с B и фоном страницы.
А теперь поменяем задачу: будем анимировать элемент B…

…и у нас возникает логическая проблема. Элемент B должен быть на отдельном композитном слое, финальная композиция изображения, которое увидит пользователь, происходит на GPU. Но элемент A, который мы вообще никак не трогаем, визуально должен находиться поверх элемента B.
Вспоминаем One Big Disclaimer — в CSS нет специального режима для GPU-композиции, это просто оптимизация для решения специфических задач. Мы должны получить элементы A и B именно в том порядке, который был задан через z-index. Как в этом случае должен поступить браузер?
Совершенно верно: он перенесёт элемент A на отдельный композитный слой! Добавив тем самым ещё один тяжёлый repaint:

Это называется неявная композиция: один или несколько не-композитных элементов, которые по
z-indexнаходятся выше композитного элемента, также становятся композитными, то есть отрисовываются в отдельное изображение, которое затем отправляется на GPU.
С неявной композиции вы будете сталкиваться гораздо чаще, чем думаете: браузер выносит элемент на композитный слой по множеству причин:
translate3d, translateZ и т.д.<video>, <canvas>, <iframe>.transform и opacity через Element.animate().transform и opacity через СSS Transitions и Animations.position: fixed.will-change [2].filter [3].Подробнее можете посмотреть в файле CompositingReasons.h [4] проекта Chromium.
На первый взгляд может показаться, что главная проблема у GPU-анимаций — неожиданные тяжёлые repaint. Но это не так. Самой большой проблемой является…
И вновь вспоминаем, что GPU — это отдельный компьютер. Отрисованные изображения слоёв нужно не только передать на GPU, но и хранить там, чтобы потом красиво анимировать.
Сколько весит изображение одного слоя? Давайте рассмотрим пример. Попробуйте угадать размер обычного прямоугольника, размером 320×240, залитого сплошным цветом #ff0000.

Обычно веб-разработчики думают так: «это одноцветное изображение… сохраню-ка я его в PNG и проверю размер, должен быть меньше килобайта». И будут правы: это изображение действительно весит всего 104 байта в PNG.
Но проблема в том, что PNG (как и JPEG, GIF и т.д.) — это формат хранения и передачи данных. Чтобы отрисовать такое изображение на экране, компьютер должен распаковать его и представить в виде массива пикселей. Таким образом, наше изображение в памяти компьютера будет занимать 320×240×3 = 230 400 байт. То есть мы умножаем ширину изображения на его высоту — так мы получим количество пикселей. Затем количество пикселей умножаем на 3, так как цвет каждого пикселя описывается 3-мя байтами: RGB. Если бы изображение было полупрозрачным, мы бы умножили на 4, так как нужен ещё один байт для описания значения прозрачности (RGBA): 320×240×4 = 307 200 байт.
Браузер всегда отрисовывает композитные слои в RGBA-изображения: судя по всему, нет достаточно быстрого и эффективного способа автоматически определить, есть ли у отрисовываемого DOM-элемента прозрачные области.
Рассмотрим типичный пример: карусель из 10 фотографий размером 800×600. Вы решили сделать плавную смену изображений в карусели, поэтому заранее каждой фотографии указали will-change: transform, а потом с помощью JS анимируете переходы на действие пользователя, например, перетаскивание. Посчитаем, сколько дополнительной памяти потребуется, чтобы просто отобразить такую страницу: 800×600×4 × 10 ≈ 19 МБ.
19 МБ дополнительной памяти потребовалось для отрисовки всего лишь одного контрола на странице. А учитывая любовь современных разработчиков к SPA-страницам с множеством анимированных контролов, параллакс-эффектам, retina-изображениям и прочим визуальным штукам, дополнительные 100–200 МБ на страницу — далеко не предел. Добавьте сюда неявную композицию (признайтесь, вы ведь об этом раньше даже не задумывались? :) и мы получим совсем печальную картину.
Причём, довольно часто эта дополнительная память расходуется впустую, просто для отображения точного такого же результата:

И если для десктопных клиентов это ещё не так сильно заметно, то для мобильных устройств эта проблема стоит особенно остро. Во-первых, практически на всех современных устройствах используются экраны c высокой плотностью пикселей: умножаем изображения слоёв ещё на 4–9. Во-вторых, на таких устройствах довольно мало памяти, по сравнению с десктопами. Например, у ещё-не-такого-старого iPhone 6 всего 1 ГБ памяти, причём она общая для RAM и VRAM (память для GPU). Учитывая, что в лучшем случае треть этой памяти будет использоваться самой системой и фоновыми процессами, ещё треть — на браузер и текущую страницу (и это при условии, что вы не пользуетесь десятком фрэймворков и очень сильно всё оптимизируете), то для GPU-спецэффектов останется порядка 200—300 МБ. Причем, iPhone 6 относится к дорогим high-end устройствам, на более доступных устройствах памяти гораздо меньше.
У вас может возникнуть резонный вопрос: а можно ли на GPU хранить картинки в формате PNG, чтобы сэкономить память? Да, технически это возможно, однако особенность работы GPU такова, что каждый слой рисуется попиксельно [5]. А это означает, что для отрисовки одного пикселя на экране нужно будет каждый раз заново декодировать PNG-изображение, чтобы получить нужный цвет. В таком случае скорость самой простой анимации вряд ли будет подниматься выше 1 fps.
Стоит отметить, что на GPU существуют свои форматы сжатия изображений [6], однако они даже близко не сравнимы с PNG или JPEG по степени сжатия, а возможность их применения в том числе ограничена поддержкой самого GPU.
Теперь, когда мы рассмотрели теоретическую часть работы анимаций на GPU, давайте для удобства соберём все «за» и «против» их использования.
Как видите, при всех своих уникальных достоинствах, у GPU-анимации есть ряд очень существенных недостатков, главные из которых — repaint и потребление памяти. Поэтому все наши оптимизации как раз будут связаны именно с этими двумя пунктами.
Прежде, чем мы начнём оптимизировать сайт для качественных анимаций, нам необходимо запастись специальными инструментами, которые будут не только показывать нам результат оптимизаций, но и проблемные места.
В Safari Web Inspector встроен отличный инструмент, который позволяет увидеть все композитные слои на странице, потребляемую ими память, а также — что не менее ценно — показать причину выноса элемента на отдельный слой. Чтобы увидеть этот инструмент:

В DevTools также есть похожий инструмент, однако для его включения нужно выставить специальный флаг:
chrome://flags/#enable-devtools-experiments и включите флаг Developer Tools experiments.
в правом верхнем углу и перейдите в раздел Settings.
В этой панели отображаются все активные композитные слои страницы в виде дерева. Если выбрать слой, будет доступна информация о нём: размер, объем занимаемой памяти, количество перерисовок, а также причина выноса на композитный слой.
Итак, мы настроили окружение и теперь можем приступить непосредственно к оптимизации. Мы уже определили две основные проблемы с использованием композитных слоёв: лишний repaint, после которого изображение слоя нужно передать на GPU, и потребление памяти. Поэтому все наши оптимизации будут направлены на сокращение циклов перерисовки и снижение потребления памяти.
Очень простая, очевидная, но самая важная оптимизация. Напомню, что неявная композиция — это вынос на отдельный композитный слой элементов только для того, чтобы правильно скомпоновать его на GPU с другим, явным композитным слоем (видео, CSS анимация и т.д.). Особенно сильно эта проблема может ощущаться на мобильных устройствах при старте анимации.
Рассмотрим небольшой пример.
У нас есть элемент A, который мы хотим анимировать по действию пользователя. Если посмотреть на страницу с помощью инструмента Layers, мы увидим, что на ней нет никаких дополнительных слоёв. Однако сразу после нажатия на кнопку Play появится несколько композитных слоёв, которые исчезнут к концу анимации. Если посмотреть на инструмент Timeline, будет видно, что начало и конец анимации сопровождаются repaint значительных областей страницы:

Давайте рассмотрим пошагово, что сделал браузер в этом примере.
A — CSS Transition свойства transform. Однако браузер определил, что по z-index элемент A находится ниже элемента B. Поэтому он принимает решение вынести оба элемента на отдельные композитные слои.A. В этом случае браузер понимает, что больше нет смысла тратить драгоценные ресурсы на содержание элементов на отдельных слоях, поэтому он возвращается к самому оптимальному варианту – отображение элементов A и B на основном холсте. А это значит, что элементы нужно заново отрисовать на нём (ещё один repaint) и отправить полученное изображение на GPU. Этот процесс так же, как и в п.4, может сопровождаться «морганием».Чтобы избежать проблем с неявной композицией и сократить количество артефактов, рекомендую следующее:
z-index. В идеале эти объекты должны находится прямо внутри <body>. Конечно же, это не всегда возможно из-за особенностей вёрстки, когда анимируемый элемент находится глубоко в DOM-дереве и зависит от потока. В таких случаях можно создавать копию элемента, который нужно анимировать, и размещать его прямо в <body>.will-change [7]. В этом случае браузер может заранее (но не всегда!) вынести элемент на композитный слой и анимация всегда будет начинаться быстро. Этим свойством стоит пользоваться очень аккуратно, иначе рискуете многократно повысить потребление памяти.transform и opacityИменно эти свойства гарантированно не влияют на геометрию элемента и не зависят от окружения, в котором находится анимируемый элемент, поэтому могут полностью работать на GPU. Фактически это означает, что эффективно вы можете анимировать только перемещение, масштаб, вращение, прозрачность, а также искажения, в том числе в 3D-пространстве. Однако с их помощью вы можете эмулировать некоторые другие анимации.
Рассмотрим классический пример: смена цвета фона у элемента. Чтобы анимированно поменять цвет фона, достаточно написать вот так:
<div id="bg-change"></div>
<style>
#bg-change {
width: 100px;
height: 100px;
background: red;
transition: background 0.4s;
}
#bg-change:hover {
background: blue;
}
</style>
Но такая анимация работает на CPU, вызывает repaint на каждый шаг и не достаточно плавная. Мы можем оптимизировать её и вынести на GPU: достаточно создать дополнительный слой поверх элемента с нужным цветом и менять у него прозрачность:
<div id="bg-change"></div>
<style>
#bg-change {
width: 100px;
height: 100px;
background: red;
}
#bg-change::before {
background: blue;
opacity: 0;
transition: opacity 0.4s;
}
#bg-change:hover::before {
opacity: 1;
}
</style>
Такая анимация будет работать гораздо быстрее и плавнее, но стоит помнить про неявную композицию и повышенное потребление памяти. Хотя последний параметр мы всё-таки сможем немного оптимизировать.
Посмотрите на картинку ниже. Видите ли вы разницу между ними?

Это два визуально идентичных композитных слоя, однако первый весит 40 000 байт (39 КБ), а второй — в 100 раз меньше, всего 400 байт. Почему? Посмотрите на код [8] этих слоёв:
<div id="a"></div>
<div id="b"></div>
<style>
#a, #b {
will-change: transform;
}
#a {
width: 100px;
height: 100px;
}
#b {
width: 10px;
height: 10px;
transform: scale(10);
}
</style>
Разница в том, что у элемента #a физические габариты — 100×100 пикселей (100×100×4 = 40 000 байт), а у элемента #b — 10×10 пикселей (10×10×4 = 400 байт), увеличенные до 100×100 с помощью transform: scale(10). Но так как #b является композитным слоем из-за will-change, свойство transform в данном случае будет примеряться уже на GPU в момент отрисовки финального изображения.
Суть трюка очень проста: с помощью width и height уменьшаем физические габариты элемента, а с помощью transform: scale(…) масштабируем уже отрисованную текстуру до нужного размера. Конечно же, разницу по весу в несколько порядков можно получить только для очень простых одноцветных слоёв. Но, например, если вам нужно анимировать большие фотографии, можно запросто уменьшить их габариты на 5–10% и компенсировать это за счёт масштаба: потеря качества не сильно должна бросаться в глаза, но зато сэкономите драгоценные ресурсы.
Мы уже выяснили, что анимации свойств transform и opacity через CSS Transitions или Animations автоматически создаёт композитный слой и выполняет анимацию на GPU. Также мы можем делать анимацию и с помощью JS, однако в этом случае, как правило, нам нужно приложить чуть больше усилий, чтобы элемент оказался на композитном слое: указать translateZ(0) у transform, will-change: transform, opacity или другие свойства, которые создают композицию.
Под JS-анимацией подразумевается та анимация, где на каждый
requestAnimationFrameвручную высчитывается новый кадр. ИспользованиеElement.animate()[9] является вариацией декларативной CSS-анимации.
С одной стороны CSS Transitions/Animations довольно просто создать и переиспользовать, с другой — через JS движения по сложным траекториям делаются гораздо легче, чем в CSS, а также это единственный способ реагировать на пользовательский ввод.
Какой из этих способов лучше и универсальнее? Может, стоит оставить только JS и использовать какую-нибудь библиотеку для анимации всего?
На самом деле у CSS-анимаций есть одно очень важное преимущество: они полностью работают на GPU. Так как вы декларативно объявляете, где анимация начнётся и где закончится, браузер может предварительно подготовить весь набор необходимых инструкций и отправить их на GPU. В случае императивного JS единственное, что может знать браузер — это состояние текущего кадра. Для плавной анимации мы должны как минимум 60 раз в секунду в основном потоке браузера (а JS работает именно в нём) высчитывать данные для нового кадра и пересылать их на GPU, где этот кадр отрисуется. Помимо того, что эти расчёты и отправка данных работают намного медленнее, чем CSS-анимации, они ещё зависят и от загруженности основного потока:
В примере выше показано, что будет, если в основном потоке на JS будет выполнятся интенсивная операция. На CSS-анимации это никак не повлияет, так как новый кадр полностью считается в отдельном потоке (и даже на отдельном устройстве), в случае с JS придётся дождаться завершения тяжёлой операции и только после этого посчитать новый кадр.
Поэтому по возможности старайтесь делать анимации через CSS, особенно прелоудеры и индикаторы прогресса. Такие анимации не только будут работать быстрее, но и не будут блокировать тяжёлыми расчётами на JS.
На самом деле вся эта статья является результатом исследования и экспериментов, которые я провёл для оптимизации сайта Chaos Fighters [10]. Это отзывчивый промо-сайт для мобильной игры с большим количеством анимаций. Когда я только начинал его делать, я всего лишь знал, как сделать анимации плавными на разных устройствах за счёт GPU, но не понимал, как именно это работает. В итоге первая версия сайта стабильно крэшила iPhone 5 — а это была самая последняя новинка от Apple на тот момент — всего через пару секунд пользования сайтом. Но сейчас этот сайт довольно плавно работает даже на менее мощных устройствах.
Предлагаю рассмотреть самый, на мой взгляд, интересный пример оптимизации с этого сайта.
В самом верху страницы есть описание игры, под которым в фоне вращается что-то типа красного солнца. Вращение бесконечное и не интерактивное: отличный кандидат для простой CSS-анимации. Первый подход — самый наивный: сохраняем изображение солнца в виде картинки, размещаем на странице как элемент <img> и с помощью CSS-анимаций придаём ей вращение:
На первый взгляд всё просто и задача решена. Только вот изображение солнца получилось довольно большим — мобильные пользователи будут не очень этому рады.
Посмотрим внимательнее на картинку солнца. По сути, это несколько лучей, выходящих из центра изображения. Все лучи одинаковые, поэтому мы можем сохранить изображение только одного луча и переиспользовать его для создания нужного изображения. В итоге у нас получится одна небольшая картинка, на порядок меньше оригинала.
Для этой оптимизации придётся немного усложнить вёрстку: .sun теперь будет контейнером, в котором размещается несколько элементов с изображением луча; каждый элемент будет повёрнут на определённый градус.
Результат визуально такой же, как и раньше, однако объём передаваемых по сети данных гораздо меньше. Но размер композитной текстуры остался прежним: 500×500×4 ≈ 977 КБ.
Для наглядности изображение солнца в нашем примере достаточно небольшое, всего 500×500 пикселей, но на реальном сайте, с учётом разных размеров у мобильных устройств (телефоны и планшеты) и плотностью пикселей, текстура солнца весила примерно 3000×3000×4 = 36 МБ! И это всего лишь один анимированный объект на странице…
Вновь пристально смотрим на нашу разметку и панель Layers в браузере. Мы вполне логично упростили себе задачу: вращаем весь контейнер с солнцем. И весь этот контейнер был отрисован браузером в одну большую текстуру, которая была отправлена на GPU. Но из-за такого упрощения в текстуру попали как полезные данные (лучи), так и бесполезные — промежутки между лучами.
Более того, бесполезных данных в текстуре гораздо больше, чем полезных! Не самый лучший способ потратить драгоценные ресурсы устройства, которых и так крайне мало.
Решение проблемы точно такое же, как и в случае оптимизации изображения для загрузки: на GPU нужно отправить только полезные данные, а именно лучи. Даже можем посчитать, сколько сэкономим памяти:
Потребление памяти сократится в 2 раза. Чтобы это произошло, мы должны анимировать не весь контейнер с солнцем, а каждый луч по отдельности. Если мы будем анимировать только лучи, то именно изображения лучей попадут на GPU, промежутки между ними не будут занимать ресурсы.
Нам нужно ещё немного усложнить разметку для независимой анимации лучей, но теперь CSS нам скорее мешает, чем помогает. Для начального размещения луча мы уже воспользовались свойством transform. Нам нужно начать анимацию именно с этого угла и совершить оборот в 360˚. Фактически это означает, что для каждого луча нужно сделать свою секцию @keyframes, а это очень много кода.
Намного проще написать небольшой JS-код, который возьмёт на себя всю начальную расстановку элементов сцены и позволит нам точнее настраивать анимации, количество лучей и т.д.
Визуально мы получили абсолютно ту же самую анимацию, но потребление памяти сократили в 2 раза.
Но и это ещё не всё. С точки зрения композиции всего сайта анимированное солнце — не центральный, а фоновый, вспомогательный элемент. Да и сами лучи не содержат чётких контрастных элементов. А это означает, что мы можем немного ухудшить качество изображения луча и это будет практически незаметно для пользователя. Таким образом мы cможем отправить на GPU текстуру меньшего размера, а затем отмасштабировать её до нужного значения: это позволит нам ещё немного сократить потребляемую память.
Попробуем сократить размер текстуры луча на 10%. Физический размер текстуры будет 250×0.9 × 40×0.9 = 225×36 пикселей. Соответственно, чтобы визуально она была размером 250×20 пикселей, нам нужно выставить ей коэффициент масштабирования 250/225 ≈ 1.111.
Добавляем несколько важных штрихов в наш код: background-size: cover; у .sun-ray, чтобы фоновая картинка подстраивалась под размер контейнера, и добавим transform: scale(1.111) при анимации луча.
Обратите внимание, что мы меняем только размеры у элемента, размер самой PNG-картинки луча остался прежним. Именно прямоугольник, описанный элементом, отрисовывается в текстуру, а не PNG-картинка.
В итоге размер всей композиции солнца в памяти GPU составляет 225×36×4 × 12 ≈ 380 КБ (было 469 КБ). Потребление памяти сократилось на 19% и мы получили довольно гибкий код, где изменяя параметр downscale можно добиваться нужного соотношения качества картинки и потребления памяти. Если остановиться на значении 0.1, то получается, что с помощью усложнения такой, казалось бы, простой анимации вращения объекта мы сократили потребляемую память в 977 / 380 ≈ 2.5 раза!
Думаю, многие заметили, что у предложенного решения есть один недостаток: анимация теперь работает на CPU, а значит будет блокироваться тяжёлыми JS-вычислениями. Поэтому тем, кто хочет закрепить полученный материал, предлагаю сделать небольшое домашнее задание. Сделайте форк последнего примера [11] и доработайте его таким образом, чтобы вся анимация работала на GPU, но при этом не потерялось эффективность и гибкость решения. Результаты размещайте в комментариях.
Исследование, проведённое для оптимизации проекта Chaos Fighters, заставило меня полностью пересмотреть процесс создания современных сайтов. Вот мои основные правила:
position: fixed, <iframe>, <video> также выносятся на отдельный слой.translateZ() каждому элементу. Например: translateZ(0.0001px), translateZ(0.0002px) и т.д. Браузер решит, что слои находятся в разных плоскостях и отключит оптимизацию.transform: translateZ(0) или will-change: transform куда попало, чтобы ускорить анимации или избавиться от визуальных артефактов. У выноса элементов на GPU есть огромное количество недостатков и подводных камней, которые стоит учитывать. Неумелое использование композиции в лучшем случае приведёт в обратному эффекту: общему замедлению сайта. В худшем — просто «уронит» браузер.В очередной раз напомню про One Big Disclaimer: нет единой спецификации по GPU-композиции и каждый производитель браузеров одни и те же проблемы может решать по-своему. Возможно, часть информации из этой статьи в скором времени потеряет актуальность. Например, разработчики Google Chrome исследуют способы по ускорению передачи данных с CPU на GPU, вплоть до использования общей памяти, когда никуда ничего передавать не нужно. А Safari уже сейчас для некоторых простых случаев (например, обычный элемент с background-color и без содержимого) вместо отрисовки текстуры на CPU отрисовывает элемент на GPU в реальном времени, сводя размер потребляемой памяти практически к нулю.
В любом случае я надеюсь, что данная статья поможет вам немного лучше разбираться в анимациях на GPU и создавать красивые и качественные проекты.
Автор: Одноклассники
Источник [13]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/css/218733
Ссылки в тексте:
[1] W3C: https://www.w3.org
[2] will-change: https://www.w3.org/TR/css-will-change-1/
[3] filter: https://drafts.fxtf.org/filters/#FilterProperty
[4] CompositingReasons.h: https://cs.chromium.org/chromium/src/third_party/WebKit/Source/platform/graphics/CompositingReasons.h?q=file:CompositingReasons.h
[5] каждый слой рисуется попиксельно: http://www.html5rocks.com/en/tutorials/webgl/shaders/
[6] форматы сжатия изображений: https://en.wikipedia.org/wiki/Texture_compression
[7] will-change: https://developer.mozilla.org/docs/Web/CSS/will-change
[8] на код: https://sergeche.github.io/gpu-article-assets/examples/layer-size.html
[9] Element.animate(): https://developer.mozilla.org/en-US/docs/Web/API/Element/animate
[10] Chaos Fighters: https://ru.4game.com/chaos-fighters/
[11] последнего примера: https://codepen.io/sergeche/pen/YGJOva
[12] Layer Squashing: https://www.chromium.org/developers/design-documents/gpu-accelerated-compositing-in-chrome
[13] Источник: https://habrahabr.ru/post/313978/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.