Знакомая сцена: заказчик пишет, что сайт «грузится вечность», а у разработчика страница открывается мгновенно. 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 на реальном телефоне всё равно могут вести себя иначе.

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 красный.

Основные метрики — 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 этих свойств нет вовсе, поэтому каждое чтение страхуем опциональной цепочкой и выбираем безопасное поведение по умолчанию.
С navigator.deviceMemory картина похожая: Chrome с 63-й версии, Edge с 79-й, Opera с 50-й, а в Safari и Firefox свойства нет.
Тяжёлое — только когда устройство потянет
Типичный кандидат на адаптацию — фоновое видео на главной. Это мегабайты трафика и работа декодера, а пользователь со слабым телефоном и без него всё поймёт. Логика «от простого к сложному»: по умолчанию показываем картинку-постер, а видео подгружаем только если ничто не говорит о слабом устройстве.
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-му перцентилю и подстраивать тяжёлые части страницы под сигналы устройства. Всё остальное — дело привычки.
Комментарии (0)