Блокнот · Заметка 001 · Скорость сайта

Где на статическом сайте прятались последние 18 баллов

Изображения сжаты. JavaScript занимает три файла. Фреймворков нет. Но Lighthouse всё равно показывал 82. Остальные баллы прятались в трёх местах, которые аудит назвал, но не объяснил.

Serhat Belen

Основатель BLN Global · · Читать 7 мин

Короткий ответ

Изображения и JavaScript уже были в порядке. Баллы терялись в трёх местах. Первая причина: проверка, которая должна была отсекать лишнюю работу, сама считывала разметку и заставляла браузер пересчитывать её. Вторая: файл стилей и внешние шрифты задерживали первую отрисовку на 600 ms. Мы встроили CSS в страницу, а шрифты перенесли на свой сервер. Третья: 6 из 24 предупреждений о контрасте оказались ложными. Полное сканирование затем нашло ещё 13 страниц, не попавших в выборку.

На сайте нет фреймворка. Python-скрипты генерируют обычный HTML, стили лежат в одном CSS-файле, JavaScript разбит на три небольших файла. Изображения хранятся в WebP и трёх размерах. Всё очевидное для быстрой загрузки мы уже сделали.

Мобильный балл застыл на 82.

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

Первое место: сама проверка, написанная, чтобы пропустить работу

Предупреждение Lighthouse «принудительная перекомпоновка» (forced reflow) означает, что JavaScript заставляет браузер раньше времени пересчитать макет страницы. Обычно браузер накапливает изменения стилей и применяет их разом. Если между ними код запрашивает размеры, браузеру приходится сразу выполнить весь накопившийся пересчёт.

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

  • В первом заходе getComputedStyle вызовы убрали. Балл не изменился.
  • На втором круге показался настоящий виновник: код, написанный, чтобы пропускать лишнюю работу, if (window.scrollY > 0) проверка. Она сама читала параметры макета. Строка, которая должна была пропустить работу, запускала именно её.
  • На третьем проходе мы удалили функцию целиком. Начальное состояние уже задавал CSS. JavaScript не требовалось собирать его заново.

Остался ещё один шаблон void element.offsetWidth было. Это известный приём, который намеренно запускает перерасчёт макета, чтобы перезапустить переход. Вместо него два последовательных requestAnimationFrame поставили. Работает так же и не ломает сетку.

function yenidenBaslat(fn) { /* Если вкладка открыта в фоне, rAF не запустится и демо останется пустым. */ if (document.hidden) { fn(); return; } requestAnimationFrame(function () { requestAnimationFrame(fn); }); }
Перезапускаем переход без принудительного перерасчёта макета. Для фоновой вкладки нужен запасной сценарий: там rAF вообще не срабатывает.

Вывод такой: предупреждение о производительности чаще говорит не «ты делаешь слишком много», а «ты делаешь это не вовремя».

Второе место: 600 ms до первой отрисовки

Второе предупреждение касалось ресурсов, блокирующих первую отрисовку. При открытии страницы браузер сначала загружал и разбирал стили, затем шрифты. До этого экран оставался пустым. Задержка составила 600 миллисекунд.

Файл стилей встроили в страницу

Отдельный CSS-файл на втором визите приходит из кеша. Это плюс. Но на первом визите он добавляет лишний запрос туда-обратно, а первый визит как раз видит человек из поиска. При публикации мы минифицируем CSS и встраиваем в страницу. Проверили: после Brotli встроенный вариант оказался меньше, чем отдельный файл вместе со вторым запросом.

Шрифты перенесли на свой сервер

Установка из Google Fonts цеплялась к двум разным источникам: стиль шел с fonts.googleapis.com, для файлов fonts.gstatic.com. Для каждого нового источника браузеру нужны DNS-запрос, TLS-рукопожатие и новое соединение. Если убрать его, замеры покажут:

250 KB165 KB

Шрифтовая загрузка турецкой страницы. Вместо 6 файлов с 2 внешних источников она получает 3 файла с одного.

Как мерили CSS Google Fonts загрузили с настоящим идентификатором Chrome, unicode-range По полям unicode-range отделили подмножества, которые турецкая страница реально скачает (latin + latin-ext), и измерили каждый файл. У другой стороны в этом репозитории assets/fonts реальный вес файлов.

Как уменьшались шрифты и где пришлось заплатить за десять языков, вынесли в отдельную заметку: десять языков, три файла шрифтов.

Третье место: где инструмент ошибся

Проверка доступности выдала 24 предупреждения о контрасте цветов. Если бы мы исправили их вслепую, половина дизайна сломалась бы. 6 предупреждений оказались ложными.

Причина content-visibility:auto Так и было. Эта настройка откладывает обработку блоков за пределами экрана и ускоряет первую отрисовку. То, что нужно. Но расчётный цвет отложенного блока не попадал в инструмент аудита, и тот сообщал: «белое на белом». На экране такой проблемы не было.

Реальные коэффициенты мы по одному измерили в браузере. 18 предупреждений подтвердились, и мы их исправили. В 6 случаях ошибся инструмент. content-visibility убрали только из двух проблемных разделов, в остальных оставили.

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

И рана, которую мы сами открыли

Чтобы верстка не прыгала, всем картинкам width и height Добавили атрибут. Ход верный: браузер резервирует место под картинку ещё до загрузки. Но демо-блоки сломались: часть картинок нарисовалась шириной 58 пикселей и высотой 1500 пикселей.

Причина такая: в HTML width и height атрибуты дают подсказку, а в CSS height заполняют значение. В макете для этих блоков оставили только aspect-ratio задали, height пустовало. Атрибут лег туда и перебил пропорцию. Правка в одну строку:

img{max-width:100%;height:auto;display:block}
Атрибуты остаются на месте и дальше держат сдвиг макета. Высоту возвращает CSS.

Где сканирование правда помогает

Главная набрала 100, и казалось, что все готово. Нет. Мы мерили выборку. У сайта все 150 страниц прогнали по одному через Lighthouse, и вышло вот что: 137 страниц получили 100 по всем четырем разделам, не 13 страниц. Доступность, лучшие практики и SEO на каждой странице получили 100. Просадка была только в производительности.

Одиннадцать из тринадцати страниц были на одном языке: арабском. Минимум: 83.

/ar/safecoast/ 83 /ar/laya/ 84 /ar/localmind/ 84 /ar/adforge/ 85 /ar/garanti/ 85 /ar/gtin/ 86 /ar/visionguard/ 86 /ar/ 89 /ar/growthos/ 97 /ar/dream/ 99 /ar/twin/ 99
Арабские страницы после полного сканирования. Два оставшихся отклонения, /pl/modelmesh/ и /ru/terms.html, получили по 98. Это обычный разброс между замерами.

Lighthouse сам назвал причину: Веб-шрифт загружен → ar.woff2. Арабский шрифт приходил поздно и заново собирал страницу. Метрики запасного арабского шрифта сильно отличались от Noto Sans Arabic, поэтому сдвиг получался большим. Исправили тем же решением, которое уже приняли для Martian Mono: обмен не так optional.

0,2870

CLS на арабских страницах

Как мерили После правки заново замерили 15 арабских страниц: у всех 15 CLS стал нулем. Баллы за производительность выросли с 83-89 до 98-100. 9 из 15 страниц получили ровно 100. Замеряли живой сайт в Lighthouse.

Главный урок здесь не про скорость. Взять выборку и сказать «сайт 100» значит заявить о странице, которую ты не мерил. Поэтому пятнадцать страниц месяцами жили с 83 баллами. Мы туда просто не смотрели.

Предел результата

Оценка получена в лабораторных условиях с заданным устройством и сетью. Телефон и связь реального посетителя могут быть хуже. При повторных замерах одна и та же страница набирает от 98 до 100. Оценка 100 означает не «быстро для всех», а «измеримые помехи устранены». Когда накопятся данные посетителей, ориентируйся на полевые данные Chrome.

Общее у этих трёх правок одно: ни одна не требовала делать больше работы. Мы убрали работу, которая запускалась не вовремя или вообще была лишней.

Частые вопросы

Что Lighthouse называет «принудительной перекомпоновкой»?

JavaScript заставляет браузер раньше времени пересчитывать накопленные изменения стилей. Обычно браузер применяет их разом. Если код запрашивает размеры, очередь приходится обработать немедленно. offsetWidth, getBoundingClientRect, scrollY и getComputedStyle каждое чтение запускает это.

Всегда ли стоит встраивать CSS в страницу?

Нет. Отдельный файл на втором визите приходит из кеша. Это плюс. Встраивание ускоряет первый визит и тормозит повторные. Решай по сайту: если основная доля приходит из поиска, встраивание даёт выигрыш. У нас замерили, выиграли.

Бросать content-visibility?

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

100 баллов действительно означают, что сайт быстрый?

Нет. Это лабораторный замер с фиксированными устройством и сетью. При нескольких проверках подряд одна страница получает от 98 до 100. Реальный результат появится в полевых данных Chrome, но на их сбор нужно время.

Почему нужно измерять все страницы, а не делать выборку?

У нас 13 из 150 страниц вообще не попали в выборку, а 11 были на одном языке. Главная держала 100, арабские страницы застряли на 83. Нельзя спорить о странице, которую ты не мерил.

Источники

Похожие работы