Улучшаем производительность сайта на Magento 2 так, чтобы это понравилось Google Page Speed Insights

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

Меня зовут Сергей, я — CTO в Magecom. Моя статья основана на докладе, с которым я выступал на Magento Meetup #11, где я рассказал о результатах трудов всей нашей команды Magecom.

Допустим, у вас стоит задача улучшить производительность сайта, про который вы пока что почти ничего не знаете. Например если это новый клиент, который как и пришел к вам с конкретной проблемой, что у него медленно работает сайт. Сайт, естественно, на Magento2.

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

Google PageSpeed Insights

Для оценки результатов нашей работы мы используем Google PageSpeed Insights.

Это набор скриптов для измерения метрик. Но не только. В первую очередь, я бы сказал, что это инструмент, который используется клиентом, чтобы подтвердить свои ощущения, что его сайт как-то медленно работает. Т. е. этот же инструмент можно использовать и для того, чтобы показать клиенту эффективность вашей работы над улучшением производительности. Да, могут быть чисто субъективные ощущения скорости работы («У меня работает быстро»), но цифры — лучше.

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

Как мы видим этот процесс

Довольно часто можно увидеть у клиента вот такой жизненный флоу проекта.

На старте поднимается Magento, у которой перформанс из коробки небольшой. Далее ставится n-е количество модулей, покупная тема — производительность проседает еще больше. На этапе go-live увеличивается объем контента — много продуктов, страниц, туча виджетов на главной странице, полотно категорий в главном навигационном меню (читай узлов в DOMe). Перформанс проваливается. После go-live выдохнули, посмотрели что сайт работает медленно, потратили недельку на «что-то с этим сделать» немного улучшили показатели, но даже не достигли показателей на начале разработки.

Вторая строчка — это часы, которые при таком подходе были потрачены на улучшение performance.

Как бы мы хотели видеть этот процесс

Мы бы хотели, чтобы у нас на старте проекта и во время девелопмента было все по максимуму. Естественно, допускается незначительная просадка, особенно на этапе go-live, но после относительно небольших усилий, мы должны вернуть показатели в норму.

Но для этого нужно сделать небольшие инвестиции (требование — до 8 часов) на старте проекта и в период разработки.

Собственно, моя статья про то, что можно сделать за эти 8 часов на старте проекта. Причем неважно, вы проект с нуля стартуете, или он вам только что зашел в компанию. Наша задача — поднять максимально стандартизировано performance и с ним дальше жить.

Каким образом мы эту процедуру разрабатывали? Мы просто пошли по пути наименьшего сопротивления и начали проверять все, что советует нам Google PageSpeed Insights и Lighthouse.

Prerequisites: измеряй перформанс

npm install -g lighthouse-ci
npx lighthouse-ci https://som.e/plp.html --filename=plp.html

Главное правило — вам нужно прийти к тому, что вы должны измерять performance.

Отлично, что есть тулзы, которые это позволяют делать автоматически. Очень хорошо, если у вас настроен CI/CD. Можно установить Lighthouse CI — npm-пакет, который можно запускать при пушах и деплоях. Он будет всегда выдавать репорт при каждом pull request, благодаря чему вы всегда будете видеть, какой pull request ломает performance.

Допустим, установили модуль, добавили 150К UI-скриптов на фронт на все страницы, хотя это вроде просто стор локатор. Лучше избежать ситуации, когда вы поставите стор локатор и ничего не замерив, сделаете на основании этого стор локатора на чекауте in-store pick up, а дальше выяснится, что он очень медленный, и вам нужно это все дело выпиливать, а у вас уже чекаут на нем построен. Конечно, лучше, чтобы вы сразу получали нотификации об этом и могли моментально фиксить.

Итак, первое, с чего стоит начать — это настроить измерение performance.

Что нам рекомендует Google PageSpeed

Для оценки производительности Google Page Speed Insights измеряет ряд метрик, а также оценивает, некоторые параметры сайта, которые по его мнению могут существенно повлиять на эти метрики. Для каждого такого параметра есть рекомендации, как его улучшить, если он вдруг оказался в красной (и не только в красной) зоне. Иногда это общие рекомендации, иногда — персонализированы под конкретную платформу. Собственно по ним и пройдемся.

Не подгружать картинки, которые не видны на первом экране

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

Решений этой проблемы есть несколько:

  • Старый способ: зайти на marketplace, найти extension c lazy load и установить.
  • Новый способ: так как HTML5 поддерживает самостоятельно lazy load картинок, для этого нужно к тегу img просто добавить атрибут loading=lazy.

Мы в Magecom написали небольшой extension для этого, который, если опустить детали, выглядит приблизительно так:

/**
 * @see \Magento\Framework\Controller\ResultInterface::renderResult
 */
public function afterRenderResult(
    \Magento\Framework\Controller\ResultInterface $subject,
    \Magento\Framework\Controller\ResultInterface $result,
    \Magento\Framework\App\ResponseInterface $response
): \Magento\Framework\Controller\ResultInterface
{
    $content = $response->getBody();
    $content = $this->imageProcessor->process($content);
    $response->setBody($content);
    return $result;
}
public function process(string $content): string
{
    $closure = function (array $match) {
         return str_replace('<img', '<img loading="lazy"', $match[0]);
    };

    return preg_replace_callback('/(<\s*img[^>]+>)/i', $closure, $content);
}

Перед тем, как сервер вернёт html в браузер, проходимся регуляркой по этому html и всем тегам <img> добавляем атрибут loading="lazy«. Всё.

Сократить время ответа сервера

Самая наболевшая тема. Если опустить детали, то Magento медленная.

Основная причина, которая часто упоминается в контексте этой проблемы — это огромное количество модулей, которые не факт, что будут использоваться в приложении. Поэтому один из способов улучшить этот показатель, это выключить неиспользуемые модули. По этой теме можно найти несколько докладов на различных площадках и даже на Mageconf об этом упоминалось. В сети есть много решений какие модули можно выключить, если вы, например не используете MSI или GraphQL. Решений масса.

Эффект очевидный — чем больше модулей мы выпилим, тем меньше конфигов будет собираться, и быстрее работать всё это дело. Ну а если выпилить максимальное количество модулей, то и эффект будет максимальный

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

public function getUnusedModules(): array
{
    $allModules = $this->getAllModules();
    $requiredModules = $this->getRequiredModules();
    $unusedModules = array_diff_key($allModules, $requiredModules);
    ksort($unusedModules);
    return $unusedModules;
}

private function getRequiredModules(): array
{
    $requiredModules = [];
    $modules = $this->configProvider->getConfig();

    foreach ($modules as $moduleName) {
        $this->addRequiredModules($requiredModules, $moduleName);
    }
    ksort($requiredModules);
    return $requiredModules;
}

private function addRequiredModules(array &$requiredModules, string $moduleName): void
{
    if (array_key_exists($moduleName, $requiredModules)) {
        return;
    }

    $module = $this->moduleRepository->getModuleByModuleName($moduleName);
    $requiredModules[$moduleName] = $module;
    foreach ($module->getDependencies() as $dependency) {
        $type = $dependency['type'];
        $dependencyName = $dependency['module'];
        if ($type === 'hard') {
            $this->addRequiredModules($requiredModules, $dependencyModule->getFullName());
        }
    }
}

Создаем whitelist модулей, которые используются на проекте, например Magento_ConfigurableProduct, Magento_Checkout, Magento_Sitemap, добавляем к этому списку все зависимости этих модулей (см. composer.json секция require) и завимости зависимостей, и так рекурсивно, пока не получим полный список модулей, используемых на проекте. А далее удаляем все оставшиеся модули, перечислив их в секцию replace в composer.json проекта.

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

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

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

Preload Key Request

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

Решение может быть таким: нужно найти эти ресурсы и добавить их в xml layout, через link и указать relation preload.

На web.dev, где описана работа этого алгоритма, можно посмотреть, как это вычитывать. Или можно просто при запуске Google PageSpeed lnsights в репорте посмотреть какие ресурсы ключевые, чтобы добавить их в preload.

Минифицировать CSS

Этот пункт уже в Magento реализован. Все, что нам осталось сделать — включить минификацию. Я предлагаю делать это на этапе, когда мы стартуем проект.

bin/magento config:set dev/css/minify_files 1 --lock-config

Например, у нас в компании настройка проекта разворачивания автоматизирована, поэтому она добавлена в скрипты с билдами. У нас проект со старта в config.php прописан, что мы минифицируем скрипты. Если кому-то локально в девелопер режиме нужно, чтобы они были не минифицируемые, тогда он в env.php перебивает настройки.

Минифицировать JavaScript

Если у вас минификация для JavaScript не включена, Lighthouse будет ругаться и попросит выключить дефолтную минификацию и использовать Terser. При этом если вы включите минификацию в Magento, то он не будет ругаться и не заметит, что вы используете Magento минификацию, а не Terser.

Terser лучше выжимает, но профита не особо много, поэтому использовать его только ради этого я не вижу. Чуть дальше Terser нам будет полезен, и посмотреть его можно на Github.com.

Если не хотите ставить и настраивать Terser, то есть другие варианты.

Устранить ресурсы, блокирующие рендеринг

Задача — как можно быстрее отрисовать пользователю первый экран, чтобы он скорее начал контактировать с сайтом. Все, что нам не нужно на первом экране, мы относим в футер и не загружаем прямо со старта. Основные ресурсы — это CSS и JavaScript.

Google Page Speed Insights рекомендует сделать следующие шаги

Для CSS нам нужно включить critical css.

php bin/magento config:set dev/css/use_css_critical_path 1 --lock-config

Есть тулза, которая позволяет сгенерировать critical CSS, а также определить какой из CSS является critical, а какой нет. Помогает выделить его в отдельных файлах, а также есть npm пакет для этого.

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

php bin/magento config:set dev/js/move_script_to_bottom 1 --lock-config

Как выяснилось, рекомендации довольно спорные.

Применение этих рекомендаций незначительный прирост показателя First Contentful Paint но при этом значительно ухудшает Cumulative Layout Shift. И поэтому если измерить производительность ДО и ПОСЛЕ этих настроек, то в итоге мы увидим деградацию производительности

Поэтому по-умолчанию, мы не применяем эти рекомендации.

Keep Request Counts Low

Самый известный момент — это Keep Request Counts Low. Количество запросов с фронта очень большое, их нужно уменьшить, и здесь все довольно просто.

Мы включаем merge CSS. Вместо сотни CSS-файлов подгружается один смердженный.

CSS:

php bin/magento config:set dev/css/merge_css_files 1 --lock-config

Что касается JavaScript, появился Magepack — это advanced bundler, очень хорошо себя зарекомендовал на проектах, и его очень легко настраивать.

JavaScript:

composer require creativestyle/magesuite-magepack
npm install -g magepack

bin/magento config:set dev/js/merge_files 0 --lock-config
bin/magento config:set dev/js/enable_magepack_js_bundling 1 --lock-config

magepack generate --cms-url="{HOMEPAGE_URL}" --category-url="{PLP_URL}" --product-url="{PDP_URL}"
magepack bundle

Кстати, здесь нам все же понадобится Terser, потому что если мы используем bundler Magepack, то нам нужно сначала сгенерировать статику, а потом, на основании определенного конфига, мы из этой статики собираем бандлы. И только после этого уже минифицируем бандлы. А если мы включаем Magento минификацию, то таким образом заставляем Magepack работать с минифицированными файлами.

Избегать чрезмерного размера DOM

Если у вас на странице больше 3000 узлов, то обязательно Lighthouse будет ругаться на это. Это невозможно пофиксить легко, нет какого-то общего правила, как это сделать.

Есть несколько косвенно помогающих способов.

  1. Отключить Page Builder. Отключение его естественно может снизить количество узлов. Но Page Builder — это очень важный елемент работы интернет-магазина, поэтому такой шаг нужно обязательно обсудить с клиентом. Возможно, он оплатил лицензию за Magento только ради того, чтобы здесь был Page Builder или нанял контент-менеджера без знания HTML, только потому что есть Page Builder. Если таких жестких требований нет, то ради того, чтобы DOM size уменьшился, можно попробовать отказаться от использования Page Builder.
  2. Загружать некоторые части контента через Ajax. Например, у вас в магазине 1000 категорий, и они все попали в меню, вы их отрисовали, и внутри у вас в <ul> и 1000 <li>, а еще в каждой лишке, например, <span> или другие элементы.

Вы можете оставить только, верхнеуровневое меню, а нижние уровни уже подтягивать при помощи Ajax.

Показывать изображения в next-gen форматах

Появился новый формат картинок WebP, который по качеству не уступает JPEG и при этом занимает меньше места. Вы можете напрямую установить с маркетплейса какой-нибудь модуль, который будет конвертировать на лету.

Есть вариант использования ресурсов различных CDN, таких как Fastly, Cloudflare. Они умеют конвертировать ваши картинки в WebP формат, и работают даже лучше, чем модули. CDN сам определяет, поддерживает ли браузер WebP формат (браузеры передают соответствующие заголовки), и если да, то на лету конвертирует JPEG в WebP.

Serve static resources с помощью эффективной политики кэширования

Здесь все хорошо по умолчанию, потому что дефолтные настройки nginx уже настроил правильный cache policy для всех типов ресурсов, которые отдаются. Единственный момент — если мы начинаем использовать в WebP nginx config. Magento не знает про WebP, и вам нужно будет в соответствующую секцию это дописать.

locator /media/{
     try_files  $uri  $uri/ /get.php?$args;
location ~ ^/media/theme_customization/.*\.xml {
      deny all;
}
location ~* \. (ico|jpg|jpeg|png|gif|svg|js|css|swf|eot|ttf|otf|woff|woff2|webp)$  {
           add_header  Cache-Control “public”;
           add_header X-Frame-Options “SAMEORIGIN” ;
          expires +1y;
          try_files $uri $uri/ /get.php?$args;
}
location ~* \. (zip|gz|gzip|bz2|csv|xml)$ {
         add_header Cache-Control “no-store”;
         add_header X-Frame-Options “SAMEORIGIN” ;
         expires         offf:
         try_files  $uri $uri/ /get.php?$args;
}
add_header X-Frame-Options “SAMEORIGIN” ;
}

Убрать неиспользуемый CSS

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

Но с выключенным Merge css показатели ниже, поэтому, как и с render blocking resources, игнорируем эту рекомендацию

Включить сжатие текста

Желательно, чтобы сервер отдавал скрипты и все текстовые файлы в сжатом виде (gzip). Здесь у нас все хорошо — nginx config по умолчанию их гзипует.

Есть еще небольшая микронастройка — включить минификацию html-файлов. Эта настройка уберёт все лишние пробелы и переносы. Конечно, вы в этом случае очень много не сэкономите, но тем не менее лишний байт сэкономим:

php bin/magento config:set dev/template/minify_html 1 --lock-config

Эффективное кодирование изображений

Если у CDN есть функция оптимизации картинки, и вы загружаете ее с хорошим качеством, то он их выжимает до веб качества и отдает в браузер минимум контента. Также есть куча extensions, которые тоже самое делают — мы рекомендуем Apptrian. Мы его часто используем, и у нас не возникает с ним никаких проблем.

Core Web Vitals

Google Page Speed Insights измеряет метрики в двух концептуально разных условиях.

Lab Data — замеры производятся в лабораторных условиях. То есть на каком-то стандартизованном железе, с вполне конкретными характеристиками, с установленной последней версией браузера, который конечно же Google Chrome. На основании этих метрик вам и даются рекомендации по улучшению производительности.

Field Data — практически те же параметры, но которые замеряются на девайсах конкретных пользователей.

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

Например, если большинство кастомеров используют сайт через браузер Internet Explorer (потому что это какие-нибудь гос учреждения, где может использоваться устаревшее ПО), то они могут и не заметить, что вы добавили картинкам атрибут loading=lazy, потому что данный браузер попросту не поддерживает этот атрибут.

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

Что дальше

Это неполный список рекомендаций. Некоторые мы все еще не проверили на собственном опыте, и они находятся у нас в беклоге в ТODO. Но эффект от их исправлений не должен быть большим, потому что как правило Google Page Speed Insights не ругается на них, когда мы имеем дело с мадженто.

Также есть некоторые показатели (например, уменьшить время выполнения JavaScript) которые нельзя улучшить «на скорую руку» и придется хорошенько погрузиться в проект чтобы понять, как можно это исправить.

На данный момент TODO список выглядит так

Вывод

Итак, мы рассмотрели несколько простых и надежных способов улучшить производительность сайта, при чем таким образом, чтобы это еще и «понравилось» Google Page Speed Insights.

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

Примечание: На самом деле Google Page Speed Insights — это всего лишь web service. А набор метрик — это другая тулза, которая называется Lighthouse, для простоты. Google Page Speed Insights просто используют эту тулзу. Но для простоты в статье используется только название Google Page Speed Insights, даже когда речь больше идет про Lighthouse

Сподобалась стаття? Натискай «Подобається» внизу. Це допоможе автору виграти подарунок у програмі #ПишуНаDOU

👍ПодобаєтьсяСподобалось9
До обраногоВ обраному1
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
Самый известный момент — это Keep Request Counts Low. Количество запросов с фронта очень большое, их нужно уменьшить, и здесь все довольно просто.

Там нє low, а під кожен тип зарезервовані треди на _одне_ доменне ім’я, приблизно такі:
2-5 стилі, 1-2 на жс, і до 5 паралельних на зображення
stackoverflow.com/a/30064610/1346222
taligarsiel.com/...​ects/howbrowserswork1.htm
Це для хттп1.1

новый формат картинок WebP, который по качеству не уступает JPEG

Фігасє, новий
І він більше аналог png, яким в вебі мало користуються
А по якості він краще жпег

Serve static resources с помощью эффективной политики кэширования

І на який біс магенті треба знати про статику?

Если у CDN есть функция оптимизации картинки, и вы загружаете ее с хорошим качеством, то он их выжимает до веб качества и отдает в браузер минимум контента

developer.mozilla.org/...​/Web/HTML/Element/picture
Це така ж «нова фіча», як і webp

Про тупе кешування згенерованої сторінки нічого не сказали
Про підхід ручного випилу стилів та/чи підходу робити міксини на sass

Про тупе кешування згенерованої сторінки нічого не сказали

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

Те що одні вважають очевидним — інші можуть не знати

Якщо на жс не робити анімації і тримати його простим, то не буде він нічого жерти

Зображення кешуються браузером (якщо не наробити фігні)

cls — Cumulative Layout Shift?

Те що одні вважають очевидним — інші можуть не знати

Холиварный вопрос. Может и нужно «прям всё описывать». (ИМХО: нет)

Якщо на жс не робити анімації і тримати його простим, то не буде він нічого жерти

Категорически не соглашусь. Вот анализ демки Люмы: i.imgur.com/LvAOhl7.png
Процессор тупо занят JS-ом. И это «по дефолту» для Мадженты

всё описывать

Просто зазначити, що це перший пункт в такого роду справах

И это «по дефолту» для Мадженты

¯\_(ツ)_/¯
А це капець

JS бандлинг спорный вопрос. Зависит от проекта. На некоторых без бандлинга показатели выше т.к. поддержка http2 уже есть и на мобилках.

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

Якщо прогнати цей PageSpeed по популярних сайтах, то можна зрозуміти, наскільки «важлива» для них ця метрика.
А от що вона показує добре, так це абсурдність сучасного вебу. Коли найпопулярніші мови використовують JIT+компіляцію, або навіть засовують сам компілятор у файли сайту (!). Так, саме таке я спостерігав у Flutter Web (там взагалі є якась оптимізація для цього?). Ну а підвантажити на всіх сторінках CSS на кількасот Кб — це взагалі вже галузевий стандарт.
Звісно, коли в тебе навіть цикли for повільні (привіт пітон), то хто ж буде думати, що можна автоматично відключати модулі, які не імпортуються, не вантажити стилі, які не використовуються та не віддавати користувачеві мегабайти поліфілів та компіляторів, коли він хоче «замовити піцу».

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