Пользователь нажимает кнопку — интерфейс замирает на секунду. Скролл дёргается при обновлении данных. Анимации пропускают кадры. Эти симптомы появляются, когда основой UI-лагов почти всегда становятся длинные задачи (Long Tasks) — операции JavaScript, блокирующие основной поток дольше 50 мс. В эру Core Web Vitals, где INP (Interaction to Next Paint) стал ключевым метриком, игнорировать эту проблему фатально.
Что происходит под капотом?
Браузерный движок — однопоточный дирижёр. Когда скрипт выполняется дольше 50 мс, он блокирует:
- Отрисовку (rendering)
- Обработку взаимодействий
- Парсинг HTML/CSS Вот почему вы видите "белые экраны" или заедания при сложных вычислениях.
Инструментарий следствия
Chrome DevTools — ваша первая остановка:
- Откройте вкладку Performance
- Запишите сессию при типичном пользовательском сценарии
- Ищите красные треугольники 🚩 с меткой "Long Task"
- Разверните блок и проверьте Bottom-Up панель — там откроются реальные виновники: функции или операции, съедающие драгоценные мсек.
Ещё эффективнее — полевое исследование. Добавьте прямо в код наблюдение за длинными задачами:
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
console.error("⚠️ Длинная задача:", entry.duration);
// Отправка в систему мониторинга
}
});
observer.observe({ type: "longtask", buffered: true });
Тактика экстренной операции
Расчленение монолитов: yield и кооперативная многозадачность
Тяжёлую функцию, обрабатывающую 10000 элементов? Режьте:
function chunkedProcessing(items, batchSize = 100) {
let index = 0;
function processNextBatch() {
const batch = items.slice(index, index + batchSize);
for (const item of batch) {
heavyTransform(item); // Ваша CPU-heavy операция
}
index += batchSize;
if (index < items.length) {
// Уступаем поток ДО завершения фрейма
setTimeout(processNextBatch, 0);
}
}
processNextBatch();
}
Почему 0 мс? Это не задержка, а помещение коллбэка в конец очереди макрозадач. Браузер сможет выполнить отрисовку между батчами.
Атомарная работа с анимациями — requestAnimationFrame
Обновление 500 DOM-элементов? Делите на порции внутри кадров анимации:
function updateElementsWithRAF(elements) {
let i = 0;
function updateFrame() {
const start = performance.now();
// Ограничение по времени выполнения (не более 4 мс на кадр)
while (i < elements.length && performance.now() - start < 4) {
elements[i].classList.toggle('active');
i++;
}
if (i < elements.length) {
requestAnimationFrame(updateFrame);
}
}
requestAnimationFrame(updateFrame);
}
Вынос в параллельную вселенную: Web Workers
Если надо декодировать видео, сортировать гигабайты данных или выполнить тяжелую математику — используйте Web Workers. Пример архитектуры:
main.js
const csvWorker = new Worker('csv-processor.js');
csvWorker.postMessage(largeCSVData);
csvWorker.onmessage = e => {
updateUI(e.data.processedResults);
};
csv-processor.js
self.onmessage = e => {
const parsedData = parseCSV(e.data); // Не блокирует UI!
const processed = applyComplexTransforms(parsedData);
self.postMessage({ processedResults: processed });
};
Критично: Не злоупотребляйте! Передача больших данных между потоками через postMessage
может съесть выигрыш в производительности из-за сериализации. Используйте Transferables для массивов, изображений и буферов:
mainContext.postMessage(
{ buffer: largeUint8Array },
[largeUint8Array.buffer] // Массив Transferable объектов
);
Контратака на первопричины
- Лекарство от вторичного рендеринга: Порции вычисления, изменяющие DOM, группируйте через
queueMicrotask()
илиrequestAnimationFrame
. - Ленивые загрузки с интеллектом: Разделение кода — хорошо, но не загружайте скрипты анализа данных сразу для страницы каталога. Используйте динамические
import()
в обработчике событий для кода, нужного только при взаимодействии:
button.addEventListener('click', async () => {
const analytics = await import('./heavyAnalytics.js');
analytics.trackComplexEvent(); // Загрузится ТОЛЬКО при клике
});
- Алгоритмический дзен: Map/Set вместо массивов при частых поисках, мемоизация сложных вычислений. О(n²) убьёт ваш интерфейс — познакомьтесь с O(n log n) решениями.
Профилактика рецидивов
- Интегрируйте Long Task detection в CI/CD
- Используйте Lighthouse CI для проверки INP
- Мониторьте реальные пользовательские метрики через RUM (например, зона INP в CrUX)
- Для SPA внедряйте шаблон PRPL pattern: Push (ресурсы), Render (начальный роут), Pre-cache (остальное), Lazy-load (по требованию)
Психика обострения
Не гонитесь за 0 длинных задач — в 99% проектов это экономически не оправдано. Ваша цель — разгромить задачи заметнее 150 мс, ощущаемые пользователем. Организуйте непрерывный цикл: профилируйте → исправляйте критичное → профилируйте.
Современные фреймворки не защищают от этой проблемы "из коробки". React, Vue или Svelte управляют рендерингом, но блокирующий код компонента или store напрямую скажется на производительности. Решение всегда итеративное:
- Локализовать задачу → Разбить её → Перенести в воркер если нужно → Проверить на реальном устройстве.
Каждый миллисекунд, отогнанный у длинной задачи — это шаг к интерфейсу, который чувствуется как "интуитивный". Ваши пользователи не отблагодарят вербально за 200 мс против 300 мс. Они просто перестанут чувствовать систему и начнут действовать на уровне инстинктов. К этому стоит стремиться.