javascript

Две строки CSS, которые снизили производительность со 120 до 40 FPS

  • четверг, 30 января 2025 г. в 00:00:07
https://habr.com/ru/articles/864840/

Введение

FPS расшифровывается как «кадры в секунду» (Frames Per Second) и означает измерение того, сколько кадров, или изображений, отображается на экране за одну секунду. Для frontend-разработчика эта метрика позволяет понять насколько интерфейс плавно и четко работает.

От переводчика

Всем привет, с вами Максим Иванов, и сегодня мы поговорим о простом, но интересном трюке, который поможем вам сделать вас сайт быстрее за счет улучшения показателя FPS. Раньше всего с этой аббревиатурой вы могли столкнуться в еще подростковом возрасте, во всяком случаев, в моем детстве было именно так, когда у меня появился первый компьютер и я начал играть в игры на ПК. Уже тогда я знал, не понимая зачем мне это, что чем выше FPS, тем более плавным и реалистичным будет видеопоток.

1. На что влияет FPS?

Частота кадров в первую очередь влияет на то, насколько плавным выглядит изображение в игре, как отмечалось выше. И еще она влияет на то, насколько в целом комфортно воспринимать картинку с экрана, ведь если значение FPS опускается ниже 30 кадров в секунду, то человеческий мозг начинает воспринимать происходящее на экране монитора или телевизора как тормоза, лаги и другие недостатки, вызванные слабостью железа вашего устройства. Выходит, что именно FPS — это главный показатель производительности, во всяком случае для видеоигр это именно так. Неудивительно, что можно услышать от геймеров такую фразу как «хочу поднять до 120 или даже 240 кадров в секунду» [1].

2. Чем FPS в играх отличается от частоты кадров в кино?

Многие годы в интернете ходил миф, что человеческий глаз не может увидеть более 25 кадр/с, а связано это было со старыми фильмами. Кино, кстати, до сих пор в большинстве своем снимают с частотой 24 кадр/с, но если раньше это было связано с дороговизной пленки, то сейчас — с излишней реалистичностью картин. Поэтому режиссеры придерживаются «золотого стандарта», тем самым делая кино фантазийным, чтобы люди, наоборот, могли отвлечься от реальности [2].

3. Чем отличается FPS в Web-приложениях от частоты кадров в кино?

Ваше веб-приложение будет отрисовываться с максимально возможной частотой кадров, настолько насколько это возможно. Отчего это будет зависеть? Это будет зависит от комбинации графического процесса (GPU) вашего устройства и монитора. Стандартные офисные мониторы имеют частоту 60 Гц, но есть мониторы с более высокой частотой – 144 Гц, 240 Гц и даже 360 Гц. Герц (Гц) – число изображений, которые отображаются в секунду при обновлении монитора. Чем выше их количество, тем более эффектной и детализированной будет выглядеть картинка на дисплее [3]. Таким образом, нужно учесть, что для фильмов специально обычно делают пост-обработку, чтобы уменьшить итоговую частоту кадров. А вот ваши веб-приложения будут рисоваться с максимально и допустимо возможной частотой кадров на вашем мониторе или устройстве. Если ваш дисплей работает на частоте 60 Гц, отобразить лишние кадры (> 60 FPS) вы не сможете.

4. Безопасно ли полагаться, что браузер всегда будет рисовать страницы веб-приложений с частотой 60 кадров в секунду?

Считается, что хорошие интерфейсы, отображаемые на экране, должны минимально работать при частоте 60 кадров в секунду, хотя и не всегда возможно поддержать это значение стабильным. Существует 1001 фактор, когда что-то может привести к снижению FPS из-за работы вашего кода написанного на JavaScript. Да даже перерисовка CSS и HTML может вызвать такую ​​нагрузку на устройство, что это снизит производительность. Просто нужно помнить об этом. Но как мы помним из опыта кино, все, что выше 24-25 кадров в секунду, будет распознаваться как непрерывное движение (если вы не паук). Поэтому порой потеря десятки кадров не страшно.

5. Как браузер рисует страницу web-приложения на экране моего монитора?

В первую очередь, браузер имеет механизм рендеринга, который отвечает за отображение и визуальное представление веб-страницы. Думайте о механизме рендеринга как о художнике, работающем с пустым холстом, его ответственность состоит в том, что он создает страницы путем применения правильных структур и цвета, которые движок использует из HTML и CSS разметки.

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

В тоже время механизм рендеринга анализирует CSS и ассоциирует стили с узлами DOM дерева, давая на выходе CSSOM — это объект, представляющий стили, связанные с DOM. На основании всего теперь можно создать новое дерево — дерево рендеринга (Render Tree). Впоследствии механизм рендеринга выполняет процесс компоновки.

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

Критический путь рендеринга (Critical Rendering Path)
Критический путь рендеринга (Critical Rendering Path)


Какие действия выполнил браузер в самой простой ситуации [4]:
1. Обработка HTML-разметки и создание модели DOM.
2. Обработка CSS-файла и создание модели CSSOM.
3. Создание модели визуализации из DOM и CSSOM.
4. Определение формы и расположения объектов, создание макета.
5. Вывод объектов на экран.

Внимание: если мы внесем изменения в DOM или CSSOM, браузеру придется повторить все действия, чтобы определить, как вывести пиксели на экран. Чтобы оптимизировать процесс визуализации, нужно уменьшить время, затрачиваемое на выполнение пяти действий из списка выше.

Rendering pipeline
Rendering pipeline

6. Как оптимизировать этот процесс на реальном проекте?

Совсем недавно, в одном из проектов, я (автора оригинальной статьи) добавил анимацию на задний план своей веб-страницы, где точки двигались по диагонали экрана. Выглядело это так:

Все отлично работало в Chrome и Safari, но я заметил серьезное падение производительности в Firefox.

Вы, возможно, не сможете увидеть это на видео, однако FPS значительно снижается. И как пользователь, вы действительно можете почувствовать, что что-то идет не так (падение производительности). Это было настолько плохо, что я просто отключил эту анимацию на время в Firefox. Хорошо, давайте разбираться и фиксить этот момент.

Как работает CSS анимация?

Анимация построена с использованием двух div. Внешний div является первым дочерним элементом элемента body.

<body>
	<div class="background-mask">
		<div class="background-gradient"></div>
	</div>

	<!-- Остальной контент -->
</body>

Элемент .background-gradient отвечает за создание градиента, который охватывает всю ширину и высоту своего родительского контейнера. Вот так:

Внешний контейнер .background-mask отвечает за следующие вещи:

  1. Установка фиксированного положения, чтобы контейнер заполнил все размеры области просмотра.

  2. Создание точечной маски поверх градиента.

Последний шаг гарантирует, что цвет точек будет соответствовать цвету градиента, расположенного непосредственно под ними:

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

.background-mask {
	--mask-size: 24px;

	/* Позиционируем */
	position: fixed;
	width: 100%;
	height: 100%;
	z-index: -1;

	/* Наша маска */
	mask-image: radial-gradient(black 2px, transparent 2px);
	mask-size: var(--mask-size) var(--mask-size);
	mask-position: 0px 0px;
	animation: mask-move 3s infinite linear;
}

.background-gradient {
	background: var(--red);
	background-image: var(--gradient);
	width: 100%;
	height: 100%;
}

@keyframes mask-move {
	0% {
		mask-position: 0px 0px;
	}

	100% {
		mask-position: var(--mask-size) var(--mask-size);
	}
}

@media (prefers-reduced-motion: reduce) {
	.hero-background-mask {
		animation: none;
	}
}

Если вам интересно узнать больше о масках в CSS, то я могу порекомендовать этот подробный пост Ахмада Шадида.

От переводчика: Также я рекомендую просмотреть публикацию моего коллеги, который написал исчерпывающую статью на тему масок в CSS.

Что влияет на проблемы с производительностью?


Не все свойства CSS анимируются одинаково, но не вдаваясь слишком глубоко в то, как браузер отображает HTML на странице, вспоминаем через что проходит рендеринг:

  • Layout — браузер вычисляет размер и положение элементов на странице;

  • Paint — браузер отрисовывает все визуальные сущности на странице, такие как картинки, цвета, тени;

  • Composite — браузер накладывает элементы друг на друга в правильном порядке.

Порядок рендеринга выглядит следующим: Layout → Paint → Composite. В дальнейшем будем называть - rendering пайплайн (конвейер рендеринга).

И здесь уже важно понимать, что этапы Layout и Paint могут быть ресурсоемкими для CPU (ЦП, центрального процессора), поэтому важно попытаться сократить количество операций, когда ваш CSS будет триггерить браузер, запуская конвейер снова и снова. Браузер - наш друг, он помогает в некоторой степени, оптимизировать проблемы с производительностью для определенных свойств, чтобы не было избыточных запусков некоторых этапов нашего rendering пайплайна.

Иногда вычисления могут также использовать аппаратное ускорение перенеся их с CPU на GPU (графический процессор на видеокарте). Анимация определенных свойств, таких как translate и opacity, как раз таки помогает избежать лишнего запуска Layout этапа и использовать аппаратное ускорение.

К сожалению, это не относится к анимации mask-position. Взглянув на Chrome, я увидел, что количество отрисовок для фонового div увеличивается с каждым новым кадром. Через несколько секунд я заметил, что он уже запустил отрисовку более 1000 раз.

Даже при таком большом количестве отрисовок анимация в Chrome казалась плавной. Однако в Firefox она казалась очень дерганой. Причем я не смог найти способ измерить количество отрисовок в Firefox, поэтому любые мои предположения о плохой производительности Firefox являются чисто догадками.

Я заметил, что производительность не падала на маленьком размере экрана, но ухудшалась с его ресайзом. Моя рабочая теория заключается в том, что Firefox не оптимизирует постоянные триггеры Layout этапа для масок размера 24x24, что приводит к падению FPS при наличии большего количества таких масок на экране. Но опять же, я могу быть совершенно неправ.

Как я это пофиксил?


Вместо того, чтобы анимировать свойства CSS, которые не оптимизируются браузером, такое как mask-position, мне нужно было опереться на более производительные свойства, такие как translate. Решение заключалось не в том, чтобы перемещать позицию маски во время анимации, а в том, чтобы двигать фоновый элемент с помощью свойства translate. С абстрактной точки зрения анимация будет работать следующим образом:

Вот изменение в двух строках CSS:

/* --mask-size = 24px */

@keyframes mask-move {
	0% {
		transform: translate(calc(var(--mask-size) * -1), calc(var(--mask-size) * -1));
	}

	100% {
		transform: translate(0px, 0px);
	}
}

Браузер больше не анимирует положение маски, которое запускало макет в каждом кадре. И несмотря на то, что теперь фон перемещается в каждом кадре анимации, translate не триггерит Layout и Paint этапы нашего rendering пайплайна. Вы можете видеть, что фоновый div отрисовывается только дважды за все время, а не 1000+ в минуту как это было до этого.

Внимательные зрители наверняка заметили, что в данном случае, мы родили себе проблему. Если вы помните, высота и ширина фона заполняют область просмотра. Смещение фона влево и вверх на 24 пикселя оставляет нам это пустое пространство в области просмотра.

Но решить эту проблему очень просто, нужно прибавить размер маски к ширине и высоте контейнера:

.background-mask {
	--mask-size: 24px;

	width: calc(100% + var(--mask-size));
	height: calc(100% + var(--mask-size));
}

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

Список материалов и ссылки на них

[1]. Что такое FPS в играх — и на что влияет частота кадров в секунду
[2]. До 60 fps: исследование наглядно показало возможности человеческого глаза
[3]. Гайд новичка: разрешение, частота обновления, матрицы и другие геймерские параметры монитора
[4]. Отрисовать за 16 мс / Глеб Михеев

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Полезна ли была данная публикация?
71.43% Да10
28.57% Нет4
Проголосовали 14 пользователей. Воздержались 2 пользователя.