Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Однажды мы заметили, что поисковая выдача в мобильной версии Циан замедлилась: плохо работала на стороне браузера, грузилось много ресурсов, приложение долго открывалось. Естественно, это не радовало пользователей и сказывалось на метриках. В мае 2021-го Google анонсировал изменения: с августа он станет учитывать метрики Core Web Vitals в ранжировании поисковой выдачи. Мы стали искать, в чём может быть наша проблема. В этой статье расскажем, где же проблема крылась, и как мы её решили.
Core Web Vitals (CWV)
Google предложил метрики для определения качества страниц: FID, LCP и CLS. Дословно Core Web Vitals — это основные интернет-показатели. Каждый из этих показателей представляет собой отчётливый аспект пользовательского опыта. На текущий момент основными показателями являются загрузка, интерактивность, визуальная стабильность.
FID — First Input Delay
Время реакции страницы на первое действие пользователя (при загрузке). В нашем случае это время между тем, как пользователь начал листать галерею, и тем, как на странице что-то изменилось (хотя бы появилась крутилка), или тем, как всё пропало с экрана и началась загрузка новой страницы.
LCP — Largest Contentful Paint
Время от запроса пользователя до окончания рендеринга самого большого элемента в видимой части страницы. Да, важно отрисовать именно самый большой элемент, а остальное потихоньку догрузить. Для нас самый большой элемент — это список объектов недвижимости, но в иных случаях им может быть текстовый блок, видео или баннер.
CLS — Cumulative Layout Shift
Совокупное смещение макета. Эта метрика чуть хитрее, поскольку состоит из произведения двух параметров: доли неожиданно смещаемых элементов относительно видимой области и расстояния, на которое элементы сместились. В настоящее время учитываются смещения с 1-й по 5-ю секунду после начала отрисовки. То есть если <div />
схлопнуть после начала 6-й секунды, то внезапно CLS примерно с 0,2499 упадёт до 0, если другие элементы останутся на своих местах. (Подробнее показатель описан в этой статье.)
Самодиагностика
Промышленное решение
У нас хороший CLS, прекрасный FID, и подводил только LCP. Мы знаем это благодаря тому, что у нас есть единый отлаженный механизм мониторинга наших сервисов через Grafana. Но пришлось его научить показывать CWV-метрики. С помощью пакета web-vitals мы стали накапливать гуглометрики с реальных пользователей. Затем настроили удобные графики и начали отслеживать динамику.
Работать с web-vitals оказалось комфортно. Для него не обязательно использовать выделенные хранилища и Grafana (у нас всё это просто уже было настроено и стандартизировано). Метрики можно писать прямо в Google Analytics, там тоже есть графики, разрезы и динамика. Рекомендуем, тем более есть инструкция.
Пока данные накапливались, мы прогнали ключевые страницы через аналитические сервисы. Нам понравились Google Search Console, WebPageTest и Lighthouse. Мы в основном использовали последний, но хороших слов заслуживают все.
Коротко об этом под катом
Google Search Console
Это возможность посмотреть на сайт глазами Google. Данные собираются непосредственно с пользователей Google Chrome. Google Search Console разделяет метрики мобильной и десктопной версий сайта и позволяет просматривать состояние по каждому из основных интернет-показателей постранично.
Для доступа к сервису необходимо подтверждение владения доменом, поэтому посмотреть, как там у других, или оценить локальную версию (до прода) не получится.
WebPageTest
Бесплатный мощный инструмент для анализа производительности веб-страниц. Его основные преимущества:
Тонкая настройка эмуляций, которая позволяет в лабораторных условиях воспроизводить загрузку страниц на разных скоростях.
Внутри используется Lighthouse с открытым кодом для улучшения качества страниц.
Можно делать тесты с разных локаций.
Мультибраузерность.
Бесплатность.
А ещё можно развернуть свой инстанс.
Lighthouse
Инструмент для аудита конкретных страниц:
Анализирует данные из navigator.performance;
Анализирует загрузку ресурсов и ответы API;
Внутри себя имеет набор разных алгоритмов, которые вычисляют метрики вроде TTI;
Формирует человекопонятный отчёт с рекомендациями к работе.
Ситуация нас не удивила:
CLS — прекрасен, у нас очень чёткая сетка и для мобильной, и для адаптивной версии. Впрочем, мы тоже нашли, что улучшить, но об этом дальше.
FID — от «хорошо» до «терпимо», на самом деле этот параметр не так просто сломать.
LCP — будем чинить.
Минутка занимательной арифметики
С 29 марта по 4 апреля 2 471 156 загрузок нашей мобильной поисковой выдачи и карточек объявления заняли как минимум на 1 секунду больше, чем должны были по Core Web Vitals. А это уже 28 суток. Целый февраль!
Итак, мы подтвердили очевидное с цифрами в руках. Теперь пора решать проблему.
Берём Lighthouse, настраиваем тесты, запускаем.
Внезапно наибольшей проблемой оказывается JavaScript.
Проблема оказалась комплексной. Часть была в коде, часть — в окружении.
Проблемы и решения вне кода
CDN
У нас есть CDN — это географически распределённые хранилища. Когда вы открываете сайт Циан, вам в браузер загружается не только базовый HTML, но ещё скрипты и картинки.
Мы обнаружили, что в первый раз CDN работал прекрасно. А во второй раз нашлись проблемы с заголовками кэширования. То есть всё то, что мы выиграли за счёт географии в момент, когда пользователю отправлялись скрипты при первом запросе, превращалось в тыкву при ответе или при повторном запросе. То, что уже лежало на устройстве пользователя, закачивалось ему снова из топологически близкого хранилища.
Починить это оказалось проще, чем обнаружить. Мы с девопсами договорились мониторить проблемы с кэшированием в будущем.
GTM — Google Tag Manager
GTM — это удобный инструмент для подключения сторонних аналитических скриптов. Идея прекрасна: разработчик один раз внедряет GTM на страницу, и всё. Дальше его работа заканчивается, добавить или удалить внешний скрипт может любой пользователь, у которого есть права.
Скрипты для аналитики сейчас бывают весьма продвинутыми. Плюс они стремятся начинать работу как можно раньше, чтобы собрать и передать какие-то данные до того, как пользователь закроет страницу. В итоге до четверти скриптовой нагрузки внезапно пришлось на аналитику.
Нам понадобилось время на то, чтобы аналитики вычистили неиспользуемое и дубли. В дальнейшем мы договорились о процедуре подключения новых инструментов через ревью отдела веб-разработки. Также задумались о том, чтобы сделать A/B-подключение, если скриптов понадобится много. Это поможет сделать так, чтобы на часть пользователей приходилась только часть скриптов.
Проблемы и решения в коде
Наш сайт — полностью динамический. Любая страница собирается React’ом от и до. При этом большая часть страниц отдаётся в основном как HTML. Это старая добрая магия SSR — Server Side Rendering.
В чём плюсы:
поисковые роботы получают привычное им чтиво и прекрасно всё индексируют;
нагрузка ложится приличной частью на наши сервера, а не на устройство пользователя;
пользователь получает не огромный бандл, собранный вебпаком из скриптов и компонентов из веб-приложения, а кусок HTML и небольшой кусок-довесок из кода.
Всё это прекрасно работало. Кроме последнего пункта.
Overсборка
Мы когда-то заморочились со сборками и всё прекрасно отладили. Сборка была компактной, правильно «побитой», лишнего особо ничего не улетало.
А когда взяли webpack-bundle-analyzer, проанализировали текущую нашу сборку и удивились. В сборке оказалось много неиспользуемого.
Мы решили начать с нуля, применив практики по оптимизации и ускорению:
1. Грамотно поделили сервисы на чанки;
2. Настроили хороший tree-shaking с помощью Webpack 5;
3. Использовали Preact в режиме compat;
4. Применили content-visibility для изображений;
5. Заинлайнили критический CSS;
6. Убрали неиспользуемые зависимости.
Подробности расскажем ниже.
Артефакты прошлых лет
На заре разработки микросервисов мы использовали глобальные CSS-файлы и Sentry для мониторинга. Со временем мы от этого отошли, но сами файлы так и продолжили подключаться на всех страницах, хоть и использовались лишь в паре старых сервисов:
common.css — базовые стили, reset.css и пр.;
grid.css — старая сетка сайта;
sentry.js — инструмент для сбора всех клиентских ошибок. Со временем нам стало очевидно, что мы не хотим собирать всё подряд, и на смену этой библиотеке пришла наша, более легковесная и простая, но мы не убрали эту.
Что мы сделали:
grid.css унесли в единственные её использующие 2 старых сервиса;
common.css перенесли с CDN прямо в код HTML;
sentry.js просто удалили.
Inline CSS
Чуть-чуть остановимся на прямом включении CSS. Это хорошее решение для улучшения CLS. Если происходит что-то на линии и внезапно лаги нападают именно на CSS-файл (особенно когда все стили проекта и сторонних библиотек добросовестно собраны вебпаком в один красивый большой файл), а скрипты и сама страница подгрузились, то браузер пытается всё отрисовать. Потом стили долетают, и всё начинает судорожно перестраиваться.
Поэтому выносить компактный набор стилей, управляющий в первую очередь макетом и основной цветошрифтовой палитрой, оказалось хорошей идеей.
Время менять шапки
В Циан мы используем микросервисную архитектуру. Обычно в формировании страницы со стороны фронтенда участвует 3 сервиса: шапка, футер и микросервис конкретной страницы. Это сильно упрощает разработку и ускоряет деплой.
Самым медленным микросервисом оказалась шапка. Проблема была на стороне браузера: много стилей, много JS, крайне неоптимальное чанкование, проблемы с зависимостями и т. д.
Мы поняли очевидное: шапка будет развиваться в плане функционала. Какие бы мы оптимизации ни включили в текущей архитектуре, так мы только отложим проблемы. Поэтому мы решили переписать шапку полностью.
Причины тотального рефакторинга таковы:
архитектура устарела, новые функции требовали слишком много бойлерплейта для реализации;
текущая архитектура не предполагала ленивой загрузки функции;
компонентный набор устарел;
не было возможности кластеризации в части SSR.
Для начала мы отделили мобильную шапку именно как отдельный микросервис. Для адаптивной версии шапка осталась старой, а для мобильной версии мы разработали новую архитектуру.
Основной идеей стала загрузка по требованию. Шапка реализована не микроприложением, а куском HTML с теми функциями, которые нужны здесь и сейчас. Более того, большинство действий, которые можно выполнить с шапки, дозагружаются сразу после клика. Например, можно авторизоваться, кликнув по «Войти». В норме это незаметно, но мы предусмотрели и обработку ситуаций, когда нам не удаётся оперативно дозагрузить (обычно предупреждалка и/или повторная попытка подгрузки).
Теперь шапку необходимо было оттестировать и замерить. После внутренних тестов мы выложили новую шапку в A/B-режиме. Мы внимательно смотрели за логами и ловили обращения в службу поддержки.
У нас изменилась структура деплоя, потому что микросервис шапки стал масштабируемым, и проблемы производительности SSR стало возможно решать «грубой силой», просто добавляя мощности.
И вскоре на новую шапку переехала и адаптивная (десктопная) версия.
Content-visibility
Наша выдача состоит из 28 объявлений, и обычно в каждом объявлении есть как минимум 3 фотографии. Мы использовали стандартную ленивую загрузку изображений при пролистывании галереи, но не учли, что при загрузке страницы нам не нужны изображения объявлений, которые находятся за начальной областью видимости.
Мы начали использовать CSS-свойство content-visibility
. Браузеры умные: они начинают загружать изображения заранее, при «приближении» к ним, а не только когда уже виден серый прямоугольник. Мы использовали это свойство только и именно для картинок, что снизило время браузера на рендеринг страницы чуть ли не вполовину. Обычно пользователи приходят на выдачу с заранее накрученными параметрами: цена, комиссии и прочее их уже устраивают. И смотрят они именно на картинки.
Попутные находки
Мы используем React в разработке, а для прода — Preact в режиме Compact. Поскольку Preact на 99 % совместим, то всё взлетело с первого раза и дало как увеличение скорости рендера, так и уменьшение бандлов.
Иконки были сделаны универсально, как SVG. Неважно, являлись ли они растром PNG или вектором. Добрый webpack, видя PNG’шку в SVG’шке, преобразует файл картинки в Base64 и подключает его в бандл. Пришлось это почистить.
Подводим итоги
Что у нас получилось со страницей поисковой выдачи на мобилке:
Было | Стало | |
Размер JS-бандла | 500 kB gzipped | 100 kB gzipped |
FCP | 4.3 s | 2.8 s |
LCP | 5.0 s | 3.8 s |
CLS | 0.002 | 0.002 |
TTI | 9.1 s | 5.7 s |
TBT | 570 ms | 490 ms |
А вот наши итоги в целом:
Нам удалось войти в зелёные зоны по всем метрикам.
Мы получили улучшенный, расширяемый и масштабируемый код.
Мы осознали, насколько важно взаимодействовать не только с пользователями, но и с другими подразделениями Циан, и разработали каналы коммуникации и обратной связи.
Мы поняли необходимость и методы ранней диагностики проблем, в том числе с использованием метрик CWV.
Мы сделали наших пользователей чуть-чуть счастливее.