Знакомая сцена: заказчик пишет, что сайт «грузится вечность», а у разработчика страница открывается мгновенно. Lighthouse на ноутбуке показывает зелёные цифры, анимации плавные, кнопки отзываются сразу. Возникает соблазн списать жалобу на плохой интернет у заказчика или на его старый телефон.

Чаще всего правы оба. Страница действительно быстрая на машине разработчика и действительно медленная у пользователя. Дело не в квалификации: её просто проверяют в тепличных условиях, где тормоза физически не видны. Мощный процессор, быстрая сеть и прогретый кэш прячут те самые проблемы, на которые жалуются пользователи.

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

Пять теплиц, в которых живёт разработчик

Железо: ноутбук против среднего Android

Современный ноутбук разработчика однопоточно в несколько раз быстрее процессора в недорогом Android-смартфоне. А именно однопоточная производительность решает, как быстро браузер разберёт и выполнит JavaScript: основная работа страницы идёт в одном главном потоке (подробнее — в статье про однопоточность в JavaScript).

Возьмём безобидный на вид обработчик: фильтрация каталога по клику.

filterButton.addEventListener('click', () => {
  // 5000 товаров, для каждого — поиск по строке и пересборка карточки
  const visible = products.filter((item) =>
    item.title.toLowerCase().includes(query.toLowerCase())
  );
  list.innerHTML = visible.map(renderCard).join('');
});

На ноутбуке этот код отрабатывает, скажем, за 40 мс — глазом не заметить. На телефоне, который медленнее в четыре раза, получается 160 мс. Если прибавить пересчёт стилей и отрисовку пяти тысяч карточек, интерфейс ощутимо замирает после каждого нажатия. Метрика INP (Interaction to Next Paint, «задержка до следующей отрисовки») считает хорошим ответ за 200 мс и быстрее, и на телефоне этот обработчик выходит за порог.

Сеть: localhost против мобильного интернета с задержкой

Про скорость сети обычно думают в мегабитах. Но для загрузки страницы не менее важна задержка — RTT (round-trip time, время, за которое запрос доходит до сервера и ответ возвращается обратно). Прежде чем браузер получит первый байт HTML, ему нужно несколько таких кругов: найти IP-адрес через DNS, установить TCP-соединение, договориться о шифровании TLS и только потом отправить сам запрос.

// Грубая прикидка «сколько ждать первый байт» по числу кругов
const roundTrips = 4;   // DNS + TCP + TLS + сам запрос

console.log(roundTrips * 2);    // localhost, RTT ~2 мс   → ~8 мс
console.log(roundTrips * 20);   // домашний Wi-Fi, ~20 мс → ~80 мс
console.log(roundTrips * 150);  // мобильная сеть, ~150 мс → ~600 мс

Числа условные, но порядок честный: на мобильной сети больше полсекунды уходит ещё до того, как сервер начал что-то отдавать. А каждый скрипт или шрифт с нового домена — это ещё один набор кругов. На localhost всего этого просто нет, поэтому лишний сторонний виджет там ничего не стоит (как держать такие скрипты в узде — в статье про сторонние скрипты).

Тёплый кэш: сотый визит против первого

Разработчик открывает свой сайт сотни раз в день. Шрифты, библиотеки, картинки давно лежат в кэше браузера, DNS-запись запомнена, соединение с сервером часто ещё открыто. Пользователь из поисковой выдачи приходит впервые и качает всё с нуля.

<!-- У разработчика всё это из кэша. У нового посетителя — 4 запроса к 3 разным доменам -->
<link rel="stylesheet" href="https://fonts.example.com/css?family=Inter">
<script src="https://cdn.example.com/slider.min.js"></script>
<script src="https://cdn.example.com/analytics.js"></script>
<img src="https://media.example.com/hero-4000px.jpg" alt="Главный баннер">

Если хотя бы один из этих доменов нужен для первого экрана, стоит заранее открыть к нему соединение подсказкой preconnect — так браузер начнёт DNS, TCP и TLS параллельно с разбором HTML:

<link rel="preconnect" href="https://media.example.com">

Пустая база: три карточки вместо трёхсот

В локальной базе обычно лежит пара тестовых записей с короткими названиями вроде «Тест 1». В бою у пользователя в корзине 40 позиций, в ленте — 300 карточек, а название товара занимает три строки. Сложность рендера растёт вместе с данными, и то, что было незаметно на трёх элементах, тормозит на трёхстах.

Лечится генератором правдоподобных данных «с запасом»: длинные тексты, большие списки, картинки разных пропорций.

// seed-данные для локальной разработки: не 3 записи, а 500 «живых»
const longTitles = [
  'Кроссовки беговые с амортизирующей подошвой и сетчатым верхом',
  'Рюкзак городской водоотталкивающий с отделением для ноутбука 16"',
  'Чайник',
];

const products = Array.from({ length: 500 }, (_, index) => ({
  id: index + 1,
  title: longTitles[index % longTitles.length],
  price: Math.round(Math.random() * 300 + 10),
  image: `https://picsum.photos/seed/${index}/600/${400 + (index % 3) * 100}`,
}));

География: сервер рядом с разработчиком

Сервер и разработчик часто находятся в одной стране, а то и в одном дата-центре. Пользователь с другого континента получает RTT в 150–250 мс просто из-за расстояния: свет по оптоволокну быстрее не бегает. Без CDN, который раздаёт статику с ближайшего к пользователю узла, каждый из кругов из раздела про сеть умножается.

Проверить, как сайт выглядит издалека, можно сервисами вроде WebPageTest: там выбирается локация тестового агента и тип соединения.

Как притвориться пользователем в DevTools

Купить десяток телефонов и раскатать их по разным странам не обязательно. Большую часть разрыва закрывают встроенные инструменты Chrome.

Замедлить сеть

На вкладке Network есть выпадающий список троттлинга. В нём готовые профили:

Профиль Скачивание Отдача Задержка (RTT)
Fast 4G 9 Мбит/с 1,5 Мбит/с 60 мс
Slow 4G 1,6 Мбит/с 750 Кбит/с 150 мс
3G 500 Кбит/с 500 Кбит/с 400 мс

В старых туториалах эти профили называются Fast 3G и Slow 3G — это те же Slow 4G и 3G, их просто переименовали. Рядом с выпадающим списком стоит галочка Disable cache: с ней каждая перезагрузка превращается в первый визит. Профиль Slow 4G с выключенным кэшем — хорошая отправная точка: именно такие параметры сети Lighthouse использует по умолчанию для мобильного отчёта.

Если нужен свой профиль (например, с потерей пакетов), его создают в Settings → Throttling → Network throttling profiles.

Замедлить процессор

Сеть — только половина. На вкладке Performance в настройках окружения есть троттлинг CPU. Раньше там были фиксированные множители 4x и 6x, но 4x на мощном ноутбуке и 4x на слабом — это разные телефоны. Начиная с Chrome 134 DevTools умеет калибровать троттлинг: короткий бенчмарк замеряет рабочую машину и подбирает множители для двух пресетов — Low-tier mobile и Mid-tier mobile. Запускается калибровка там же или в Settings → Throttling → Calibrate.

Для типичного пользователя команда Chrome советует брать Mid-tier, а Low-tier — если аудитория сидит на совсем бюджетных устройствах (подробности — в блоге Chrome for Developers). Одна оговорка: троттлинг замедляет только процессор. Видеочип не эмулируется, поэтому тяжёлые CSS-эффекты вроде backdrop-filter на реальном телефоне всё равно могут вести себя иначе.

Настройки окружения в панели Performance: CPU — Mid-tier mobile, сеть — Slow 4G

Lighthouse: полезен, но это ещё не точка истины

Lighthouse в мобильном режиме эмулирует экран телефона, сеть Slow 4G (RTT 150 мс, 1,6 Мбит/с) и замедление CPU в 4 раза. Это уже гораздо честнее, чем голая машина разработчика. Но у отчёта три слабых места:

  • сеть по умолчанию симулируется — страница грузится на полной скорости, а итоговые цифры потом пересчитываются по модели;
  • это один прогон одной страницы, без реального пользователя, который кликает, листает и открывает меню;
  • в обычном отчёте о загрузке нет INP: без настоящих взаимодействий его не посчитать, вместо него Lighthouse показывает косвенную метрику Total Blocking Time.

Запускать Lighthouse стоит в окне инкогнито: расширения браузера тоже выполняют код на странице и портят замер.

# Lighthouse из командной строки: мобильный профиль включён по умолчанию
npx lighthouse https://example.com --view

# Тот же прогон, но с реальным, а не симулированным троттлингом сети
npx lighthouse https://example.com --throttling-method=devtools --view

Настоящий телефон через remote debugging

Самая честная проверка — дешёвый Android на столе. Телефон подключают кабелем, в его настройках разработчика включают отладку по USB, а на компьютере открывают chrome://inspect. DevTools подключаются к вкладке на телефоне: можно записать профиль в Performance и увидеть, какой именно скрипт съедает время, уже без всякой эмуляции.

Лаборатория против поля: чьим цифрам верить

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

Google собирает полевые данные из Chrome в открытый отчёт CrUX (Chrome User Experience Report), и их видно в PageSpeed Insights — в верхнем блоке, с данными реальных пользователей. Нижний блок с баллом — это лабораторный Lighthouse. Нередко они расходятся: балл 95, а полевой LCP красный.

PageSpeed Insights: полевые метрики в жёлтой зоне при зелёном балле Lighthouse

Основные метрики — Core Web Vitals — и пороги «хорошо»:

Метрика Что измеряет Хорошо
LCP (Largest Contentful Paint) когда отрисовался самый крупный элемент первого экрана до 2,5 с
INP (Interaction to Next Paint) как быстро страница реагирует на клики и нажатия клавиш до 200 мс
CLS (Cumulative Layout Shift) насколько сильно прыгает вёрстка при загрузке до 0,1

Важная деталь: оценка ставится по 75-му перцентилю. Это значит, что хорошо должно быть у трёх посетителей из четырёх, включая медленных. Среднее по больнице не спасает: если у 30 % пользователей страница грузится по 5 секунд, LCP провален, даже если у остальных всё мгновенно. Про инструменты для отлова прыгающей вёрстки есть отдельная подборка — инструменты для тестирования CLS.

Собрать полевые метрики самому

CrUX показывает данные только для сайтов с заметным трафиком, а обновляются они скользящим окном за 28 дней. Чтобы видеть метрики своих пользователей сразу и по каждой странице, их собирают на самой странице — это называется RUM (Real User Monitoring). Проще всего через библиотеку web-vitals от команды Chrome:

import { onLCP, onINP, onCLS } from 'web-vitals';

function sendToAnalytics(metric) {
  const body = JSON.stringify({
    name: metric.name,      // 'LCP' | 'INP' | 'CLS'
    value: metric.value,    // миллисекунды (для CLS — безразмерное число)
    rating: metric.rating,  // 'good' | 'needs-improvement' | 'poor'
    id: metric.id,
    page: location.pathname,
    memory: navigator.deviceMemory ?? null,
    network: navigator.connection?.effectiveType ?? null,
  });

  // sendBeacon переживает закрытие вкладки, fetch с keepalive — запасной вариант
  if (!navigator.sendBeacon?.('/analytics', body)) {
    fetch('/analytics', { body, method: 'POST', keepalive: true });
  }
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);

Стоит обратить внимание на поля memory и network: с ними видно не просто «INP плохой», а «INP плохой у пользователей с 2 ГБ памяти». Часть метрик библиотека отправляет не сразу, а когда пользователь взаимодействует со страницей, переключает вкладку или уходит — это нормально.

Подстроиться под слабое устройство в коде

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

Что браузер рассказывает о себе

// Network Information API — только Chromium-браузеры
navigator.connection?.effectiveType; // 'slow-2g' | '2g' | '3g' | '4g'
navigator.connection?.rtt;           // оценка задержки в мс, округлённая
navigator.connection?.downlink;      // оценка скорости в Мбит/с, округлённая
navigator.connection?.saveData;      // true, если включена экономия трафика

// Объём оперативной памяти в ГБ, округлённый
navigator.deviceMemory;              // например, 2, 4 или 8

// Число логических ядер процессора — есть во всех браузерах
navigator.hardwareConcurrency;       // например, 4

Две ловушки. Во-первых, effectiveType вычисляется по недавно замеренным задержке и скорости, а не по реальному типу сети: телефон в перегруженном Wi-Fi вполне может отчитаться как 3g. Значение 4g при этом — потолок шкалы: оно означает «не медленно», а не «быстро». Во-вторых, в Safari и Firefox этих свойств нет вовсе, поэтому каждое чтение страхуем опциональной цепочкой и выбираем безопасное поведение по умолчанию.

Поддержка браузерами
chrome
Chrome
61
firefox
Firefox
 
edge
Edge
79
safari
Safari
 
opera
Opera
48

С navigator.deviceMemory картина похожая: Chrome с 63-й версии, Edge с 79-й, Opera с 50-й, а в Safari и Firefox свойства нет.

Поддержка браузерами
chrome
Chrome
63
firefox
Firefox
 
edge
Edge
79
safari
Safari
 
opera
Opera
50

Тяжёлое — только когда устройство потянет

Типичный кандидат на адаптацию — фоновое видео на главной. Это мегабайты трафика и работа декодера, а пользователь со слабым телефоном и без него всё поймёт. Логика «от простого к сложному»: по умолчанию показываем картинку-постер, а видео подгружаем только если ничто не говорит о слабом устройстве.

function isConstrained() {
  const connection = navigator.connection;

  if (connection?.saveData) return true;
  if (['slow-2g', '2g', '3g'].includes(connection?.effectiveType)) return true;
  if (navigator.deviceMemory && navigator.deviceMemory < 4) return true;

  return false; // данных нет (Safari, Firefox) — считаем, что всё в порядке
}

const hero = document.querySelector('.hero');

if (!isConstrained()) {
  const video = document.createElement('video');
  video.src = '/media/hero.mp4';
  video.muted = true;
  video.loop = true;
  video.autoplay = true;
  video.playsInline = true;
  hero.append(video);
}

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

Картинки и шрифты по размеру устройства

Самая частая причина плохого LCP на телефоне — баннер на 4000 пикселей, который отдают всем подряд. Атрибуты srcset и sizes позволяют браузеру выбрать версию под экран, а loading="lazy" откладывает загрузку картинок ниже первого экрана:

<!-- Главная картинка первого экрана: грузим сразу, но нужного размера -->
<img src="/img/hero-800.jpg"
     srcset="/img/hero-480.jpg 480w, /img/hero-800.jpg 800w, /img/hero-1600.jpg 1600w"
     sizes="100vw"
     width="1600" height="900"
     alt="Команда за работой над макетом">

<!-- Всё, что ниже первого экрана, — лениво -->
<img src="/img/card-1.jpg" loading="lazy" width="400" height="300" alt="Карточка товара">

Ленивую загрузку на главную картинку первого экрана не ставим: браузер отложит её и LCP только ухудшится. Подробнее о выборе версий картинки — в статье про загрузку картинок в зависимости от устройства, о форматах — в сравнении JPEG, PNG, WebP и AVIF, о шрифтах — в статье про разумную загрузку шрифтов.

Привычки, которые лечат искажение

Разовая проверка под троттлингом помогает, но через месяц сайт снова обрастёт виджетами. Надёжнее встроить реальность в рабочий процесс.

  • Работать под Mid-tier mobile и Slow 4G хотя бы при проверке новой фичи. Не всё время, но каждый раз перед тем, как отдать задачу.
  • Держать на столе дешёвый Android. Не флагман и не свой старый айфон, а модель из нижнего ценового сегмента — как у заметной части аудитории.
  • Сидеть на seed-данных с запасом. Сотни записей, длинные строки, картинки без заданных пропорций.
  • Завести бюджет производительности. Например, «JavaScript первого экрана — не больше 170 КБ в gzip», и проверять его в CI, чтобы лишняя библиотека не проскочила незаметно. Помогает и tree shaking, который вырезает из бандла неиспользуемый код.
  • Смотреть полевые метрики, а не только балл Lighthouse. Раз в пару недель заглядывать в PageSpeed Insights или в свой RUM-дашборд.

Бюджет удобно зафиксировать прямо в конфиге Lighthouse CI — сборка упадёт, если страница его превысит:

{
  "ci": {
    "assert": {
      "assertions": {
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "total-blocking-time": ["warn", { "maxNumericValue": 300 }],
        "resource-summary:script:size": ["error", { "maxNumericValue": 170000 }]
      }
    }
  }
}

Итог

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

Чтобы увидеть сайт глазами пользователя, достаточно трёх шагов: включить в DevTools калиброванный троттлинг CPU и Slow 4G с выключенным кэшем, сверяться с полевыми метриками по 75-му перцентилю и подстраивать тяжёлые части страницы под сигналы устройства. Всё остальное — дело привычки.