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

В начале — пару слов о себе. Меня зовут Полина и я работаю «Rocket Business» уже несколько лет. Свой путь от джуна до тимлида прошла именно здесь. Поэтому своим мини-пособием решила делиться в корпоративном блоге: оставляю в помощь потомкам и аудитории Хабра. 

Тут — кликабельное оглавление для удобной навигации.

  1. Инструменты проверки.

  2. Кэширование.

  3. Рендеринг.
    3.1. Блокировка рендеринга.

  4. Оптимизация картинок.
    4.1. Сжатие.
    4.2. Конвертация.

  5. Еще немного об оптимизации картинок: повышение LCP.

  6. «Удалите неиспользуемый код JavaScript».

  7. Короткие выводы.

Инструменты проверки

Для начала, наши инструменты проверки.

  1. Google Pagespeed. Все seo-шники и клиенты сверяются по нему. Но довольно часто он врет и крутит баллы как в плюс, так и в минус. Дает список рекомендаций для повышения оценки. Мы стремимся к зеленой зоне – 90 и выше. 

  2. Google Lighthouse. То же самое, что и Pagespeed, но во вкладке Chrome. Врет меньше, часто показывает больше рекомендаций, чем Pagespeed.

  3. Остальные инструменты разработки Google Chrome. В частности вкладка Network – тут обращаем внимание на порядок, объем и waterfall загруженных ресурсов. Также используем вкладку Performance, тут можно детально изучить процесс загрузки страницы, время рендеринга, подключение к ресурсам и тайминги основных метрик Core Web Vitals.

Кэширование

Первым делом стоит изучить вкладку Network. Смотрим, сколько времени отдается сам html-документ и сколько он весит. Возможно, есть проблемы с кэшированием.

Например, тут все очень плохо:

Сам Pagespeed тоже это не пропустит:

Настройка кэширования сильно зависит от бэка и типа CMS. Это обширная тема, которая тянет на отдельную статью. Маякните в комментариях, если такой материал нужен — выкачу подробный мануал.

Потребовалось пересмотреть настройки кэширования и стало лучше:

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

Кстати, 301-й ответ сервера тоже замедляет страницу на несколько сотен миллисекунд. По возможности нужно держать актуальные ссылки на страницах.

С ответом сервера разобрались, переходим к рендерингу.

Рендеринг

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

  • Важно не раздувать DOM: чем больше в нем элементов и уровень вложенности, тем дольше их будет обрабатывать браузер.

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

  • Лучше, чтобы было как можно меньше js-кода, изменяющего страницу: может упасть показатель CLS. Используем JS для интерактива.

Изучить процесс рендеринга можно на вкладке Performance в Chrome.

Блокировка рендеринга

В Lighthouse и Pagespeed есть рекомендация «Устраните ресурсы, блокирующие отображение».

Несколько рекомендаций:

  1. Есть скрипты в шапке? По возможности переноси все в футер (jquery и подобное, конечно, оставить, иначе js на части страниц упадет). Добавляем async или defer.

  2. В шапке подключаются гугл-шрифты? Подключи их локально, так как на коннект и скачивание их с сервера гугла уходит время. Вот хороший сервис для локализации гугл-шрифтов.

  3. Всем шрифтам (не только гугл) обязательно добавляем font-display: swap в подключении, чтобы текст отображался до загрузки шрифтов (FOIT).

  4. Есть подключения с cdn в шапке? Также, как и с гугл-шрифтами, тянем локально.

  5. Критичный CSS — спорная штука, лучше просто не раздувать стили по сайту.

  6. Если все-таки нужно в начале страницы подключиться к внешнему ресурсу, используй dns-prefetch.

  7. Если есть ресурсы, которые 100% будут на странице и влияют на отображение (например, стили), то можно предзагрузить их, используя prefetch.

В дополнение, важно сокращать количество HTTP-запросов, так как одновременно могут скачиваться только несколько ресурсов, а остальные становятся в очередь. Поэтому настраиваем объединение и сжатие стилей и скриптов. В WP подключаем все в functions.php и ставим этот плагин. В Битриксе нужно правильно подключать внешние ресурсы и поставить галочку в настройках главного модуля. Для MODX есть MinifyX.

Сжимать вручную не рекомендуется, потому что такой файл становится неподдерживаемой обузой для сайта и будет только раздуваться с доработками. Еще можно поработать с иконками – объединить их в svg-спрайт, а иконки из шапки (по крайней мере на мобилке) лучше вставить как inline-svg. Так пользователям не придется ждать, пока загрузится файл svg-спрайта.

Для поиска лишних запросов прекрасно подходит вкладка «network» в Chrome. Также можно попробовать использовать cdn для всех ресурсов, например, на WP есть масса плагинов.

До:

После:

Оптимизация картинок 

Что еще тормозит загрузку? Очередь из картинок, которых даже не видно, поэтому обязательно используем lazyload. Другие требования к картинкам на сайте: несколько вариантов изображения (для мобилки и десктопа), поддерживается современный формат и заданы width и height.

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

Сжатие

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

find /dir/www/site.ru -name "*.jpg" -print0 | xargs -0 jpegoptim --all-progressive --strip-all -ft -m85

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

С png все сложнее. Есть готовый скрипт, но он занимает очень много времени, поэтому лучше минимизировать количество png на сайте.

Скрипт для оптимизации:

find /dir/www/site.ru -name "*.png" -print0 | xargs -0 optipng -o7 -strip all -force

Для работы необходим optipng

Конвертация

По требованиям Google все картинки нужно переводить в webp. В целом требование обоснованное, так как в этом формате они весят сильно меньше. На нашей практике, самый удобный способ внедрить webp – через отдачу на сервере. Как это работает? 

  1. Браузер запрашивает у сервера image.jpg.

  2. Сервер смотрит, поддерживает ли браузер webp.

  3. Сервер смотрит, есть ли у него image.webp.

  4. Если все есть, то отдает image.webp вместо image.jpg с необходимым заголовком.

Почему такой способ? Он убирает огромные конструкции из <picture> и @support()позволяет автоматизировать переход на webp.

Как сгенерировать webp-копии всех картинок?

Создаем в корне сайта файл webp-convert.sh. Также для работы нам понадобится установленный cwebp на сервере.

Внутрь вставляем:

#!/bin/bash

# converting JPEG images
find $1 -type f -and \( -iname "*.jpg" -o -iname "*.jpeg" \) \
-exec bash -c '
webp_path=$(sed 's/\.[^.]*$/.webp/' <<< "$0");
if [ ! -f "$webp_path" ]; then
  cwebp -quiet -q 90 "$0" -o "$webp_path";
fi;' {} \;

# converting PNG images
find $1 -type f -and -iname "*.png" \
-exec bash -c '
webp_path=$(sed 's/\.[^.]*$/.webp/' <<< "$0");
if [ ! -f "$webp_path" ]; then
  cwebp -quiet -lossless "$0" -o "$webp_path";
fi;' {} \;

Делаем файл исполняемым, в консоли:

chmod a+x ./webp-convert.sh

А далее запускаем скрипт. Время выполнения зависит от размера сайта.

./webp-convert.sh ./

Для отдачи  webp копий добавляем (для апача) в .htaccess:

<ifModule mod_rewrite.c>
  RewriteEngine On
  RewriteCond %{HTTP_ACCEPT} image/webp
  RewriteCond %{REQUEST_URI}  (?i)(.*)(\.jpe?g|\.png)$
  RewriteCond %{DOCUMENT_ROOT}%1.webp -f
  RewriteRule (?i)(.*)(\.jpe?g|\.png)$ %1\.webp [L,T=image/webp,R]
</IfModule>

<IfModule mod_headers.c>
  Header append Vary Accept env=REDIRECT_accept
</IfModule>

AddType image/webp .webp

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

Для nginx отлично описано тут.

Также на некоторых хостингах это включается одной кнопкой, например, на Reddock. 

Конечно, такой грубый способ не всегда подходит, можно воспользоваться готовыми модулями. Для WP есть масса плагинов, например этот. Вот вариант для Битрикса.

Еще немного об оптимизации картинок: повышение LCP

Если на странице есть баннер в начале, то его обязательно нужно закинуть в <head> в preload. Таким же образом можно подкинуть стили, но лучше не перебарщивать:

<link rel="preload" href="/image.jpg" as="image">

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

Немного вариаций:

Битрикс

В header.php:

<?$APPLICATION->ShowViewContent('PRELOAD');?>

В шаблоне компонента (для страниц, созданных комплексным компонентом) или на индексной странице (для простых страниц):

$image = /* путь до картинки, из arResult или просто строкой */;
$APPLICATION->AddViewContent("PRELOAD", "<link rel='preload' as='image' href='".$image."'>");

Wordpress

Например, тянем из картинки страницы.

В functions.php:

function set_preload() {
    if (has_post_thumbnail() ) {
       $image = get_the_post_thumbnail_url();
        return '<link rel="preload" href="'.$image.'>" as="image">'; 
    }
}

В header.php, в <head>:

<?=set_preload(); ?>

Если все правильно настроено, картинка из preload начинает грузиться сразу после документа:

А в Pagespeed картина такая:

«Удалите неиспользуемый код JavaScript»

Pagespeed ругается на раздутый js во внешних скриптах. Такие ресурсы можно отложить, но аккуратно.

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

   <script>
		function loadScripts() {
			var script = document.createElement('script');
			script.src = 'https://www.google.com/recaptcha/api.js?render=токен';
			script.async = true;
			document.body.appendChild(script);

			document.removeEventListener("scroll",loadScripts);
			document.removeEventListener("click",loadScripts);
		}
		document.addEventListener("scroll", loadScripts);
		document.addEventListener("click", loadScripts);
</script>

Кстати, гугловскую капчу можно заменить на cloudflare turnstile, она не так требовательна к ресурсам.

По поводу отложенной загрузки метрик или тег-менеджера лучше консультироваться с seo-шником. У него же нужно запросить актуальный список используемых инструментов аналитики, так как на сайт может грузится что-то неиспользуемое, например, facebook pixel.

Подведем короткие итоги

Резюмируем. Чтобы получить быстрый и работающий сайт, нужно:

  1. Стили объединить;

  2. Скрипты тоже объединить, перенести в футер;

  3. Картинки сжать, конвертировать и отложить, svg объединить в спрайт;

  4. Баннер в начале страницы — в preload, иконки из шапки — встроить;

  5. Кэширование на сервере настроить;

  6. Тяжелые внешние скрипты отключить или отложить.

В результате всех манипуляций мы можем получить что-то такое:

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

Комментарии (18)


  1. VladimirFarshatov
    21.12.2023 16:46

    И в карму и статье. Браво! погромистам Сбера и всех остальных "ведущих" площадок - изучать наизусть! :)

    К вам вопрос: что Вы им посоветуете (да вот даже Хабру) если страница тянет на 100+ запросов дозагрузки и весит 10+ метров? А то .. легко оптимизировать то, что весит 40килобайт.. там и так нет ничего..

    Кстати, вопрос №2: как быть, если на страницу одновременно тянется тайпскрипт, вуй и ангуляр с реактом? ;)

    ПС. вот выдача рыжелиса по этой страничке после написания основной части коммента. Да, стало заметно лучше, но .. как думаете, что тут можно оптимизировать ещё? :)


    1. bert2000
      21.12.2023 16:46

      А чёт тут изучать то собственно? Лид учит джунов? Дожили. Ну пусть так, ладно.

      Если осмыслить суть статьи, то тут банально везде "рекомендуется" применять паттерн Lazy Load. Это классика проектирования вообще-то. Хочешь чтоб быстро грузилось? Грузи только то что действительно нужно! Чаще всего это некий "общий скелет" (структура) и "первое мясо" (первый блок наполнения).
      Всё "большое" очень и очень плохо! Всегда держи всё мелкими нарезками и подгружай лениво по необходимости. Это придумано ещё в прошлом веке. Этим помнится гордо били себя в грудь противника МСа 20 лет назад, гордо называя это AJAXом. Хотя МС ещё в 30 лет назад делал тоже самое, но под другим соусом. Паттерн ленивой загрузки известен уже полвека.

      Ну для джунов я думаю статья "очень даже". Заодно есть повод изучить паттерны.

      Всех с наступающим! )


    1. pserebryakova Автор
      21.12.2023 16:46

      Спасибо за обратную связь :)
      По вашему вопросу: каждый сайт как правило уникальный, поэтому тут нет универсальной формулы. Как подметил @bert2000 , в статье я описала паттерн Lazy Load, можно следовать его принципам при оптимизации. На Хабре, например, ситуация не такая и плохая, просто немного раздутый js и внешние скрипты - можно поработать с этой частью.
      По второму вопросу: а для чего это все на одной странице? В таком случае лучшая оптимизация - переписать фронт!)


      1. bert2000
        21.12.2023 16:46

        Я, как "революционер" (и профессионал) конечно согласен с Вашим тезисом "лучшая оптимизация - переписать фронт!" (если это не шутка). Но Вас "за такое" могут и "к стенке поставить" же! ))) Слишком радикально и "дорого" (с нуля считай всё писать). Но по-сути верно. Применение паттерна ленивой загрузки можно автоматизировать. И да, это давно есть в виде компоновки (сборки) страниц, т.е. их генерации движками.
        Такой же подход я использую и в базах данных , точно с таким же гарантированным результатом охрененного "повышения" производительности. Почему в кавычках написал? Да потому что изначально пишу оптимальный код и проекты, которые из коробки не нуждаются в оптимизациях и повышении производительности. Спагетти код действительно проще выбросить и на его месте написать нормальный. К сожалению столь "радикальные" для всех и вполне привычные для меня меры, часто просто всех пугают и всё. В общем и Вам не советую! ))))

        С днём энергетика всех причастных.


        1. pserebryakova Автор
          21.12.2023 16:46

          Да, радикально и "дорого", но очень эффективно, плюс помимо оптимизации клиент получает современный дизайн и, как правило, лучшее ранжирование поисковиками


        1. melt
          21.12.2023 16:46

          Такой же подход я использую и в базах данных

          Что вы имеете ввиду, можете поподробнее?


          1. bert2000
            21.12.2023 16:46

            А что там подробнее? Такая же ленивая загрузка из БД. Грузишь только то надо и первую страницу (пейджинг на 20-50 максимум 100 записей). Большие поля грузятся только по требованию, автоматически они вообще не загружаются. А вот системные (ID, метки разные, сессия, линки) грузятся "всегда" (без требований). Всё это базовые функции в моих ORM.

            Т.е. суть та же - "первое мясо" (первая страница) + скелет (ID, метки и связи) загружаются сразу. Остальное потом-лениво (смотря что понадобится).


      1. VladimirFarshatov
        21.12.2023 16:46

        Не фронтендер, но приходилось тоже. Я бы дополнил паттерн ещё и YAGNI - стоит тщательно смотреть на полезность конкретно этой загрузки на конкретно этом этапе показа той или иной страницы. Часто браузеры ограничивают количество одновременных запросов на сайт небольшой чиселкой в 16шт одновременно, остальные да, встают в очередь.

        И таки соглашусь, рефакторинг - перманентное состояние фронтов, т.к. новые дизайны, новые идеи .. всё это надо не только дополнять, но и вдумчиво рефакторить имеющееся.


  1. NickRudoy
    21.12.2023 16:46

    Интересный туториал, было бы интересно узнать материал по кешированию


  1. yantar
    21.12.2023 16:46

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


    1. pserebryakova Автор
      21.12.2023 16:46

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


  1. danidddi
    21.12.2023 16:46

    Туториал???? Спасибо за заботу о джунах)


  1. NotSlow
    21.12.2023 16:46

    Кэширование (про генерацию страниц, а не статику) - это конечно хорошо, но нельзя забывать что кэш не вечен. И если некэшированная страница генерируется 5с, а после кэша 0.1с,то очень рано считать работу законченной...

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

    А еще розовые мечты оптимизаторов про кэширование могут легко разбиться о хотелки владельца сайта :)

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

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

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

    И в таком духе :(


    1. ionicman
      21.12.2023 16:46

      И он абсолютно прав - если проблему можно решить просто отмасштабировавшись - это не проблема. Он смотрит с тз бизнеса, вы - как технарь. Об этом стоит помнить, принимая решения об оптимизации, в тч кэшировании.

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


    1. Shado_vi
      21.12.2023 16:46

      вы что то знаете о разных уровнях/видах кэширования?
      например приведённых в 11 видов кэширования для современного сайта / Хабр (habr.com)


    1. pserebryakova Автор
      21.12.2023 16:46

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


    1. VladimirFarshatov
      21.12.2023 16:46

      Если страница генерируется 5 секунд на сервере, то что-то не так с кодовой базой бэка. Основная задержка генерации часто - выборка данных из БД, а не их пред- или пост- обработка. Современные БД при правильной архитектуре на современном сервере .. получить время отклика в секундах даже на 3-4 миллиардных данных, кмк, проблематично.

      Скорее всего, проблема в правильном индексировании и тюнинге как архитектуры БД, запросов, так и самого сервера СУРБД или NoSQL, просто как пример:

      Hidden text

      Очень часто видел на беках применение разных фреймворков с автопостроителями запросов, где предварительно вытаскивается 100500 записей с идентами сущностей по условиям фильтрации, а потом с дополнительным фильтром в цикле запрашивает 2000 объектов из БД для формирования данных к странице поштучными запросами (Active Record во второй части) - никакой сервер не выдерживает сколько нибудь серъезной нагрузки! А ведь решение "на поверхности": достаточно собирать запрос, отдающий сразу нужное количество на страницу. Это редко где превышает сотню объектов, а часто 10-30, ибо "больше не лезет на экран". Но! Такой рефакторинг требует:

      а) построения сложного запроса к БД, зачастую с позиционированием выдачи этого десятка, которая часто не тривиальна;

      б) наличия спеца по (No)Sql, который напишет рабочий запрос в разумное время и сможет гарантировать его правильную работу для всех кейсов фильтра. Фильтр может оказаться сильно большим.

      Как пример, параметрическая выдача набора товарных позиций (100шт, со второй страницы) по фильтру из десятка свойств товара на базе в миллион товаров в табличке и сервере Мускл 5.1 на 4-х ядерном ксеоне 3Ггц и 16Гб ОЗУ (больше не влезало) отдавал результат за 0.15сек, с дисковой подсистемой всего в 160мб/сек и гарантированным не впихиванием в память ни кеша ни тем более таблиц БД. При этом, на популярных позициях (кеширование индексов InnoDB - тюнингом) попадания в кеш наблюдались примерно в 95% случаев... 2011год. Выдача картинок к товарам - статикой nginx.

      Но .. примерно подобная задача и "микросервисная архитектура" со всего тремя "микросервисами" уже на существенно усиленном сервере выполняется на порядок медленее, т.к. один микросервис делает модно-молодежную аутентификацию запроса (а вдруг кто пробрался внутрь системы? мелочь, но есть), второй выбирает из БД набор данных (часть) а третий добирает остатки из другой БД ибо "требуется рефакторинг, но пока никак" .. таймаут промеж микросервисов и 2 загрузки .. 1.5секунды "в кармане". :(


      1. pserebryakova Автор
        21.12.2023 16:46

        Так и есть, проект из примера на старом Битриксе с раздутой БД, поэтому описанный мною вариант кеширования - просто временное решение для легаси-проекта, где на рефакторинг уйдет слишком много времени...