Загрузка...
Загрузка...
Практическое руководство по оптимизации INP — метрики отзывчивости интерфейса. Причины задержек, разбиение длинных задач, оптимизация JavaScript и обработчиков событий.
Полное руководство по оптимизации Largest Contentful Paint. Причины медленного LCP, методы диагностики, практические решения для изображений, шрифтов и серверной инфраструктуры.
SEOПрактическое руководство по устранению Cumulative Layout Shift. Диагностика причин сдвигов макета, резервирование размеров, оптимизация шрифтов и динамического контента.
SEOКак оптимизировать анкорный текст внутренних ссылок для SEO. Разнообразие анкоров, передача релевантности, избежание переоптимизации. Практические рекомендации.
SEOРуководство по массовой проверке URL. Сценарии использования, инструменты, интерпретация результатов. Проверка статус-кодов, редиректов и битых ссылок на масштабе.
Поделитесь с коллегами или изучите другие материалы блога
INP (Interaction to Next Paint) заменил FID в марте 2024 года как метрика отзывчивости в Core Web Vitals. INP измеряет задержку между действием пользователя (клик, нажатие клавиши, тап) и визуальным откликом браузера. Медленный INP означает «зависающий» интерфейс — кнопки не реагируют, формы тормозят. В этом руководстве разберём причины плохого INP и практические методы оптимизации.
INP фиксирует задержку от начала взаимодействия до следующего кадра отрисовки. Браузер измеряет три фазы:
INP — худшее (или одно из худших) взаимодействие за сессию. Один тяжёлый клик может испортить метрику для всей страницы.
| Оценка | INP | Восприятие |
|---|---|---|
| Хорошо | ≤ 200 мс | Мгновенный отклик |
| Требует улучшения | 200 – 500 мс | Заметная задержка |
| Плохо | > 500 мс | Интерфейс «зависает» |
Проверить INP можно в инструменте Web Vitals на reChecker.
JavaScript выполняется в одном потоке. Задача дольше 50 мс блокиет main thread — браузер не может обработать input, отрисовать кадр, выполнить другие скрипты. Один цикл обработки события (input → handler → render) должен укладываться в 200 мс.
Обработчик клика, который парсит JSON, фильтрует массив из тысяч элементов или выполняет сложные DOM-операции, блокирует поток. Пользователь кликает — ничего не происходит — через секунду интерфейс обновляется.
Аналитика, чаты, реклама, виджеты — часто неоптимизированы. Они добавляют задачи в main thread и конкурируют с вашим кодом за время выполнения.
Чтение layout-свойств (offsetHeight, getBoundingClientRect) после записи в DOM заставляет браузер пересчитывать layout синхронно. Цикл read-write-read-write в одном кадре может добавить сотни миллисекунд.
Тяжёлые библиотеки (Moment.js, Lodash целиком, неоптимизированные бандлы) увеличивают время парсинга и выполнения при загрузке. Первое взаимодействие происходит на фоне инициализации — INP страдает.
Отложить нефоновую работу на свободное время:
function processChunk(data, index) {
// Обработка порции данных
}
function processInChunks(data, chunkSize = 100) {
let index = 0;
function scheduleChunk() {
if (index >= data.length) return;
const chunk = data.slice(index, index + chunkSize);
index += chunkSize;
processChunk(chunk, index);
requestIdleCallback(scheduleChunk, { timeout: 50 });
}
requestIdleCallback(scheduleChunk);
}
Простой способ «уступить» main thread:
function handleClick() {
// Критичная часть — обновить UI сразу
updateButtonState('loading');
setTimeout(() => {
// Тяжёлая работа — в следующем таске
const result = heavyComputation();
updateResults(result);
}, 0);
}
if ('scheduler' in window && 'yield' in window.scheduler) {
await scheduler.yield();
}
Позволяет явно уступить поток. Поддержка пока ограничена.
Для частых событий (scroll, resize, input) — не выполнять тяжёлую работу на каждое срабатывание:
function debounce(fn, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
searchInput.addEventListener('input', debounce((e) => {
performSearch(e.target.value);
}, 300));
button.addEventListener('click', () => {
// 1. Мгновенный визуальный отклик
button.disabled = true;
button.textContent = 'Загрузка...';
// 2. Тяжёлая работа — асинхронно
requestAnimationFrame(() => {
setTimeout(() => {
const data = fetchAndProcessData();
renderResults(data);
button.disabled = false;
button.textContent = 'Готово';
}, 0);
});
});
element.addEventListener('touchstart', handler, { passive: true });
passive: true сообщает браузеру, что вы не вызовете preventDefault. Браузер может начать скролл сразу, не дожидаясь завершения обработчика.
// Плохо: read-write-read-write
for (const el of elements) {
const height = el.offsetHeight; // read — форсирует layout
el.style.height = height + 10 + 'px'; // write
}
// Хорошо: все reads, потом все writes
const heights = elements.map(el => el.offsetHeight);
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px';
});
function updateLayout() {
const scrollTop = container.scrollTop; // read
requestAnimationFrame(() => {
items.forEach((item, i) => {
item.style.transform = `translateY(${scrollTop + i * 50}px)`; // write
});
});
}
Загружайте только нужный код для текущего экрана:
// Вместо
import { heavyLibrary } from './heavy';
// Используйте динамический импорт
button.addEventListener('click', async () => {
const { heavyLibrary } = await import('./heavy');
heavyLibrary.doSomething();
});
Загружайте виджеты после первого взаимодействия:
let chatLoaded = false;
chatButton.addEventListener('click', () => {
if (!chatLoaded) {
chatLoaded = true;
const script = document.createElement('script');
script.src = 'https://chat.widget.com/script.js';
document.body.appendChild(script);
}
openChat();
});
// main.js
const worker = new Worker('processor.js');
worker.postMessage({ data: largeArray });
worker.onmessage = (e) => {
updateUI(e.data.result);
};
// processor.js
self.onmessage = (e) => {
const result = heavyComputation(e.data.data);
self.postMessage({ result });
};
Worker выполняется в отдельном потоке — не блокирует main thread.
| Действие | Приоритет | Сложность |
|---|---|---|
| Измерить INP и найти худшие взаимодействия | Высокий | Низкая |
| Разбить обработчики > 50 мс на части | Высокий | Средняя |
| Добавить мгновенный визуальный отклик (disabled, loading) | Высокий | Низкая |
| Устранить layout thrashing | Высокий | Средняя |
| Дебаунс/троттлинг для scroll, input, resize | Средний | Низкая |
| Отложить загрузку третьесторонних скриптов | Средний | Средняя |
| Code splitting для тяжёлых модулей | Средний | Средняя |
| Web Workers для вычислений | Низкий | Высокая |
// Измерение длительности обработчика
button.addEventListener('click', () => {
const start = performance.now();
doWork();
console.log('Handler took:', performance.now() - start, 'ms');
});
import { onINP } from 'web-vitals';
onINP(console.log);
Отправляйте INP в аналитику для мониторинга в production.
Минимум JavaScript — INP обычно хороший. Проблемы могут быть от третьесторонних скриптов (аналитика, чаты). Отложите их загрузку.
React, Vue, Angular — гидрация и обновление DOM могут блокировать main thread. Оптимизация: code splitting, lazy loading компонентов, разбиение длинных задач. Избегайте синхронных операций при первом взаимодействии.
Корзина, фильтры, сортировка — частые интерактивные элементы. Обработчики должны быть лёгкими. Тяжёлые операции (фильтрация тысяч товаров) — в Web Worker или с debounce.
import { onINP } from 'web-vitals';
onINP((metric) => {
// Отправка в Google Analytics, собственную аналитику
gtag('event', 'inp', {
value: metric.value,
rating: metric.rating,
id: metric.id,
});
});
Настройте алерты при ухудшении INP (например, p75 > 500 мс). Раннее обнаружение проблем.
Анализируйте INP по устройствам, браузерам, страницам. Мобильные часто хуже десктопов. Конкретные страницы могут иметь проблемы.
INP — одна из трёх метрик Core Web Vitals. Подробнее о LCP и CLS — в полном руководстве Core Web Vitals 2026. Проверить все метрики вашего сайта можно в инструменте Web Vitals на reChecker.