Перейти к содержимому

Style layout как оптимизировать

  • автор:

Организация и оптимизация стилей

В этом посте я приведу пример организации стилей на типичном проекте.

Небольшое вступление, попробую объяснить актуальность проблемы и зачем это нужно.
Рассмотрим такую ситуацию. Разработчику ставят задачу, реализовать очередной функционал на сайте. Это допустим включает добавление новых разделов, блоков, элементов. Разработчики зачастую не доверяют чужому коду, и когда доходят до верстки, находят css-файл с названием типа main.css и дописывают в конец свои новые стили.
Проходит некоторое время, приходит новый разработчик, ему ставят подобную задачу, он если и пытается разобраться в стилях, то видит, что там нет никакой закономерности, и повторяет то же, что делали предыдущие.
Руководство ставит сроки, разрабатывается все новый и новый функционал, проект растет. В итоге css файлы превращаются в мусорку, сайт грузится дольше, появляется больше дефектов и т.д..
Я думаю, многим это знакомо.

Теперь поговорим непосредственно о самой структуризации стилей.
Держать все стили в одном файле неразумно, со временем в нем довольно сложно становится ориентироваться. Плюс на каждой странице используются около 10% правил из этого файла, а весит он не мало.
Гораздо оптимальнее разделять стили по логическим блокам сайта.

Так же к проекту необходимо подключить библиотеку для работы с css (LESS, SASS, SCSS). Нам понадобится работа с переменными, функциями.
Для уменьшения запросов на сервер необходима сборка файлов. Файлы должны склеиваться по специальной конструкции, можно, например, использовать стандартную констукцию css — import. Здесь возможно потребуется помощь разработчика для редактирования исходников выбранной вами библиотеки css.
Плюс, для уменьшения объема, файлы должны приходить клиенту сжатые. dotLess, например, сжимает файлы при значении в web.config.

Каждому логическому блоку будет соответствовать отдельный css файл. Так упрощается поддержка, поиск нужных стилей, да и вообще ориентация в файлах. Все данные файлы являются исходниками, будем содержать их в папке /css/sources/. Остальные css-файлы — сборщики, они линкуются на страницы и собирают импортом исходники.
Допустим рассматриваемый проект это некая соцсеть, исходя из этого можно выделить следующую структуру:

/css
/sources — папка для ресурсов, не выкладывается на сервер
/global-vars — файлы данной папки подключаются в каждый css-файл сборщик по мере необходимости
locals.css — глобальные переменные
functions.css — глобальные функции
/common
reset.css — думаю, объяснять не нужно, понятно, что за стили
utils.css — стили типа .clearfix
/content
base.css — основные стили для контента, а именно — h1-h6, p, буллиты для списков (ul.text, ul.dashed и т.д.), dl, dd, dt, изображения и панели в тексте (обтекание текстом), текстовые колонки и т.д.
panels.css — различные панели
tables.css — стили для таблиц в контенте (th, черезполосица)
/controls
buttons.css — виды кнопок
forms.css — общие стили для input-полей (к примеру, внутренняя тень, фокус (рамка), оформление валидационных сообщений и их скрытие по умолчанию)
tabs.css — табы, вкладки
system-notifications.css — системные сообщения, как правило бывают 3-х типов: success (зеленые), failure (красные) и info (серые, синие)
pager.css — пейджер
banners.css — баннеры на сайте
balloons.css — всякие баллуны, всплывающие подсказки, кастомные тултипы и т.д.
/member
thumb.css — аватарка пользователей
card.css — карточка пользователя (аватарка, имя, краткие данные)
cards-list.css — например, грид с карточками
profile.css — профиль пользователя
/modules — различные модули к сайту
search.css
news-list.css
gifts.css
games.css
/not-auth — для неавторизованных пользователей
login.css — форма авторизации
registration.css — форма регистрации
/auth — для авторизованных пользователей
my-account.css
mail-system.css — inbox сообщения, outbox и т.д.
auth-menu.css — меню навигации в авторизованной зоне
my-profile.css — просмотр своего профайла, редактирование
/layouts
common.css
header.css
top-menu.css
footer.css
overlay.css — например, все всплывающие поверх слои будут с затемнением 0.5
main.css — основной сборщик, линкуется на всех мастер-страницах
/layouts
default.css — основной layout сайта, собирает файлы из папки /layouts, подключается на мастере с основным layout’ом
popup-windows.css — стили для popup’ов, подключается на мастер-страницах для popup окон
not-auth.css — собирает стили из папки /sources/not-auth/
auth.css — собирает стили из папки /sources/auth/
/themes — различные тематики сайта
new-year.css
st-valentine.css
/%section-name% — какой-нибудь новый раздел сайта, «сайт в сайте», характерный наличием своего подменю и т.д.
/css
%section-name%.css
layout.css
/sources
menu.css

Конечно же для каждого проекта своя уникальная структура. Важен принцип разделения файлов.

Дополнительное описание к некоторым файлам:

main.css — собирает файлы из папок:
/sources/global-vars
/sources/common
/sources/content
/sources/controls
/sources/member
/sources/modules

functions.css — содержит глобальные функции, типа:

.rounded-corners ( @radius)
<
border-radius : @radius;
-moz-border-radius : @radius;
-webkit-border-radius : @radius;
* behavior : url ( /libs/progressive-IE.htc ) ;
>

sources/layouts/common.css — глобальные стили по layout’у:

.columns-wrapper
<
display : table ;
* zoom : 1 ;
>
.columns-wrapper .column
<
display : table-cell ;
* float : left ;
>

Подключение файлов not-auth.css и auth.css зависит от состояния авторизации пользователя. Например, в asp.net это будет выглядеть так:

* This source code was highlighted with Source Code Highlighter .

Хочу привести концепцию, которой, я считаю, следует держаться.
Новые страницы должны строиться из компонентов, «кирпичиков» — css классов. Некоторые неверно понимают данную концепцию и строят страницу из классов типа mar-bottom-15, float-left или pad-20, что тоже является большой ошибкой.
На всем сайте должен сохраняться единый стиль элементов, т.е. заголовок h1 на одной странице должен выглядеть так же, как и h1 на другой. Заголовки, абзацы, списки, панели, табы, пейджеры, контентные таблицы и т.д. по дизайну должны соблюдать единый стиль.
Перед тем как писать стили для новой страницы сайта следует проанализировать уже существующие css-файлы проекта, возможно там уже есть необходимые стили. Css файл не должен увеличиваться без необходимости.

В заключении скажу, что все описанное выше так же актуально и для JS.

  • css
  • структура стилей
  • клиентская оптимизация

Новый способ: Как ускорить загрузку сайта и улучшить поведенческие факторы? Ленивый рендеринг контента через content-visibility

Скорость отрисовки первого экрана страницы влияет на значение конверсии. С 2021 года будет влиять и на результаты ранжирования сайтов в поисковой выдаче.

Появилось новое свойство CSS, которое позволяет повысить скорость отрисовки контента на странице.

Что за свойство? Как работает? Как внедрить на продвигаемых сайтах?

Разберемся с вопросом далее.

Lazy rendering 2020

Стандарты веба продолжают совершенствоваться. Для примера, мало кто знает, но такие теги как video состоят из множества элементов, которые скрыты. Аналогично, как и списки. В перспективе такие элементы будут открыты для стилизации. Инициируют решение Google, Microsoft и прочие. Инициатива называется Open UI. Здесь совершенствования относятся к теме дизайна страниц.

В плане поисковой оптимизации важным является анонс Google о новом CSS-свойстве.

Поддержка начинается с браузеров на базе Chromium 85. Актуальная пользовательская версия браузера Google Chrome уже поддерживает новый элемент.

Свойство называется contain. Разберемся с целями и как работает.

Какие цели? Цели свойства сводятся к улучшению взаимодействия со страницами. Список целей:

  • Повышение скорости отрисовки контента;
  • Повышение скорости загрузки страницы;
  • Старт взаимодействия с контентом без ожидания отрисовки всего содержимого страницы.

Поисковая оптимизация и рендеринг контента на странице сайте

В плане производительности эффективность данного решения является низкой.

Главная цель свойства заключается в улучшении значения метрики рендеринга страницы.

Для решения задачи требуется решение, которое бы обеспечило предсказуемую изоляцию поддерева DOM от остальной части страницы. Разберемся как работает свойство.

Как работает? Свойство contain передает сообщение браузеру о том, как работать с контентом, который находится за пределами зоны viewport, то есть за экраном просмотра.

Браузер использует сообщение от contain для того, чтобы пропустить разметку и покраску элементов, которые размещены за пределами экрана просмотра.

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

Для автоматической настройки contain есть специальное свойство content-visibility.

Ленивый рендеринг с применением content-visibility: auto

Решение с content-visibility: auto гарантирует максимальный возможный прирост производительности рендеринга страницы при минимальных усилиях.

Как работает content-visibility? Свойство contain получает атрибуты layout, style и paint, а при выходе элемента за края экрана —size и перестаёт красить и проверять содержимое.

  • Если контент за пределами viewport, то браузер принимает решение о том, рисовать элемент или нет, в зависимости от дистанции к элементу;
  • Если контент в зоне просмотра, то происходит отрисовка.

Откуда берется прирост скорости? Обычный процесс после открытия страницы:

  • Загрузка части страницы, включая все требуемые ресурсы;
  • Отрисовка и размещение контента на странице. Включая контент, который является невидимым;
  • Следующий этап загрузки частей страниц, пока не выполнятся работы по всей странице.

Часть контента не затребована, поэтому не требует обработки.

Рендеринг страницы без использования свойства content visibility

Если применить content-visibility: auto, то шаги те же самые, но количество работы на шаге 2 значимо уменьшается. Браузер будет рендерить и размещать только контент, который выводится на экране просмотра.

Для невидимого контент рендеринг не происходит, размещаются лишь пустые контейнеры. В результате рендеринг выполняется значимо быстрее. Прирост составляет в среднем 50%.

Пример. Прирост скорости рендеринга в 7 раз:

Рендеринг страницы с content visibility

Как начать использовать на сайте? Все просто. Сначала следует разделить контент на зоны.

Как использовать content-visability на сайте

К зонам прописать соответствующее правило. Пример:

.story < content-visibility: auto; contain-intrinsic-size: 1000px; /* Дает понимание про следующую зону */ >

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

Что в коде означает contain-intrinsic-size? Рендеринг контента приводит к сдвигу полосы прокрутки. Такая реализация является не идеальной. Подобная реализация ведет к пессимизации значения Cumulative Layout Shift.

Метрика Cumulative Layout Shift оценивает стабильность макета после загрузки сайта.

Как работает? Например, клиент находится на странице, изучает контент. На сайте есть картинка, которая имеет большой размер и загружается по lazy loading. Картинка загрузилась, текст на странице переместился. Данный факт учитывается поисковой системой.

Как решить проблему с метрикой Cumulative Layout Shift? Решение заключается в использовании contain-intrinsic-size.

Минимизируем влияние на SEO при помощи contain-intrinsic-size

Через значение contain-intrinsic-size задается размер контейнера, который резервирует место под контент. В результате:

  • Полоса прокрутки не изменяется. Лояльность пользователя не падает;
  • Метрика Cumulative Layout Shift не пессимизирует сайт.

Скрытие контента через свойство content-visibility: hidden

content-visibility: hidden позволяет более гибко управлять рендерингом.

Более сложная реализация:

  • Установка content-visibility: hidden;
  • Использование на intersection observer. API обеспечивает вызов определенной функции каждый раз при пересечении целевого элемента viewport;
  • Если контент попал в зону видимости, то ставить в полную видимость.
  • display: none скрывает контент, отключает процесс рендеринга;
  • content-visibility: hidden скрывает контент, но не отключает процесс рендеринга.

Применяется исходя из специфики сайта в случае, если контент надо скрыть, и затем быстро сделать видимым. К примеру, по действию.

Кстати в MegaIndex есть бесплатный инструмент, применяя который можно провести аудит сайта по другим параметрам.

Пример отчета далее:

Аудит сайта на предмет тега title

Выводы

Свойство content-visibility позволяет значительно ускорить рендеринг без сложных манипуляций. В результате улучшается значение фактора Page Experience.

Новая возможность позволяет улучшить и метрику Largest Contentful Paint, которая является составной частью фактора ранжирования Page Experience.

Реализация на практике крайне простая. Не требуется даже разбираться в тонкостях работы браузера. В самом простом варианте реализация выполняется одной строкой кода.

Для повышения метрики скорости рендеринг следует использовать свойство content-visibility: auto. В результате на странице будет использоваться ленивый рендеринг контента, который находится за экраном просмотра. Главный контент будет отрисовываться быстрее.

Ленивый рендеринг контента на сайте с content-visibility

Данный способ подходит для большинства сайтов, является самым простым и дешевыми в реализации.

К минусам относится только сдвиг контента при прокрутке страницы. Но использование contain-intrinsic-size позволяет полностью устранить данный минус. Внедрение на сайте является легким. В результате не будет проблем с метрикой Cumulative Layout Shift.

Рекомендованный материал в блоге MegaIndex по теме оптимизации скорость загрузки по ссылке — Появился новый атрибут тега img, который позволяет ускорить загрузку сайта. Пример реализации.

Остались ли у вас вопросы, замечания или комментарии по теме использования ленивого рендеринга на страницах сайта?

20 советов по оптимизации производительности CSS

В этой статье мы рассмотрим 20 приемов оптимизации CSS, которые повысят производительность сайта.

Обновлено: 2023-06-05 20:38:59 Вадим Дворников автор материала

Используйте аналитические инструменты

Используйте инструменты разработчика, встроенные во все наиболее популярные браузеры. Чтобы запустить их, нажмите F12, Ctrl+ Shift+ I или Cmd+ Alt+ I в Safari (macOS).

Наиболее полезные вкладки включают в себя следующие данные:

  • «Сеть» — графики загрузки ресурсов. Для получения достоверных результатов отключите кэш. Ищите файлы, которые загружаются медленно или блокируют загрузку других. Браузер обычно блокирует рендеринг при загрузке и парсинге файлов CSS и JavaScript.
  • «Производительность» — содержит данные о процессах браузера. Начните запись, выполните какое-либо действие (например, перезагрузите страницу), затем остановите запись и просмотрите результаты. Обратите внимание на:
  1. Excessive layout/reflow – действия, при которых браузер вынужден пересчитывать положение и размер элементов страницы.
  2. Expensive paint – затратные действия отрисовки.
  3. Compositing – действия, при которых отрисованные части страницы объединяются для отображения на экране. Это наименее ресурсоемкие процессы.

Браузеры на основе движка Chrome, предоставляют вкладку «Аудит». В ней можно запустить инструмент Google Lighthouse. Он предоставляет рекомендации по производительности CSS.

Онлайн-инструменты

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

  • Pingdom Website Speed Test;
  • GTmetrix;
  • Google PageSpeed ​​Insights;
  • WebPagetest;

В первую очередь выполняйте основные задачи

Код CSS может загружать объемные ресурсы, которые можно оптимизировать. Примеры:

  • Активируйте на сервере HTTP/2 и GZIP-сжатие;
  • Используйте сеть доставки контента (CDN), чтобы увеличить количество одновременных HTTP-соединений.
  • Удалите неиспользуемые файлы.

Изображения являются основной причиной большого объема веб-страниц. Поэтому:

  1. Измените размеры растровых изображений. Только для небольшого количества сайтов требуются изображения шириной более 1600 пикселей.
  2. Используйте правильные форматы файлов. JPG лучше всего подходит для фотографий, SVG – для векторных изображений и PNG – для всего остального.
  3. Уменьшайте размеры файлов с помощью увеличения коэффициентов сжатия.

Замените изображения эффектами CSS

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

Удалите ненужные шрифты

  1. Используйте только те шрифты, которые необходимы.
  2. Загружайте только те значения толщины и стили, которые нужны.
  3. Ограничьте наборы символов. Google Fonts позволяет выбирать определенные символы, добавляя значение &text= к URL-адресу шрифта. Например, fonts.googleapis.com/css?family=Open+Sans&text=SitePon для отображения «SitePoint» с помощью Open Sans.
  4. Используйте вариативные шрифты, которые определяют несколько толщин и стилей путем интерполяции, чтобы файлы были меньше.

Избегайте @import

Правило @import позволяет импортировать CSS в другой файл. Например:

/* main.css */ @import url("base.css"); @import url("layout.css"); @import url("carousel.css");

Но правила @import могут быть вложенными, поэтому браузер должен загружать и анализировать каждый файл последовательно.

Несколько тегов в HTML позволяют загружать файлы CSS параллельно. Это более эффективно, особенно при использовании HTTP/2:

Конкатенация и минимизация

Большинство инструментов сборки позволяют объединить все компоненты в один большой файл CSS, в котором будут удалены ненужные пробелы, комментарии и символы.

Объединение менее необходимо при использовании протокола HTTP/2 , который объединяет запросы. Но большинство сайтов, скорее всего, выиграют от отправки одного файла, который сразу же кэшируется браузером.

Также GZIP может максимизировать сжатие, когда в файле часто используются повторяющиеся строки.

Используйте современные методы компоновки

Много лет нам приходилось использовать CSS float для верстки страниц. Но существуют более эффективные технологии:

  • CSS Flexbox — для одномерных макетов, которые могут переноситься на следующую строку в соответствии с шириной каждого блока. Flexbox идеально подходит для меню, галерей изображений, карточек и т. д.
  • CSS Grid — для двумерных макетов с явными строками и столбцами. Сетка идеально подходит для макетов страниц.

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

Уменьшите код CSS

Со временем CSS может раздуться по мере увеличения количества функций. Рекомендации по решению данной проблемы:

  • Откажитесь от использования больших CSS-фреймворков.
  • Организуйте CSS-файлы. Виджет карусели значительно легче удалить, если его CSS четко определен в widgets/_carousel.css .
  • Используйте методологии именования, такие как BEM , которые помогают в разработке отдельных компонентов.
  • Избегайте использования !important для переопределения каскада.
  • Избегайте использования стилей, встроенных в HTML.

Такие инструменты, как UnCSS, могут помочь удалить избыточный код.

Следуйте каскаду!

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

Упростите селекторы

Упрощение селекторов уменьшит размер CSS-файлов и поможет браузеру быстрее их анализировать. Вам действительно нужен такой селектор ?!

body > main.main > section.first h2:nth-of-type(odd) + p::first-line > a[href$=".pdf"]

Остерегайтесь использования ресурсоемких свойств

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

*, ::before, ::after

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

  • border-radius;
  • box-shadow;
  • opacity;
  • transform;
  • filter;
  • position : fixed.

Применяйте CSS-анимацию

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

Избегайте анимирования ресурсоемких свойств

Анимация размеров или положения элемента может привести к перестройке всей страницы в каждом кадре. Производительность можно улучшить, если анимация будет влиять только на этап компоновки . Более эффективно анимация используется следующими свойствами:

Укажите, какие элементы будут анимированы

Свойство will-change позволяет указать, какой элемент будет анимирован. Поэтому браузер может заранее оптимизировать производительность. Например:

.myelement

Можно указать любое количество свойств, разделенных запятыми. В то же время:

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

Применяйте SVG-изображения

Масштабируемая векторная графика (SVG) обычно используется для логотипов, диаграмм, иконок и более простой графики. Вместо определения цвета каждого пикселя (как в растровых изображениях JPG и PNG), SVG определяет простые фигуры (линии, прямоугольники и круги). Например:

Простые SVG меньше по размеру, чем растровые изображения. При этом они могут бесконечно масштабироваться без потери качества.

SVG может быть встроен в CSS-код как фоновое изображение. Что позволяет избежать дополнительных HTTP-запросов. Например:

.mysvgbackground < background: url('data:image/svg+xml;utf8,') center center no-repeat; >

Стилизуйте SVG с помощью CSS

Чаще всего SVG-файлы включаются непосредственно в HTML-документ:

Это добавляет узлы SVG непосредственно в DOM. Поэтому к SVG могут быть применены стили через CSS:

circle < stroke-width: 1em; >.mysvg

Объем встроенного кода SVG уменьшается, а код CSS можно использовать повторно или анимировать по мере необходимости.

Избегайте растровых изображений Base64

Стандартные растровые изображения JPG, PNG и GIF могут быть закодированы с помощью base64. Например:

.myimg < background-image: url('data:image/png;base64,ABCDEFetc+etc+etc'); >

Но это неправильный подход:

  • кодировка base64 на 30% больше, чем ее двоичный эквивалент;
  • браузер должен проанализировать строку, прежде чем ее можно будет использовать;
  • изменение изображения делает недействительным весь кэшированный файл CSS.

Несмотря на то, что выполняется меньше HTTP-запросов, это редко дает заметное преимущество, особенно по сравнению с HTTP/2-соединениями.

Критический CSS

Загрузка файла CSS блокирует рендеринг, поэтому производительность можно улучшить, выполнив следующие шаги:

  1. Располагайте стили, используемые для визуализации элементов, выше первого сгиба. Такие инструменты, как criticalCSS, помогут вам в этом.
  2. Добавьте их в элемент в раздел HTML .
  3. Загружайте основной файл CSS асинхронно, используя JavaScript.

Этот метод повышает производительность и может оказаться полезным для прогрессивных веб-приложений или одностраничных приложений. Преимущества могут быть менее очевидны для других типов сайтов. Причины этого:

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

Прогрессивный рендеринг

П рогрессивный рендеринг — это метод, который определяет отдельный CSS для отдельных элементов. Каждый загружается непосредственно перед ссылкой на компонент в HTML:

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

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

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

Научитесь любить CSS

Добавление огромного количества CSS из StackOverflow или Bootstrap может дать быстрые результаты. Но это также приведет к переполнению CSS неиспользуемым кодом.

CSS легко изучить, но трудно освоить. Более глубокое знание основ CSS поможет вам повысить производительность сайта.

Оптимизация производительности фронтенда. Часть 2. Event loop, layout, paint, composite

Ночь. Стук в дверь. Открыть. Стоят двое. «Верите ли вы в Event loop, нашу главную браузерную цепочку?» Вздохнуть. Закрыть дверь. Лечь досыпать. До начала рабочего дня еще 4 часа. А там уже ивент лупы, лейауты и прочая радость…

В первой части мы говорили о первой загрузке и работе с ресурсами. Сегодня я расскажу о второй части оптимизации производительности фронтенда. О том, что происходит с нашей страницей, когда она загружена, на что уходит процессорное время и что с этим делать. Ключевые слова: event loop, paint \ repaint, layout \ reflow, composite.

Завязка. Вопросы для самопроверки

Если хочется сразу начать поглощать контент статьи, пропустите этот раздел

Event loop популярен не только в ежедневной работе, но и на собеседованиях. И пойди разберись, где он популярнее. Существует три вопроса, которые можно задать себе, чтобы проверить, понимаете ли вы этот браузерный механизм. Два из них — завсегдатаи собеседований, а вот третий показывает понимание event loop для своей практики. Они довольно простые, но будут расставлены по возрастанию сложности:

    Будет ли выведено «1» в консоль? Почему?

function loop() < Promise.resolve().then(loop); >setTimeout(() => , 0); loop();
while (true);

Наша цель

Прийти к достаточно глубокому пониманию вот этой схемы:

Идти к ней будем постепенно, с подробными остановками на каждом из этапах

Event Loop

В старых операционных системах была похожая штука. Выглядела она условно вот так:

while (true) < if (execQueue.isNotEmpty()) < execQueue.pop().exec(); >>

Такой код всегда забивал ЦПУ процессоров в 100%. Что и было в старых версиях windows. Сейчас планировщики операционных систем очень сложные. Там есть и приоритизация, и исполнение, и различные очереди.

Итак. Чтобы начать, нам понадобится какой-то бесконечный цикл, который будет опрашивать, есть ли у нас задачи. Например такой:

Теперь нам нужно откуда-то эти задачи получать.
Зададим себе вопрос: что является триггером, чтобы наш JS код начал выполняться?
Это может быть:

  1. Браузер загрузил тег
  2. Отложенные задачи: setTimeout , setInterval , requestIdleCallback
  3. Ответ от сервера через XmlHttpRequest , fetch и т.п.
  4. События и вызовы подписчиков через браузерное API: click , mousedown , input , blur , visibilitychange , message и так далее, множество их. Часть из них вызывается пользователем (кликнул на кнопку, альт-табнулся и т.д.).
  5. Изменение состояния промисов. В некоторых случаях, это происходит за пределами нашего JS кода
  6. Обзерверы, такие как: DOMMutationObserver , IntersectionObserver
  7. RequestAnimationFrame
  8. Что-то еще? 🙂

Почти все эти вызовы планируются через WebAPI (его еще иногда называют браузерным API). То есть:

  1. Мы вызываем setTimeout(function a()<>, 100)
  2. WebAPI откладывает задачу на 100мс
  3. Через 100мс, WebAPI кладет function a() в очередь (TaskQueue)
  4. Event Loop на нужный виток подбирает задачу

Нашему JS коду нужно как-то взаимодействовать с DOM. Считывать размеры элементов, добавлять свойства, рисовать какие-нибудь выпадающие элементы. Делать интерфейс живым.

Это накладывает ограничения на отрисовку элементов. Мы не можем просто так взять и запустить 2 потока, чтобы в одном выполнялся JS, а в другом CSS и отрисовка. Это потребовало бы большого количества синхронизаций кода, либо могло бы привести к неконсистентому исполнению. Поэтому и JS, и расчет отображения элементов (расположения, цвета и т.д.) работают в одном потоке. Значит в нашем Event Loop кроме JS есть «отрисовка». Давайте поместим ее в отдельную очередь и назовем render queue:

Итак, у нас есть две точки входа. Одна для большинства JS операций, другая для отрисовки. Теперь нам нужно разобраться, что же такое SomeJSTasks и как оно работает.

Для исполнения большей части JS кода у нас есть 2 очереди:

  • TaskQueue — здесь почти все ивенты, отложенные задачи и т.п. Задача из этой очереди Task
  • MicroTaskQueue — здесь обработка промисов и MutationObserver. Из этой очереди: MicroTask

Обновление экрана

Event loop неразрывно связан с обновлением экрана. В нем исполняется не только JS код, но и рассчитываются кадры страницы.

Браузеры стараются отображать изменения на странице максимально быстро. Здесь существует ряд ограничений:

  1. Хардварные ограничения: частота обновления экрана
  2. Софтверные ограничения: настройки ОС, браузера, настройки энергосбережения и т.д.

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

Для нашего Event Loop это означает, что на исполнение тасков отводится примерно 16.6мс (при 60FPS)

Что такое TaskQueue

Как только задачи набиваются в очередь Task, на каждом витке event loop мы достаем верхний таск и исполняем его. Если у нас остается достаточно времени, т.е. в render queue не появилось тасков, мы можем достать еще одни таск и т.д., пока не придет время для отрисовки нового кадра пользователю.

Разберем пару примеров:

У нас в очереди лежит 3 таска TaskA, TaskB, TaskC. Event Loop берет первый таск и исполняет. Он занимает 4 мс. Затем Event Loop проверяет другие очереди (Microtask queue и render queue) — они пустые. Поэтому EventLoop исполняет второй таск. Второй таск занимает еще 12 мс. Итого 16 мс. Браузер добавляет в Render queue таски на отрисовку нового кадра, Event Loop берет эти таски. Они занимают примерно 1 мс. После этого Event Loop переходит обратно исполнять таски из TaskQueue.

Event Loop не умеет предсказывать, сколько времени будет исполняться тот или иной таск. И он не умеет приостанавливать таск для того, чтобы отрендерить страницу пользователя, так как браузерный движок не может знать, можно ли отрисовать то, что изменил JS или это только подготовка элементов для отображения их пользователю. То есть: Пока исполняется синхронный JS код, изменения не будут отображены пользователю, но могут быть посчитаны.

Поэтому вот другой пример:

Здесь у нас только два таска в нашей очереди. Первый таск выполнялся 240 мс, так как 60FPS предполагает рендер кадра каждые 16.6 мс. А это означает, что мы пропустили примерно 14 кадров в эту секунду. Поэтому как только таск закончится, event loop начнет выполнять ожидающие таски из render queue, чтобы нарисовать кадр. Важный момент: то, что мы пропустили 14 кадров НЕ означает, что мы 15 раз будет рисовать кадры без остановки.

Прежде чем перейти к микротаскам, разберем такую штуку, как стек вызовов:

Стек вызовов

Стек вызовов — это список, который отражает, какие функции сейчас вызываются и куда будет совершен переход, после окончания исполнения текущей функции.

function findJinny() < debugger; console.log('It seems you get confused with universe'); >function goToTheCave() < findJinny(); >function becomeAPrince() < goToTheCave(); >function findAFriend() < // ¯\_(ツ)_/¯ >function startDndGame() < const friends = []; while (friends.length < 2) < friends.push(findAFriend()); >becomeAPrince(); > console.log(startDndGame());

Мы запускаем этот код в консоли браузера и останавливаемся на дебаггере. Как будет выглядеть наш стек вызовов?

Мы начали наш стек из inline кода, значит самым верхом будет эта строчка. Как правило, в хром она будет указана просто ссылкой на нее. Обозначим ее inline . Дальше мы падаем в startDndGame , и вызываем несколько раз findAFriend . Однако он не попадет в итоговый callStack , потому что из всех findAFriend мы вышли. Итого наш стек вызовов будет выглядеть вот так:

Что же такое микротаски?

Микротаски ограничены. Это либо колбеки для promise, либо mutationObserver. Сама идея появления микротасок довольно костыльна, но она дает нам некоторые преимущества и недостатки по сравнению с TaskQueue.

Главная особенность микротасков: они будут исполнены сразу же, как только освобождается стек вызовов.

Например, в случае с этим стеком вызовов:

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

Любой JS код записывается в стек вызовов, что логично. Окончание стека вызовов, по сути, и есть окончание таска или микротаска. Интересный факт: Микротаски могут порождать другие микротаски, которые будут вызываться сразу же по освобождению стека вызовов. То есть рендер страницы может откладываться бесконечно. Это главный минус микротасков:

Если у нас 4 микротаски в очереди MicrotaskQueue, то они будут исполнены друг за другом, откладывая ререндер.

Недостаток микротасок — также и их преимущество. Например, момент срабатывания MutationObserver — это когда изменения на странице произошли, но пользователь их не увидит, пока колбек mutation observer не исполнится. Таким образом, мы можем эффективно управлять контентом, который видит пользователь.

Итого, наша картина event loop:

Мы уже разобрали, что таски могут следовать друг за другом, пропуская RenderQueue (если в ней нет тасок).

Но что исполняется внутри RenderQueue?

Рендер кадра можно поделить на несколько основных этапов. Каждый этап внутри может быть разделен на другие подэтапы (мы посмотрим это на примере Layout):

Остановимся на каждом этапе подробнее:

RequestAnimationFrame (raf)

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

Позволяет нам подписаться и запланировать изменения в DOM прямо перед отрисовкой страницы. Несколько важных фактов:

  1. Колбек для raf принимает аргумент: DOMHighResTimeStamp — количество миллисекунд прошедших с начала жизни документа. Поэтому внутри колбека можно не брать время через perfomance.now , нужное время уже присутствует
  2. Аналогично setTimeout , raf возвращает дескриптор ( id ), поэтому запланированный raf можно отменить через cancelAnimationFrame .
  3. Если пользователь переключился на другой таб, свернул браузер — перерисовки страницы не происходит. Raf тоже не вызывается
  4. JS код, который изменяет размеры элементов, считывает свойства, может зафорсить requestAnimationFrame
  5. Как посмотреть, как часто браузер обновляет кадр? Вот так:

const checkRequestAnimationDiff = () => < let prev; function call() < requestAnimationFrame((timestamp) => < if (prev) < console.log(timestamp - prev); // Должно быть в районе 16.6 мс, при 60FPS >prev = timestamp; call(); >); > call(); > checkRequestAnimationDiff();

Вот например, мой эксперимент (запускал на hh.ru):

  • Сафари вызывает(ал) raf после отображения кадра, а не до. Пруф: https://github.com/whatwg/html/issues/2569#issuecomment-332150901
  • Style (recalculation)

    Браузер пересчитывает стили, которые должны примениться из-за изменений, запланированных JS. Здесь же происходит вычисление активных media queries

    Пересчет включает в себя как прямые изменения a.styles.left = ’10px’ так и те, которые описываются через CSS файлы, например element.classList.add(‘my-styles-class’) Все они будут пересчитаны с точки зрения работы CSSOM и получения Render tree.

    Если запустить профилировщик и открыть сайт hh.ru, вот тут можно найти время, потраченное на Style:

    Layout

    Вычисление слоев, расчет положения элементов на странице, их размеров, взаимного влияния друг на друга. Чем больше DOM элементов на странице, тем тяжелее эта операция.
    Современные браузеры могут организовывать рендер и layout дерева по-разному. Например, в Хроме, кроме Layout в профилировщике вы увидите такие процессы как update layer tree и layout shift , который и отвечает за сдвиг элементов относительно друг друга.
    Здесь на графике выделенная строка — тот самый Layout сайта hh.ru при первом открытии приложения.

    Layout — это очень болезненная операция для современных веб-сайтов. Болезненна она потому, что наиболее тяжелый style recalculation происходит при первом рендере, а вот Layout происходит при:

    1. Чтении свойств влияющих на размер и положение элементов (offsetWidth, offsetLeft, getBoundingClientRect, и т.д.)
    2. При записи свойств, влияющих на размер и положение элементов, за исключением некоторых свойств, вроде transform и will-change . Для transform браузер задействует composition процесс, а в случае с will-change браузер попытается использовать composition . Вот здесь список актуальных причин.

    Layout отвечает за:

    • Вычисление слоев
    • Расчет взаиморасположения элементов на слое
    • Расчет влияния одних элементов на другие

    Layout (а вместе с ним raf и Style) может происходить не в свою очередь, когда нужно отрендерить страницу и применить изменения, а тогда, когда JS изменил размеры элементов или считал данные. Такой процесс называется force layout . Вот полный список свойств, который приводит браузер к остановке исполнения JS и вызову Layout.

    div1.style.height = "200px"; // Изменили размер элемента var height1 = div1.clientHeight; // Считываем его размер

    Браузер не сможет рассчитать clientHeight нашего div1 без пересчета его реальных размеров. В этом случае, браузер приостановит исполнение JS кода (совсем) и выполнит по очереди: Style (чтобы определить, что изменять), Layout (чтобы определить, как изменилось). Layout должен рассчитать не только элементы, которые находятся перед, но и после div1. Современные браузеры оптимизируют расчет так, чтобы не пересчитывать абсолютно все дерево. Но в худшем случае этого не избежать. Процесс пересчета элементов называется Layout Shift . Вот так его можно посмотреть (справа список всех элементов, которые были сдвинуты и модифицированы во время Layout):

    Также браузеры пытаются не форсить layout каждый раз. Поэтому, чтобы выиграть по скорости операции чтения и записи группируют:

    div1.style.height = "200px"; var height1 = div1.clientHeight; //  

    В этом коде браузер запланировал изменение высоты div1 до 200px, но затем получил задачу на считывание. Пришлось сделать layout. Затем ситуация повторилась. Обратите внимание, браузер не произвел layout на операциях записи. Потому что в этот момент нужные данные у него уже были.

    Давайте сгруппируем чтение и запись:

    div1.style.height = "200px"; div2.style.margin = "300px"; var height1 = div1.clientHeight; // 

    Такая группировка позволяет нам избежать второго layout, потому что браузер уже посчитал расположение элементов.

    Layout работает со "слоями" в нашем потоке. Посмотреть на то, как браузер выделяет слои можно в chrome devtools -> More tools -> layers:

    Таким образом, наш event loop превращается из одного витка в несколько, потому что и на этапе tasks, и на этапе microtasks мы можем запустить force layout:

    Базовые советы для оптимизации layout:

    1. Уменьшать количество DOM нод
    2. По возможности избегать force layout
    3. Компоновать чтение и запись свойств
    Paint

    На этом шаге отрисовываем элементы, применяем стили color, background и т.д. Во время первого рендера сайта мы потратим на это достаточно времени:

    В целом, эта операция не критична, так как в общем случае она не занимает много времени. Особенно после первого рендера.

    После того, как пиксели нашей страницы обзавелись цветом, мы приступаем к следующему шагу — Composition.

    Composition

    Это единственная операция, которая в классическом веб-сайте исполняется с помощью GPU. На этом этапе браузер исполняет специфические CSS стили, например transform.

    Задача этой операции: совместить слои и получить готовый кадр.

    Важное дополнение: само по себе свойство transform: translate не включает рендер элемента на видеокарте. То есть сделав transform: translateZ(0) вы не "перенесете элемент на видеокарту", это заблуждение.

    Однако современные браузеры могут сами переносить часть операций на GPU. Лучше всего с актуальной информацией по этому вопросу знакомиться из исходников.

    Именно с помощью transform советуют создавать сложные анимации. Секрет довольно прост:

    1. Анимация на transform позволяет нам не вызывать layout каждый кадр, мы экономим время
    2. Она позволяет нам избавиться от артефактов "мыльца" при анимациях, которые иногда бывают при анимировании left, right, top, bottom

    В то же время, верстать весь сайт только через transform не рекомендуется, так как это приводит к своим тормозам. Важно совмещать правильно верстку.

    Как оптимизировать рендер?

    Самая тяжелая операция для рендера кадра (для большинства сайтов) — layout. Сверху на графиках был layout для рендера главной страницы hh.ru. При сложных анимациях каждый кадр может пересчитываться все элементы в DOM, а это означает, что каждый кадр вы будете тратить по 13-20 мс впустую. Это ведет к пропуску кадров и проблемам с производительностью вашего сайта.

    Мы можем пропустить Layout, если изменяем цвета, фоновое изображение и т.д.

    Мы можем пропустить layout и paint, если изменения основаны на стиле transform и не затрагивают чтение свойств.

    Итого, советы по оптимизации можно сгруппировать так:

    1. Выносите анимации на CSS. Исполнение JS кода не бесплатно
    2. Изменяйте transform свойство для “перемещения” объектов
    3. Используйте will-change свойство — это свойство, которое позволяет браузерам "подготовить" дом элемент к изменениям определенных стилей. Важно — это свойство помогает браузеру понять, что разработчик запланировал изменить. Это свойство нельзя применять к большому количеству элементов, иначе вы получите тормоза.
    4. Используйте батчевые изменения в DOM
    5. Используйте requestAnimationFrame для планирования изменений в следующем кадре
    6. Комбинируйте задачи на запись \ чтение свойств элементов. Обращайте внимание на вызовы свойств, которые форсят layout.
    7. При возникновении сложных ситуаций, лучше всего запустить профилировщик и посмотреть на частоту и время вызовов. Это даст вам информацию о том, какой этап тормозит, оптимизируйте каждый этап отдельно.

    Вместо заключения

    В этой части мы разобрали каким образом работает Runtime в нашем браузере, его сильные и слабые стороны. Это позволяет нам:

    1. Понимать, как лучше писать код
    2. Знать в какую сторону смотреть при появлении проблем

    Также мы получили достаточно глубокое понимание event loop и его составляющих:

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *