Вузькі місця в JS-коді
Вузьке місце, це ділянка коду, яка обмежує продуктивність усього застосунку, і в JavaScript таких ділянок вісім типових: головний потік, DOM, пам'ять, цикли, мережа, відмальовування, бандл і алгоритми. Дев'яте, найпоширеніше, це відсутність профілювання, коли оптимізують навмання.
Теорія
TL;DR
- JavaScript однопотоковий: будь-яка важка синхронна операція блокує UI, рендеринг і обробку подій.
- Операції з DOM найдорожчі; читання геометрії всередині циклу спричиняє reflow.
- Витоки пам'яті дають таймери, слухачі подій, замикання та глобальні змінні.
- На великих масивах вирішують алгоритм і структура даних:
SetіMapзамість лінійного пошуку. - Мережа і розмір бандла часто важать більше, ніж швидкість самого коду.
- Анімації мають жити на GPU (
transform,opacity,requestAnimationFrame). - Спочатку профілювання, потім оптимізація.
Швидкий приклад
// Погано: читання геометрії в циклі змушує браузер перераховувати layout щоразу
items.forEach((el) => {
el.style.height = el.offsetHeight + 10 + 'px';
});
// Добре: спочатку читаємо все, потім пишемо все
const heights = items.map((el) => el.offsetHeight);
items.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px';
});Блокування головного потоку
JavaScript однопотоковий. Будь-яка важка операція блокує UI, рендеринг і обробку подій.
Приклади:
- великі цикли (
for,while,forEach) безsetTimeoutчиrequestIdleCallback; - парсинг або обробка великих JSON (
JSON.parse(hugeData)); - обчислювально важкі алгоритми (сортування, рекурсії, шифрування);
- синхронні запити (
XMLHttpRequestбез async); - великі зміни DOM в одному кадрі.
Як виправити:
- розбивати роботу на чанки (
setTimeout,requestAnimationFrame); - використовувати Web Workers для фонових обчислень;
- застосовувати debounce і throttle для подій.
Надлишкові рендери, робота з DOM і відмальовування
Операції з DOM найдорожчі з погляду продуктивності.
Проблеми:
- надто часті вставки та видалення елементів;
- перерахунок стилів і layout при кожній зміні;
- звернення до
offsetHeight,getComputedStyle,scrollTopвикликає reflow; - цикли, всередині яких змінюється DOM.
Як виправити:
- використовувати DocumentFragment, virtual DOM, batch-оновлення;
- кешувати посилання на елементи;
- мінімізувати
reflow, змінюючи клас одразу для групи елементів; - у React: мемоізація,
PureComponent,React.memo,useMemo,useCallback.
Слабка оптимізація відмальовування
Інтерфейс смикається, анімації лагають, FPS падає.
Причини:
- надто часта зміна
styleіtransform; - важкі тіні, фільтри,
border-radius; - невикористання GPU (CSS-анімації без
transform: translateZ(0)); - перерахунок layout у кожному кадрі анімації.
Рішення:
- виносити анімації на GPU;
- об'єднувати зміни DOM за один кадр;
- використовувати
will-change,transform,opacity; requestAnimationFrameзамість таймерів для анімацій.
Надлишкове споживання пам'яті
Витоки пам'яті призводять до того, що вкладка важчає, а інтерфейс зависає.
Причини:
- неочищені таймери (
setInterval,setTimeout); - «висячі» слухачі подій, які не знімаються при видаленні елемента;
- замикання, що утримують посилання на великі об'єкти;
- глобальні змінні, які ніколи не очищаються;
- кеші, які не очищаються.
Рішення:
- чистити таймери (
clearInterval,clearTimeout); - знімати обробники (
removeEventListener); - використовувати
WeakMapіWeakSet; - аналізувати через Chrome DevTools, вкладка Memory.
Неефективні цикли, колекції та алгоритми
Особливо помітно на великих масивах.
Типові помилки:
- вкладені цикли
O(n^2)без потреби; - використання
.map(),.filter(),.reduce()на великих масивах без оптимізації; - створення нових масивів чи об'єктів на кожній ітерації;
- часті виклики
Array.splice()таArray.shift(), це дорогі операції.
Що робити:
- використовувати доречніші структури:
Set,Map,WeakMap; - використовувати ітератори, генератори та
for...ofзамістьforEachна великих обсягах; - профілювати ділянки через
performance.now().
Неоптимальні структури даних
Приклад: сортування масиву на 100 000 елементів через sort() без компаратора або пошук через filter() замість Set.
Що робити:
- обирати структуру під задачу:
Setдля унікальності,Mapдля швидкого lookup; - уникати лінійного пошуку там, де можна використати хеш-пошук;
- використовувати бінарний пошук і кешування обчислень.
Мережа, бандл і профілювання
Навіть швидкий JS-код не врятує, якщо мережа перевантажена.
Проблеми:
- повторні запити без кешу;
- дублювальні виклики API на кожному рендері;
- завантаження величезних бандлів JS і CSS;
- немає стиснення gzip чи brotli;
- немає lazy loading.
Як виправити:
- кешування (HTTP-кеш, IndexedDB, Service Worker, мемоізація);
- об'єднання запитів (batching);
- React Query, SWR, заголовок
Cache-Control; - код-сплітинг і динамічний імпорт (
import()).
Важкий бандл
Чим більше JavaScript, тим довше завантаження і парсинг.
Причини:
- підключення зайвих бібліотек;
- дублювання залежностей;
- невикористання tree-shaking;
- inline JSON, великі іконки та зображення в коді.
Рішення:
- аналіз бандла (
webpack-bundle-analyzer,next build --analyze); - tree-shaking, code splitting, динамічний
import(); - використання CDN, HTTP/2, ESM-бандлів;
- мініфікація (Terser, SWC).
Відсутність профілювання та метрик
Найчастіша помилка, це оптимізація навмання.
Рішення:
- Chrome DevTools (Performance, Memory, Coverage);
console.time(),performance.mark()іperformance.measure();- Lighthouse, Web Vitals;
- Sentry Performance, New Relic, Datadog.
Стисле резюме:
| Категорія | Приклад проблеми | Як виправити |
|---|---|---|
| Блокування потоку | Довгий цикл | Розбити, Worker |
| DOM | Багато reflow | Batch update |
| Пам'ять | Витоки | WeakMap, очищення |
| Цикли | O(n^2) | Оптимізувати |
| Мережа | Повторні запити | Кешувати |
| Рендер | FPS < 60 | GPU-анімації |
| Бандл | 2 МБ JS | Tree-shaking |
| Алгоритми | Хибна структура | Підбір під задачу |
Типові помилки
- Оптимізувати без вимірювання. Без профілю в DevTools час іде на ділянки, що не впливають на результат.
- Вважати
asyncрятунком від блокування.async/awaitне переносить обчислення в інший потік: важкий цикл усерединіasync-функції так само морозить UI. Потрібен Web Worker. - Чергувати читання і запис DOM. Патерн «прочитав геометрію, записав стиль, знову прочитав» спричиняє layout thrashing. Спочатку всі читання, потім усі записи.
- Мемоізувати все підряд.
useMemoіuseCallbackсамі коштують пам'яті та порівнянь; на дешевих обчисленнях вони лише додають накладні витрати. - Забувати прибирати за собою. Слухач події чи
setInterval, створений при монтуванні й не знятий при розмонтуванні, це готовий витік. - Плутати розмір бандла з часом парсингу. Навіть закешований файл треба розпарсити й скомпілювати, тому обсяг коду важливий і на повторних візитах.
- Мікрооптимізувати цикли замість алгоритму. Заміна
forEachнаforне врятуєO(n^2), а перехід наMapврятує.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.