🔥

Тред (@ierhyna)


Привет! Сегодня поговорим про оптимизации, что влияет на метрики, расскажу про best practices и случаи из жизни :)

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

Про бандлы и доставку:

Кажется очевидным, но почему-то часто об этом забывают: - включите сжатие gzip/brotli для ресурсов - используйте в вебпаке хеш от контента в имени файла, и включите кеширование на подольше (у нас на месяц)

Тоже очевидно, но всё же, убедитесь, что вебпак собирает бандлы в продакшен-режиме с включенной опцией minimize:
notion image

Следующим шагом можно настроить в вебпаке разбивку на чанки. Чтобы это сделать, неплохо для начала проанализировать, из чего состоит итоговый бандл. Например, с помощью webpack-bundle-analyzer npmjs.com/package/webpac…

Распространён подход, когда в один чанк (или несколько) выносят зависимости node_modules, а в другой код приложения. Это кажется не очень оптимальным, поскольку при таком подходе при первой загрузке приложения пользователь всё равно скачает всё.

Лучше, изучив состав бандла через webpack-bundle-analyzer, выделить в отдельный чанк общие зависимости для всех роутов приложения (например, реакт, роутер, redux, и UI-kit), а зависимости, используемые в 1-2 страницах, включить в состав чанка этой страницы

Для генерации отдельного чанка для каждого роута отлично подходит React.lazy() в сочетании с react-router ru.reactjs.org/docs/code-spli…

Дать файлам чанков человеко-читаемые имена поможет волшебный комментарий для вебпака:
notion image

Хорошим способом сократить объем кода, скачиваемого при первой загрузке React приложения, будет импортировать с lazy() все компоненты, с которыми пользователь не взаимодействует сразу: модалки, скрытые компоненты, и подобное.

Некоторые npm пакеты могут существенно увеличить объем бандла кодом, который никогда не будет использован. Например, moment.js по-умолчанию включает все возможные локали, а lodash — все хелперы.

Исключить лишние локали из moment.js можно через moment-locales-webpack-plugin, github.com/iamakulov/mome…, IgnorePlugin или ContextReplacementPlugin github.com/jmblog/how-to-…

Про загрузку только необходимых функций lodash, можно почитать тут: azavea.com/blog/2019/03/0…

Про загрузку, отрисовку и взаимодейстивие со страницей продолжу завтра

Про загрузку и отрисовку страницы:

Чтобы разобраться в том, как оптимизировать отрисовку, нужно понимать, как браузер рендерит страницу. Пара ссылок: 1)developers.google.com/web/fundamenta…, 2)medium.com/jspoint/how-th…

Блокируют отрисовку страницы: - Стили, подключенные без атрибута media, - стили, подключенные с атрибутом media, который совпадает с текущими параметрами устройства, - скрипты без атрибута async/defer. Стили и скрипты будут загружены, даже если они не блокируют отрисовку.

Чтобы немного упростить браузеру задачу отрисовки: - разделите CSS на файлы так, чтобы один файл — один media query (плагин: npmjs.com/package/postcs…), - подключайте скрипты с async/defer, и проверяйте в них событие DOMContentLoaded

Пользователь, заходя на страницу, видит только какую-то её часть. Важно сделать так, чтобы эта видимая часть отобразилась как можно быстрее. Для этого можно выделить стили, необходимые для отрисовки первого экрана, и заинлайнить их. Плагин для вебпака: github.com/GoogleChromeLa…

Применение critical css позитивно скажется на метриках, например, Largest contentful paint

На контентные метрики (Largest contentful paint / First meaningful paint) влияет и то, как подключаются и используются шрифты. Мы однажды обнаружили, что на одном из сервисов загружался, но не использовался, кастомный шрифт. После его удаления, метрика FMP улучшилась на 76%!
notion image

Ещё немного про шрифты: - формата woff2 достаточно в большинстве случаев (caniuse.com/?search=woff2); - подключайте шрифт с preload; - используйте font-display: swap;
notion image

Про изображения: - указывайте width/height, так браузер заранее зарезервирует место для картинок - для контентных картинок — атрибут loading="lazy" (caniuse.com/loading-lazy-a…) для нативной загрузки по скроллу (полифил: github.com/mfranzke/loadi…)

Для контентных картинок можно использовать тег picture (developer.mozilla.org/en-US/docs/Web…), чтобы, например, загружать более легкие изображения на маленьких экранах. Плагин: responsive-loader (npmjs.com/package/respon…).

Совет от кэпа: убедитесь, что картинки минимизованы и имеют адекватные размеры! Особенно, если картинки могут загружать пользователи. В одном нашем сервисе мы сэкономили 10мб просто на том, что привели изображения к ширине 1024px и качеству 85% ¯_(ツ)_/¯

Кулстори про клиентский и серверный рендеринг. Есть мнение, что рендерить страницу на сервере быстрее, чем на клиенте. Поэтому мы выпилили реакт на одной из статичных страниц, и переписали её на чистом html, который собирается через Django на бэке (Django там уже был) ->

Постепенно к странице появлялись всё новые требования: сначала кастомизации в зависимости от тарифного плана и страны пользователя, а через год — от нескольких десятков параметров. Настройки делали на бэке условиями в темплейте, в итоге time to first byte составил 2 сек

Поддерживать это стало очень сложно, решили снова переписать на реакте. Страница стала отдаваться за 200мс, но выросли TTI и CLS (август-сентябрь на графике):
notion image

Тогда придумали к̶о̶с̶т̶ы̶л̶ь изящное решение: для видимой части сверстали скелетон (штуку как на картинке) и заинлайнили его стили в <style>. Поместили этот код внутри тега, куда рендерится реакт, чтобы он заменялся на реактом на готовую страницу с контентом.
notion image

Если нужно передать через шаблон данные с сервера на клиент (например, initial state для redux store), лучше завернуть их в JSON.parse(), так браузер сможет распарсить их быстрее, и это тоже улучшит TTI: joreteg.com/blog/improving…

Про взаимодействие и UX.

Если использовать скелетон, а не спиннер, то пользователям кажется, что страница загружается быстрее: uxdesign.cc/what-you-shoul…
notion image

Полезно изучить, как пользователи переходят по страницам сайта, и использовать это знание для предварительной загрузки ресурсов. Допустим, с главной 80% идут на регистрацию, тогда можно заранее загрузить ресурсы и в фоне отрендерить эту страницу. caniuse.com/link-rel-prere…
notion image

С rel="prerender" нужно работать осторожно, так как это ускоряет следующую страницу для части пользователей за счёт загрузки доп ресурсов на текущей для всех. Не рекомендуется делать более одной ссылки с rel="prerender" на странице.

Используюя rel="preload" можно скачать и закешировать ресурсы, которые скоро понадобятся. В отличие от rel="prerender", они не будут исполнены (только скачаются). caniuse.com/link-rel-prelo…
notion image

Guess.js — экспериментальная библиотека, которая на основе данных гугл-аналитики предсказывает, на какую страницу дальше пойдёт пользователь, и динамически делает prerender/preload. github.com/guess-js/guess

Для Webpack есть preload-webpack-plugin, он решает проблему добавления ссылок на ресурсы с хешем в имени: npmjs.com/package/@vue/p…

Совсем немного про отзывчивость интерфейса:

Если в течение ~100мс после действия (клик по кнопке, нажатие на меню, фокусировка в инпуте и начало ввода текста и тд) пользователь не получил визуального подтверждения, ему кажется, что интерфейс тормозит

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

Есть паттерн Optimistic UI, согласно которому предполагается, что операция завершилась успехом, и показывает результат сразу по взаимодействию, ещё до того, как выполнен запрос/пришли данные. uxplanet.org/optimistic-100…

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

Как реализовать Optimistic UI с Apollo GraphQL: apollographql.com/docs/react/per…

Как реализовать Optimistic UI с React Query: react-query.tanstack.com/guides/optimis…