Оптимізація Total Blocking Time (TBT) сайту
Total Blocking Time — сума "заблокованих" мілісекунд між FCP та TTI. Задача довше 50 мс в main thread вважається Long Task; все, що сверх цих 50 мс, суммується в TBT. Хороше значення TBT по Lighthouse: менше 200 мс (десктоп), менше 300 мс (мобільний симулятор з 4× CPU throttling).
Діагностика: знаходимо Long Tasks
Перший крок — знайти, що саме створює Long Tasks. Chrome DevTools, вкладка Performance: записуємо завантаження сторінки, дивимось червоні полосочки над main thread.
Програмно через Long Task API:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log({
name: entry.name,
duration: entry.duration,
startTime: entry.startTime,
attribution: entry.attribution,
});
}
});
observer.observe({ type: 'longtask', buffered: true });
Причина №1: тяжкий JavaScript при старті
Найчастіша причина — великий JS-бандл, який парсується та виконується синхронно при завантаженні. Кожен мегабайт JS потребує ~1 секунди парсингу на середньому мобільному пристрої.
Рішення: code splitting + lazy loading. Для React:
// До: усе грузиться одразу
import HeavyChart from './HeavyChart';
import DataTable from './DataTable';
// Після: грузимо тільки при необхідності
const HeavyChart = lazy(() => import('./HeavyChart'));
const DataTable = lazy(() => import('./DataTable'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>Показати графік</button>
{showChart && (
<Suspense fallback={<Skeleton />}>
<HeavyChart />
</Suspense>
)}
</div>
);
}
Причина №2: сторонні скрипти
Google Tag Manager, чати, пікселі рекламних сітей — кожний сторонній скрипт може створювати Long Tasks.
Рішення:
<!-- Замість синхронного: -->
<script src="https://widget.example.com/chat.js"></script>
<!-- Використовуємо async або defer: -->
<script src="https://widget.example.com/chat.js" async></script>
<!-- Або відкладена загрузка після взаємодії: -->
<script>
function loadChat() {
const s = document.createElement('script');
s.src = 'https://widget.example.com/chat.js';
document.head.appendChild(s);
}
['click', 'scroll', 'keydown'].forEach(event => {
window.addEventListener(event, loadChat, { once: true });
});
</script>
Причина №3: тяжкі вичислення
Вважуємо для вичислень, криптографія, складні фільтри — виносимо в Web Worker:
// worker.js
self.onmessage = function(e) {
const { data, operation } = e.data;
let result = heavyComputation(data, operation);
self.postMessage(result);
};
// main.js
const worker = new Worker('/worker.js');
worker.postMessage({ data: largeArray, operation: 'sort' });
worker.onmessage = (e) => setTableData(e.data);
Причина №4: React 18 - startTransition
Помічаємо некритичні оновлення як переходи:
import { startTransition, useState } from 'react';
function SearchPage() {
const [inputValue, setInputValue] = useState('');
const [searchQuery, setSearchQuery] = useState('');
function handleInput(e) {
const value = e.target.value;
setInputValue(value); // Срочне: користувач бачить набір
startTransition(() => {
setSearchQuery(value); // Несрочне: можна перервати
});
}
return (
<>
<input value={inputValue} onChange={handleInput} />
<SearchResults query={searchQuery} />
</>
);
}
Контрольний чеклист оптимізації
- Розбили JS-бандл: initial chunk < 150 kB gzip, роути ленивові
- Сторонні скрипти: всі некритичні — з defer або відкладеною загрузкою
- Тяжкі вичислення вынесені в Web Worker
- Довгі синхронні цикли розбиті через
scheduler.yield()(Chrome 115+) - React:
startTransitionдля некритичних оновлень - Рекламні сіті: перевірили для 200–500 мс TBT
Терміни
Діагностика та план — 1–2 робочих дні. Code splitting + ленива загрузка типового React-додатку — 3–5 робочих днів. Повний цикл: діагностика, оптимізація, мониторинг INP в продакшні — 1,5–3 тижні залежно від складності додатку.







