Заміри продуктивності JS-коду
Продуктивність JS міряють трьома різними інструментами під три різні питання: таймер у коді каже, скільки триває операція, профайлер каже, де саме витрачається час, а мікробенчмарк каже, яка з реалізацій швидша. Окремий шар, це метрики реального користувача: довгі задачі, FPS і Core Web Vitals, бо кінцева мета не «швидкий алгоритм», а чутливий інтерфейс.
Теорія
TL;DR
- Швидкий замір:
performance.now(),performance.mark()/performance.measure(),console.time(). У Node.js те саме дає модульnode:perf_hooks. - Профілювання: вкладка Performance у DevTools дає flame chart, FPS і long tasks; Memory дає heap snapshot; Coverage показує, скільки JS реально виконується.
- Node.js профілюють через
node --inspectплюс Chrome DevTools, абоnode --profплюсprof-process, або clinic.js та 0x. - Мікробенчмарки пишуть на Benchmark.js чи tinybench: вони роблять прогрів JIT, багато ітерацій і статистику.
- Відгук UI: Long Tasks API ловить задачі довші за 50 мс,
requestAnimationFrameдає FPS. - Сторінкові метрики: Navigation і Resource Timing (TTFB, завантаження ресурсів), Core Web Vitals (LCP, CLS, INP, TBT), Lighthouse для разової перевірки.
- Методика важливіша за інструмент: прогрів, фіксовані дані, багато прогонів, медіана і квантилі, а не середнє.
Швидкий приклад
// 1. Найпростіший точний таймер, у мілісекундах з дробовою частиною
const t0 = performance.now();
buildIndex(items);
const t1 = performance.now();
console.log(`Час: ${(t1 - t0).toFixed(2)} ms`);
// 2. Те саме через User Timing API: позначки лишаються у таймлайні DevTools
performance.mark('A');
buildIndex(items);
performance.mark('B');
performance.measure('build-index', 'A', 'B');
console.table(performance.getEntriesByName('build-index'));
// 3. Найкоротший варіант для налагодження
console.time('op');
buildIndex(items);
console.timeEnd('op');Швидкі заміри прямо в коді
Це інструментування: ви самі ставите таймер навколо ділянки, яка вас цікавить.
Браузер
// Точний таймер (у мс, з долями)
const t0 = performance.now();
// ... код ...
const t1 = performance.now();
console.log(`Час: ${(t1 - t0).toFixed(2)} ms`);// User Timing API: позначки і вимірювання
performance.mark('A');
// ... код ...
performance.mark('B');
performance.measure('my-op', 'A', 'B');
console.table(performance.getEntriesByName('my-op'));// Швидко і просто
console.time('op');
// ... код ...
console.timeEnd('op');performance.now() повертає монотонний час високої точності від старту сторінки, тому він не стрибає при зміні системного годинника, на відміну від Date.now(). Перевага performance.mark() над голим таймером у тому, що позначки потрапляють у таймлайн профайлера, і ви бачите свій замір поруч із реальними подіями браузера.
Node.js
// perf_hooks, монотонні таймери
const { performance, PerformanceObserver } = require('node:perf_hooks');
performance.mark('A');
// ... код ...
performance.mark('B');
performance.measure('op', 'A', 'B');
new PerformanceObserver(list => console.table(list.getEntries()))
.observe({ entryTypes: ['measure'] });Профілювання: шукаємо, де саме гальмує
Таймер каже «повільно», але не каже «через що». Для цього є профайлер.
Браузер DevTools
- Performance (або Performance Insights): запис, і ви отримуєте flame chart (гарячі функції), FPS, layout і recalculate style, long tasks (≥ 50 мс).
- Memory: Heap snapshot та Allocation instrumentation, шукаємо витоки і зайве сміття.
- Coverage: скільки JS і CSS реально виконується, це прямо впливає на TBT.
Node.js
node --inspectі Chrome DevTools, звідти CPU profile і heap.node --profплюсprof-process, або clinic.js чи 0x для зручних графів полум'я.
Читати треба саме flame chart, а не лише підсумкові числа: широка смуга внизу означає функцію, у якій програма провела найбільше часу, і саме її варто оптимізувати першою.
Мікробенчмарки: порівняти дві реалізації
Коли питання звучить «яка з двох функцій швидша», голий performance.now() бреше: перший прогін іде по неоптимізованому коду, доки JIT не прогріється.
- Benchmark.js та tinybench роблять прогрів, багато ітерацій і статистику за вас.
import Benchmark from 'benchmark';
const suite = new Benchmark.Suite();
function a(arr){ /* варіант A */ }
function b(arr){ /* варіант B */ }
suite
.add('A', () => a(data))
.add('B', () => b(data))
.on('cycle', e => console.log(String(e.target)))
.on('complete', function () {
console.log('Faster:', this.filter('fastest').map('name'));
})
.run({ async: true });Важливо: прогрівайте тест (відкидайте перший прогін), фіксуйте вхідні дані, робіть кілька прогонів, закривайте зайві вкладки і розширення.
Метрики відгуку інтерфейсу і сторінки
Користувач не відчуває мілісекунд окремої функції, він відчуває підвисання і смикання.
- Long Tasks API: ловимо блокування головного потоку.
new PerformanceObserver((list) => {
for (const e of list.getEntries()) {
console.log('Long task', e.duration.toFixed(1), 'ms');
}
}).observe({ entryTypes: ['longtask'] });- rAF і FPS: простий індикатор плавності.
let last = performance.now(), frames = 0;
function tick(ts){
frames++;
if (ts - last >= 1000) { console.log('FPS:', frames); frames = 0; last = ts; }
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);Сторінкові, мережеві та рендер-метрики:
- Navigation і Resource Timing (
performance.getEntriesByType('navigation' | 'resource')), звідси TTFB і час завантаження ресурсів. - Core Web Vitals (LCP, CLS, INP, TBT), ставте бібліотеку web-vitals на реальний трафік.
- Lighthouse (разово), добре ловить регресії, але не замінює польові метрики з реальних користувачів.
Методика: як міряти коректно
- Сформулюйте мету: вас цікавить CPU, GC, layout чи мережа? Від цього залежить інструмент.
- Ізолюйте контекст: якщо міряєте чистий алгоритм, приберіть мережу і DOM.
- Прогрійте JIT: зробіть кілька «холостих» запусків перед заміром.
- Повторення і статистика: медіана і квантилі важливіші за середнє, бо одне випадкове сповільнення псує середнє.
- Контролюйте шум: фіксуйте дані та розмір входу, обмежте фонові процеси.
- Дивіться на полум'я (flame chart), а не лише на числа, оптимізуйте гарячі вузькі місця.
- Перевіряйте регресії в CI: невеликий бенч плюс порогові значення.
Що міряти в типових сценаріях:
| Сценарій | Чим міряти |
|---|---|
| Алгоритм або колекції | Мікробенч (Benchmark.js) плюс performance.now() |
| Інтерактивність UI | DevTools Performance, Long Tasks, FPS |
| Рендер і стилі | Вкладки Rendering, Layout, Styles у профайлері; уникати layout thrashing |
| Пам'ять і витоки | Heap snapshots, Allocation timeline |
| Node API | perf_hooks, CPU profile, перевірка синхронних методів (замінити на async або worker_threads) |
Типові помилки
- Один прогін «на око»: шум і непрогрітий JIT спотворять картину.
- Мікробенчити DOM-операції: результат залежить від реального дерева і стилів, такі речі профілюють у DevTools, а не в бенчмарку.
- Порівнювати дві реалізації на різних вхідних даних або різних розмірах входу.
- Оптимізувати без flame chart: легко вилікувати не те місце і не отримати жодного виграшу.
- Брати середнє замість медіани: один викид від збирача сміття зробить обидва варіанти «однаковими».
- Використовувати
Date.now()для коротких замірів: роздільність груба, а час немонотонний. - Довіряти лише лабораторному Lighthouse і не збирати польові Core Web Vitals із реальних пристроїв.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.