Веб-версия PageSpeed показывает оценку и подсказки, но не отвечает на главный вопрос: что именно страница успела запросить до первой отрисовки. Этот ответ есть в полном отчёте Lighthouse — того же движка, на котором работает PageSpeed. Его можно запустить у себя одной командой, получить JSON и разобрать до конкретных файлов.

Это третья часть цикла «PageSpeed без мифов». В первой мы разобрали, что оценка — это модель, во второй — формулу, которой модель проверяется. Теперь — инструмент, который даёт для формулы цифры.

Зачем мерить у себя, а не на сайте PageSpeed

  • Полный отчёт. Список всех запросов со временем старта, наблюдаемые и модельные метрики, фазы LCP — всё в одном JSON.
  • Сколько угодно прогонов. Публичное API PageSpeed быстро упирается в лимит, а выводы, как мы разберём в четвёртой части, делаются только по серии прогонов.
  • Тот же результат. Локальный прогон воспроизводит оценку PageSpeed: на нашем каталоге — 69 против 69, LCP 12,8 против 12,2 с.
  • Можно проверить правку до выката. Сохранить HTML страницы, поправить, отдать локальным сервером и замерить.

Запуск

Нужны Node.js 18+ и установленный Chrome. Lighthouse ставить не обязательно — его подтянет npx:

npx lighthouse@12 https://example.ru/catalog/ \
  --only-categories=performance \
  --chrome-flags="--headless=new" \
  --output=json --output-path=lh.json --quiet

По умолчанию это мобильная проверка с той же моделью, что у PageSpeed: эмулированный телефон, сеть 1,6 Мбит/с, процессор в 4 раза медленнее. Для десктопа добавьте --preset=desktop. Нужен ещё и HTML-отчёт для глаз — --output=json --output=html.

Перед прогоном откройте страницу один раз. Если на сайте кэш страниц, первый заход после сброса отдаётся медленно, и Lighthouse зафиксирует «регрессию», которой нет у живых посетителей.

Что лежит в JSON

ГдеЧто
categories.performance.scoreоценка от 0 до 1
audits.metrics.details.items[0]все метрики сразу: модельные (largestContentfulPaint, speedIndex…) и наблюдаемые (observedFirstContentfulPaint, observedLargestContentfulPaint…)
audits["network-requests"]каждый запрос: адрес, тип, вес, время старта
audits["largest-contentful-paint-element"]какой элемент стал LCP и фазы: ответ сервера, задержка загрузки, загрузка, задержка отрисовки
главные места отчёта

Скрипт: что попало в окно первой отрисовки

Двадцать строк, которые превращают отчёт в список подозреваемых. Сохраните как lh-window.js рядом с отчётом:

// lh-window.js — что ушло в сеть до отрисовки крупнейшего элемента
// запуск: node lh-window.js lh.json
const d = require(require("path").resolve(process.argv[2] || "lh.json"));
const a = d.audits;
const m = a.metrics.details.items[0];
const net = a["network-requests"].details.items;
const lcp = m.observedLargestContentfulPaint;
const kb = b => Math.round((b || 0) / 1024);

console.log("оценка:", Math.round(d.categories.performance.score * 100));
console.log("LCP в отчёте:", (m.largestContentfulPaint / 1000).toFixed(1), "с");
console.log("наблюдаемые FCP / LCP:", m.observedFirstContentfulPaint, "/", lcp, "мс");

const before = net.filter(r => r.networkRequestTime < lcp);
const total = before.reduce((s, r) => s + (r.transferSize || 0), 0);
console.log(`в окне: ${before.length} из ${net.length} запросов, ${kb(total)} КБ`,
  `≈ ${(kb(total) * 8 / 1600).toFixed(1)} с к LCP`);

const byType = {};
before.forEach(r => (byType[r.resourceType] = (byType[r.resourceType] || 0) + (r.transferSize || 0)));
Object.entries(byType).sort((x, y) => y[1] - x[1])
  .forEach(([type, bytes]) => console.log("  ", type.padEnd(12), kb(bytes), "КБ"));

console.log("самые тяжёлые:");
before.sort((x, y) => y.transferSize - x.transferSize).slice(0, 10)
  .forEach(r => console.log("  ", String(kb(r.transferSize)).padStart(5), "КБ", r.url.slice(0, 110)));

Так выглядит вывод для главной страницы этого сайта (адреса сокращены):

$ node lh-window.js lh.json
оценка: 100
LCP в отчёте: 1.9 с
наблюдаемые FCP / LCP: 717 / 717 мс
в окне: 15 из 15 запросов, 345 КБ ≈ 1.7 с к LCP
   Image        257 КБ
   Font         54 КБ
   Script       15 КБ
   Document     10 КБ
   Stylesheet   8 КБ
   Other        0 КБ
самые тяжёлые:
     103 КБ …/ach-thumb/fcd85dc5afb040b_640x1280.avif
      81 КБ …/ach-thumb/32169e064265b8b_640x1280.avif
      73 КБ …/ach-thumb/24ba541f5384f27_640x1280.avif
      19 КБ …/assets-dist/fonts/jetbrains-mono-700.6e87891dc72b9bdd
      18 КБ …/assets-dist/fonts/jetbrains-mono-400.04f909ccef08bb75
      17 КБ …/assets-dist/fonts/ibm-plex-sans-400.2460b553b1a06ac5c
      10 КБ https://alekseychikun.ru/
       8 КБ …/assets-dist/js/492.0fa1b1e1a04f56ca24f0.js
       5 КБ …/assets-dist/css/987.f0a7399b12b25dcede62.css
       4 КБ …/assets-dist/js/445.9def0e0b660ffb298d4f.js

Оценка 100, но и здесь скрипт кое-что нашёл. Три самые тяжёлые картинки (73–103 КБ) — это превью работ, которые стоят ниже первого экрана. Браузер начал качать их ещё до отрисовки: ленивая загрузка Chrome стартует с большим запасом до края экрана. По формуле это ≈ 1,3 с из 1,9 с модельного LCP — на оценку пока не повлияло, но на странице потяжелее такие картинки уже стоили бы баллов. Как это лечится, разберём в части про картинки.

Скрипт печатает оценку, обе версии LCP, сколько запросов и килобайт ушло до отрисовки (и сразу — сколько секунд это стоит по формуле из второй части), сумму по типам файлов и десятку самых тяжёлых. Обычно уже по разбивке по типам всё понятно: если в окне два мегабайта Font или Media — вот она, причина.

Как читать результат

  1. Сравните модельный и наблюдаемый LCP. Если модельный в разы больше — причина в том, что попало в окно, и скрипт её покажет. Если они близки и оба большие — страница действительно медленная: смотрите ответ сервера и то, что выполняется до отрисовки.
  2. Посчитайте вес в окне по формуле. Сходится с «задержкой» в фазах LCP — причина найдена.
  3. Пройдитесь по десятке тяжёлых. Для каждого файла вопрос один: нужен ли он первому экрану? Шрифт начертания, которого нет на первом экране, видео из карточки ниже сгиба, картинка скрытого меню — нет.
  4. Загляните в фазы LCP. Большая «задержка отрисовки» при быстрой загрузке — элемент спрятан (анимация, скрипт). Большая «задержка загрузки» — браузер поздно узнал о картинке (например, она в CSS-фоне).

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

Чем этот скрипт отличается от вкладки «Сеть» в DevTools?

DevTools показывает загрузку на вашем быстром компьютере, где окно первой отрисовки совсем другое. Скрипт берёт запросы из прогона Lighthouse — того, по которому считалась оценка.

Почему фильтр по LCP, а не по первой отрисовке?

Модель считает критическим путём LCP всё, что запрошено до появления крупнейшего элемента. На большинстве страниц FCP и LCP близки; если между ними большой разрыв — посмотрите оба фильтра, это само по себе подсказка.

Нет времени разбираться с отчётами?

Пришлите адрес — прогоню серию, разберу окно первой отрисовки и пришлю список правок по важности.

· полезно?
АЧ
Алексей Чикун веб-разработчик, команда TRIOHEAD

Делаю сайты и сервисы целиком: от вёрстки до сервера. Пишу о том, что проверил на своих проектах.

Комментарии

// загружаю комментарии…

Комментарии публикуются после проверки. Ссылки и HTML не принимаются.