Skip to main content

Вузькі місця в JS-коді

Вузьке місце, це ділянка коду, яка обмежує продуктивність усього застосунку, і в JavaScript таких ділянок вісім типових: головний потік, DOM, пам'ять, цикли, мережа, відмальовування, бандл і алгоритми. Дев'яте, найпоширеніше, це відсутність профілювання, коли оптимізують навмання.

Теорія

TL;DR

  • JavaScript однопотоковий: будь-яка важка синхронна операція блокує UI, рендеринг і обробку подій.
  • Операції з DOM найдорожчі; читання геометрії всередині циклу спричиняє reflow.
  • Витоки пам'яті дають таймери, слухачі подій, замикання та глобальні змінні.
  • На великих масивах вирішують алгоритм і структура даних: Set і Map замість лінійного пошуку.
  • Мережа і розмір бандла часто важать більше, ніж швидкість самого коду.
  • Анімації мають жити на GPU (transform, opacity, requestAnimationFrame).
  • Спочатку профілювання, потім оптимізація.

Швидкий приклад

javascript
// Погано: читання геометрії в циклі змушує браузер перераховувати 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Багато reflowBatch update
Пам'ятьВитокиWeakMap, очищення
ЦиклиO(n^2)Оптимізувати
МережаПовторні запитиКешувати
РендерFPS < 60GPU-анімації
Бандл2 МБ JSTree-shaking
АлгоритмиХибна структураПідбір під задачу

Типові помилки

  • Оптимізувати без вимірювання. Без профілю в DevTools час іде на ділянки, що не впливають на результат.
  • Вважати async рятунком від блокування. async/await не переносить обчислення в інший потік: важкий цикл усередині async-функції так само морозить UI. Потрібен Web Worker.
  • Чергувати читання і запис DOM. Патерн «прочитав геометрію, записав стиль, знову прочитав» спричиняє layout thrashing. Спочатку всі читання, потім усі записи.
  • Мемоізувати все підряд. useMemo і useCallback самі коштують пам'яті та порівнянь; на дешевих обчисленнях вони лише додають накладні витрати.
  • Забувати прибирати за собою. Слухач події чи setInterval, створений при монтуванні й не знятий при розмонтуванні, це готовий витік.
  • Плутати розмір бандла з часом парсингу. Навіть закешований файл треба розпарсити й скомпілювати, тому обсяг коду важливий і на повторних візитах.
  • Мікрооптимізувати цикли замість алгоритму. Заміна forEach на for не врятує O(n^2), а перехід на Map врятує.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.