Оптимизируем метрики сайта на Magento 2

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Если попытаться охватить все аспекты и тонкости метрик и оптимизаций, то материала будет с «китайскую стену». Так что вкратце.

Преамбула

... о базовых принципах браузеров.

Загрузка каждого файла (в частности JS) состоит из этапов: загрузки, execution (который в свою очередь состоит из парсинга и компиляции).

Загрузка

Загрузка файлов происходит параллельно. Но если сервер или ваш браузер не поддерживает протокол http2, то количество параллельно загружаемых файлов будет ограничено. Все современные браузеры (даже мобильные) уже поддерживают протокол http2. И сейчас все серверы тоже стараются его использовать. Так что, идейно переживать за количество JS файлов (и «лепить» громоздкие bundling-и) не стоит.

Но ходят слухи о том, что чем меньше будет самих файлов (по количеству), тем меньше время их парсинга. Т.е. бандлинги в принципе — дело полезное (размер файлов в пределах 50-100 кБ). Но тесты на WebPageTest показали, что это не так.

Мы протестировали наш девсайт на Magento 2.4.

Results:

Без bundling-а:

  • Суммарный размер JS-файлов: 3.97MB.
  • FCP/LCP — 4.088s (это тестовый сайт, так что не обращайте внимания).

С bundling (тестировался разный размер бандла: 50/100/250 KB):

  • Суммарный размер JS-файлов: ~7MB.
  • FCP/LCP — ~5s.
  • Остальные метрики были хуже на ~30-60%.

Так что....

(если на сервере настроен http2)

Execution

Каждый JS-файл браузеру нужно распарсить и выполнить. И если загрузка может происходить параллельно, то «выполнение» (execution) происходит в основном потоке процессора по очереди. Выполнение JS файлов — самый «прожорливый» процесс и создает основные блокировки по рендеру сайта:

(не считая время загрузки).

Но выполняется не только JS. «Разукрашивает» страницу тоже CPU в том же потоке.

Советы по оптимизации JS execution

Что можно сделать для оптимизации:

1) Чем меньше размер JS файлов в целом на странице, тем меньше время их парсинга. Вы можете не грузить какие-то массивные куски кода, которые редко используются.

Пример: кнопка чата на страницах сайта (вот та назойливая в нижнем углу, на которую кликает процентов 5-10 пользователей, а остальные должны грузить ваш скрипт каждый раз на каждой странице). Вам нужно просто создать кнопку «такую же как в чате», по клику на которую будет загружаться в DOM и выполнятся JS вашего чата. Такой чат откроется не моментально, но добавить прелоадер — и пользователь уже не будет нервничать (т. к. он сам вызвал это действие). Да и ждать ему придётся, скорее всего, не больше секунды. И для 90-95% пользователей (и для метрик) вы улучшите показатели.

Или это может быть сторонний поиск (например, Алголия). Или карты. Вот ещё статья по этому поводу.

2) Также сложные куски JS обрабатывать в отдельном потоке. По данному пункту («однопоточный JavaScript и как с этим бороться») можно долго говорить. Выйдет статья не меньше этой. В двух словах: использовать «webworker». Пару статей:

  1. webworker-ы и асинхронность
  2. Webworker-ы

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

Не уверен, что асинхронность (описанная в MDN Web Docs) спасёт при самой загрузке страницы. Это более полезно для оптимизации взаимодействия с пользователем.

Также можно трудоемкие куски выносить «в конец» потока. Каждый раз, когда вы используете setTimeout/setInterval — код внутри этих методов помещается в конец потока и будет выполнен только после того, как выполнит весь остальной JS код.

3) Атрибуты async и defer.

Если в двух словах: можно отложить выполнение JS до тех пор, пока не распарсится весь DOM. Это хорошо для скриптов с domready() и тех, которые не нужны, пока пользователь не кликнет на что-то (скрипты поиска, гугл карты в попапах, да и вообще все что касается невидимого контента). Выполнять такие скрипты во время парсинга самой страницы — нет смысла. Поэтому им смело можно прописывать атрибут «defer». Только не путать с «перенесением JS в footer страницы» (о чём будет дальше).

НО! На деле не всё так просто.

По первому пункту. Как говорится: «из песни слов не выкинешь». Хотя, по-хорошему, желательно стараться писать код «без излишеств». «Краткость — сестра таланта».

По второму пункту. На практике, в частности с обычными сайтами, вряд ли найдётся что-то, что можно вынести в отдельный поток.

По третьему пункту. Если вы подключаете JS-файлы «вручную» — то добавить необходимые атрибуты не составит труда. А вот если у вас какой-то фреймворк/CMS, то уже будет зависеть от возможностей самой системы.

Итого

Идейно вы можете сделать всё из вышеперечисленного, но по факту это требует неимоверных трудозатрат). Что может быть несоизмеримо с результатом.

Главные советы:

  • Стараться писать код «сразу нормально». Планировать необходимый код до начала разработки. Что бы не повторять один и тот же код и не писать лишнего в целом.
  • Если часто используете один и тот же сторонний ресурс или функциональные блоки (чат, поиск) — есть смысл разработать фичу «отложенной инициализации» (пункт 1). Это может занять время, но вы это сделаете один раз и будете с легкостью применять на следующих проектах.
  • Разобраться с инструментами, позволяющими выполнять пункт 3. Это может занять время, но вы это сделаете один раз и далее будет проще применять на следующих проектах. Хотя лично мне больше нравится подход описанный немного дальше («Дополнительно по оптимизации»).

Советы по оптимизации CSS execution

Задействуйте GPU

Отрисовка страницы также происходит посредством CPU. На графическое ядро браузер передаёт только «композитные слои». В композитный слой элементы могут попадать сразу или «по необходимости». Сразу в композитные слоя выносятся:

  • 3D-трансформации: translate3d, translateZ и т.д.
  • Элементы <video>, <canvas>, <iframe>.
  • Анимация «transform» и «opacity» через Element.animate().
  • Анимация «transform» и «opacity» через CSS Transitions и Animations.
  • «position: fixed».
  • will-change.
  • filter.

Вот визуальный пример того, что когда выполняется JS, всё остальное «замирает».

Но не переусердствуйте, т. к. может закончиться память. Ведь любой элемент это массив пикселей: {ширина} x {высота} x {количество байт на пиксель}. «Количество байт на пиксель» обычно 3 (RGB). А в случае с прозрачностью, то 4. Т.е. квадрат 100×100 занимает 30 000 байт (если с прозрачностью, то 40 000 байт).

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

Более подробно в статьях тут и тут.

Вообще старайтесь писать компактный CSS.

Используйте поменьше наследования в классах

Пример. Вместо:

.page-products .column.main .box-store-locator .store-locator .box-current-store-info .current-store-hours-title {}

Можно сократить (если не ещё больше):

.page-products .store-locator .box-current-store-info .current-store-hours-title {}

Особенно это касается LESS/SASS стилей, где визуально вложенность не кажется «такой громоздкой».

Если вы удалите промежуточные классы для ~ 100 строк, вы сэкономите ~ 1 КБ.

А ещё лучше — используйте методику BEM. Тогда для указания элемента вам понадобится прописать всего один класс.

Не используйте картинки

Часто вижу, что для стрелочек или других простейших иконок используют картинку (svg/jpg/png в виде фона).

Ни одна картинка не будет меньше пары строк CSS. Тем более, что CSS вы потом минифицируете и он ещё сожмётся при передаче.

Группируйте селекторы

Вместо:

.some-common-class { margin: 0px; padding: 0px;}
.menu-list { margin: 0px; padding: 0px;}
.some-another-list { margin: 0px; padding: 0px;}

Пишите:

.some-common-class,.menu-list,.some-another-list { margin: 0px; padding: 0px;}

Если говорить про Мадженту, то там «из коробки» есть метод «&:extend()».

Аналитика

Где протестировать

Все знают про «PageSpeed insights» от Google. Но помимо него есть и другие способы.

PageSpeed insights

Плюсы:

  • Удобен тем, что дает много конкретных рекомендаций со ссылками (с учетом платформы, на которой разработан сайт).
  • Сразу анализирует мобилку и десктоп.
  • Может показать данные с реальных устройств. Если вашу страницу посетили более 500 раз за месяц с Chrome-браузера, то в начале страницы (над лабораторными данными), вы увидите основные метрики «в реальной жизни». Среднее значение за последних 28 дней. (т. к. JS/CSS/картинки кэшируются браузером и показатели на самом деле могут быть лучше).

Минусы:

  • Дев сайты может не тестировать (доходит до 80-90% анализа и говорит: «Не получилось. Попробуйте позже»).
  • Не имеет временной шкалы.

GTmetrix

Имеет много показателей. Временная шкала и выбор устройства доступны только зарегистрированным пользователям.

WebPageTest

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

Lighthouse

Плагин для браузера Chrome.

«PageSpeed insights» от Google использует Lighthouse. Так что вы получите такие же данные, только быстрее и без проблем (не будет случаев что анализ «отвалится»). А самый главный бонус: можно тестировать локальные сайты.

Мини вступление

Основные две составляющие программ аналитики:

  1. Время отрисовки страницы/контента, которое по факту прямолинейно размеру/вложенности HTML-я, размерам JS/CSS/картинок.
  2. Поведение страницы во время загрузки и после.

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

Стоит начать со второго т. к. там всё просто: во время и после загрузки страницы, контент должен оставаться на своём месте. В метриках эта характеристика называется «Cumulative Layout Shift (CLS)».

Немаловажная оговорка «... и после». Т. к. при скролле страницы,и взаимодействию с ней элементы по-прежнему должны оставаться на своих местах. При загрузке такие вещи в аналитику не попадут. Но попадут в статистику «реальных данных» в «PageSpeed insights».

Cumulative Layout Shift (CLS)

Контент не должен «прыгать» (менять положение). Например, если на странице есть баннер, но он не загружается сразу, то для него должно быть «зарезервировано» пустое место. Эти «сдвиги» важны даже при прокрутке страницы.

Вы можете «отловить» такие «сдвиги», запустив следующий код в Chrome (или Edge, он не работает в других браузерах):

new PerformanceObserver(l => { l.getEntries().forEach(e => { console.log(e); })}).observe({type: ’layout-shift’, buffered: true});

Или

new PerformanceObserver(l => { var i = 0, sum = 0; l.getEntries().forEach(e => { console.log(e); sum +=e.value; i++; }) console.log(’Total’, i); console.log(’Summa’, sum);}).observe({type: ’layout-shift’, buffered: true});

В консоли будут отчеты обо всех сдвигах.

Одиночные «value» до 0,1 не важны. Но важна общая сумма. У вас может быть 200 сдвигов со значением 0,1, и в итоге вы получите 20% shift. Также в логах будут указаны блоки, которые двигались (это означает, что-либо сам блок движется, либо блок перед ним внезапно «вздулся» и сдвинул нижние).

Начните исправлять CLS сверху. Сдвиги в заголовке очень важны. Реальный пример: удалил этих пару пикселей:

И получил +0,012 балла (1,2%). Также исправил сдвиг на 600 пикселей в нижнем колонтитуле и получил около 1%.

Блоки с позицией «fixed» игнорируются.

Метрики загрузки страницы

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

Основные метрики:

  • First Contentful Paint (FCP). Первая визуализация, когда появляется текст или изображение. Очень важный параметр.
  • Largest Contentful Paint (LCP). Показывает, когда появляется самый большой блок. Очень важный параметр.
  • Time to Interactive (TTI). Время, когда пользователь уже может взаимодействовать со страницей.
  • Total Blocking Time. Время, когда страница не интерактивна из-за того что процессор занят «выполнением» контента страницы (см. Преамбула -> Execution).
  • Speed Index. Мнимая величина, которая вытекает из вышеперечисленных.

«Speed Index» отдельно можно не поднимать. Он сам улучшится, когда улучшатся остальные показатели.

«Total Blocking Time» и «Time to Interactive»

Чем быстрее отрисуется страница, тем быстрее она будет доступна для взаимодействия (скролл, клик на интерактивные элементы, анимация).

Для уменьшения «Total Blocking Time» и ускорения «Time to Interactive» нужно разгрузить «основной поток». См. «Советы по оптимизации» выше и следующий абзац и «будет вам счастье»:)

Дополнительно (глобально) по оптимизации

По факту, как уже и говорилось, все показатели метрики напрямую зависят от размера страницы и подгружаемого контента. И вы можете сказать: «Ну не могу же я удалить то или это. Клиент заказал и оно должно быть». Ка бы да, и нет. Вспоминаем пример с кнопкой чата.

И вот ещё интересный пример. У сайта показатели 96-98 поинтов. И в основном благодаря тому, что они ВЕСЬ JS загружают с задержкой. По принципу «lazy-load». Теги вместо атрибута «src» содержат атрибут «data-src». И в конце страницы инлайновый JS код, который по истечению определённого времени (или при скроле, если тот будет раньше) создает теги с нужным «src» (и атрибутом «async»). В итоге загружается страница размером 1МБ за пару секунд. А остальные 15 МБ «подтягиваются» позже.

В результате пользователь видит страницу сразу, показатели метрики отличные. Единственный минус — на сайте сразу ничего не накликаешь т. к. ничего толком не работает. Слайдеры, меню, попапы, дропдауны, корзина. Всё станет динамичным только через 10 секунд (или через несколько секунд после скролла, но всё равно не сразу). Но какова вероятность, что пользователь сразу же кинется кликать на слайдеры или меню?

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

Если вдруг кто «первый раз слышит», то немедленно ознакомьтесь. Вот несколько статей тут, тут и тут.

Если говорить про Мадженто, то с версии 2.4 уже есть частичная поддержка «из коробки»: на странице категории изображения продуктов подгружаются «лениво». Но одного каталога, честно сказать, маловато. Обычно на домашней странице и странице продукта тоже много больших картинок. И тут уже придётся либо «что-то придумывать», либо использовать модули (которые для Мадженты есть даже бесплатно).

Про формат/размер изображений, минификации, сжатие данных сервером (Gzip, Deflate) и прочее не будем тоже останавливаться. Если вы с этим ещё не разбирались, то тулзы тестирования сайта вам подскажут.

А теперь разберём ещё две важные метрики (на раду с CLS): FCP и LCP.

First Contentful Paint (FCP)

FCP time(in seconds)Color-codingFCP score(HTTP Archive percentile)
0–2Green (fast)75–100
2–4Orange (moderate)50–74
Over 4Red (slow)0–49

Как уже говорилось: это время до отображения какого либо контента. И зависит напрямую от размера . Ну и, естественно, от времени ответа сервера.

Браузер не будет рендерить страницу, пока не загрузит секцию страницы. И тут всё просто: чем меньше контента в теге head, тем быстрее начнет прорисовываться страница. В частности, это касается FCP (так как с LCP могут быть особенности).

Идеальным размером секции считается до 170 КБ. Т. е. в хедере оставляем только самое необходимое. Всё остальное выносим в футер. Таким JS файлам уже и не нужен атрибут defer.

Выносить можно не только JS. А также стили и шрифты. Но к вашей странице применятся стили и шрифты только когда «до них дойдет дело». Т. е. пользователь увидит сначала блоки и текст без стилей и шрифтов, и только когда выполнение дойдет до конца страницы, то только тогда применятся css/fonts. И в таком случае вы получите очень большой показатель сдвигов (CLS).

Даже здесь есть выход: загружать в только стили тех блоков/элементов, которые видно сразу. Стили «невидимых» блоков (меню/дропдауны/попапы/слайдеры/аккордеоны) можно переносить в футер. А тексту указывать дефолтный шрифт, который максимально похож на загружаемый. Но разделить стили на две части на практике оказывается проблематично. В случае с Мадженто трудозатраты по времени не оправдываются.

Так что стили и шрифты зачастую остаются в . А в футер переезжают только JS файлы.

Largest Contentful Paint (LCP)

LCP time(in seconds)Color-coding
0-2.5Green (fast)
2.5-4Orange (moderate)
Over 4Red (slow)

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

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

Должно быть: «гармошка» или «кнопка с вызовом всплывающего окна» или «ссылка на отдельную страницу».

Не используйте AJAX / «lazy» для объектов LCP!

Вы можете использовать base64 в качестве источника изображения. В этом случае изображение будет сразу доступно на странице.

Код для определения «какой блок считается LCP» (чтобы понимать какой блок оптимизировать).

new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { console.log(’LCP candidate:’, entry.startTime, entry); }}).observe({type: ’largest-contentful-paint’, buffered: true});

Также LCP можно увидеть у метрик с временной шкалой (WebPageTest).

Был когда-то хак: показывать сначала картинку плохого качества, а после загрузки страницы — загружать изображение «нормального качества». Но на сегодняшний день это уже не работает.

А вот что работает: если LCP — это баннер в начале страницы, то сделайте его на весь экран. Система может распознать её как фон и тогда LCP станет блок поменьше. Например: текст на баннере или логотип.

Т.е. «Width: 100vw; height: 100vh». А всё что над баннером, должно быть «position: fixed».

Личный опыт

Последний проект. Magento 2.4.

По факту, отнюдь не всё из этой статьи было реализовано на проекте.

«Сдвиги» сразу проверялись на этапе разработки. Так что на выходе нам не пришлось исправлять CLS вообще. И в целом старались стили писать «красиво». Они остались в хедере.

«Lazy load» тоже отдельно не ставили. Просто использовали дефолтный loading="lazy" . (Правда, поддержка браузерами у него слабая).

Перенесли JS по максимуму в футер. Поскольку делали это не сразу, то на страницах Корзины и Чекаута пришлось отключить фичу из-за большого количества ошибок.

Прописали «defer» атрибут инстаграмовским виджетом.

Применили «лайфхак» с баннером на мобилке чем увеличили LCP.

Результат:

Хомяк: 22 / 78 поинтов
PLP (Категория): 51 / 94
PDP (страница продукта): 45 / 81

Пару советов «на дорожку»

  • Старайтесь избегать инлайновых JS/CSS. Они не кэшируются браузером. Следственно «реальные данные» будут хуже, чем могли бы быть.
  • Притча. Пришла пара с младенцем к знахарю и спрашивает: «Как нам вырастить здорового ребёнка?». «А сколько младенцу», — спросил знахарь. «Три месяца», — ответили родители. На что знахарь им ответил: «Вы опоздали на 3 и 9 месяцев...».
    Пытаться исправить ситуацию после разработки — мало толку. Переписывать CSS на GPU и рефакторить JS займёт неимоверно много времени. Равносильно «написанию с нуля». Или даже больше (т.к. переделывать сложнее чем создавать «сразу нормально»). Этим вы вряд ли будете заниматься. Или вам нужно рефакторить какой-то блок, который встречается на сайте в многих местах. Проще это делать сразу на этапе разработки (до того как блок накопируется). Править копии — лишнее время и возможные баги.
  • Если разрабатывать JS код, который подключался «в начале страницы», а потом двинуть его «в конец страницы», то вас будет ждать сюрприз: куча ошибок. «JS оптимизации» нужно настроить сразу, на этапе старта проекта. Тогда ошибки будут решаться по мере появления, а не сразу 100500 и пойди пойми какая от чего.
  • После разработки в среднем прогноз такой: +10-30 «попугаев» за 20-30 ч (при условии, что для вашего фреймворка есть готовые решения/модули/плагины).

Спасибо, что прочитали нашу статью! Заходите в наш клуб. Там много интересного.

👍НравитсяПонравилось17
В избранноеВ избранном6
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Подписаться на комментарии