Продуктивність JavaScript полягає у меншій кількості роботи, а не у меншій кількості рядків коду.
Робота з оптимізацією полягає у виборі тих обчислень, які не потрібно виконувати, чесному вимірюванні показників Core Web Vitals та виправленні найбільш проблемних частин коду, а не у скороченні кількості рядків коду заради цього.
Джерела: JavaScript Performance Isn’t About Writing Fewer Lines
Найшвидший JavaScript — це часто той, який ви ніколи не виконуєте
Робота над продуктивністю часто розглядається як створення більш ефективних циклів. У браузерах найбільші покращення зазвичай досягаються шляхом виконання меншої кількості операцій: менше викликів обробників, менше змін у DOM, менше мережевих запитів, менше часу використання основної смуги обробки. Уявіть поле пошуку, яке знову фільтрує результати після кожного натискання клавіші:
input.addEventListener("input", () => {
renderResults(filterUsers(input.value));
});
Введення імені призводить до серії проміжних запитів:
m
ma
mah
maha
mahad
Більшість цих проміжних результатів викидаються. Механізм затримки дозволяє скоротити цей потік, щоб виконувався лише один остаточний запит:
const debounce = (fn, delay = 250) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, delay);
};
};
const search = debounce((query) => {
renderResults(filterUsers(query));
});
Тіло функції не стало розумнішим; змінився графік виконання операцій, і саме тут зазвичай вирішується питання продуктивності браузера. Цей підхід — уникнення, відкладання, об’єднання — кращий за мікрооптимізацію функції, яка не повинна виконуватися так часто.
Проміжні запити не є безкоштовними. Кожен натиск клавіши може призвести до виділення масивів, обробки великих списків, оновлення елементів React чи DOM та планування відображення змін. На телефонах середнього класу ці витрати призводять до помітних затримок у роботі. Технологія debouncing — це лише один із засобів об’єднання операцій; обмеження кількості обробок прокрутки, об’єднання спостерігачів за зміною розміру та пропуск виконання операцій під час приховування вкладки є аналогічними підходами: виконувати операції лише тоді, коли результат все ще має значення.
Не оптимізуйте функцію. Оптимізуйте саму роботу.
Обробка кожного елемента за допомогою дорогої функції трансформації здається невинною:
items.map(item => transform(item));
Запитайте себе, чому взагалі виконується ця трансформація, для якого екрану та як часто змінюються результати. Якщо один і той самий користувач постійно отримує однаковий формат даних, використовуйте мемоізацію:
const cache = new Map();
function getFormattedUser(id, user) {
if (cache.has(id)) {
return cache.get(id);
}
const result = formatUser(user);
cache.set(id, result);
return result;
}
Зберігання в кеші ідентичних даних усуває необхідність багаторазової роботи процесора без необхідності переписування самої функції трансформації. Такий підхід також застосовується до сортування, фільтрації та створення моделей відображення: обчислюйте дані тоді, коли вони змінюються, а не щоразу під час відображення.
Мемоізація — це не безкоштовний рішення. Неконтрольовано зростаючі кеш-структури призводять до проблем через нестачу пам’яті. Використовуйте стабільні ідентифікатори як ключі, обмежуйте розмір кешу та скасовуйте його даний, коли змінюються вихідні записи. Краще створювати моделі відображення один раз під час зміни стану, ніж перераховувати складні структури в кожному компоненті.
Ваш JavaScript може бути швидким, навіть якщо інтерфейс працює повільно
Додавання елементів у DOM у межах циклу змушує постійно переробляти макет:
for (const item of items) {
container.innerHTML += createCard(item);
}
Краще створити рядок один раз та присвоїти його один раз:
const html = items
.map(createCard)
.join("");
container.innerHTML = html;
Робота з DOM має наслідки, які виходять за межі самої JavaScript-інструкції, що її спричинила. Перерахунок стилів та макету може значно займати час навіть тоді, коли журнал виконання скрипту виглядає „швидким“.
Фрагменти документа та створення DOM офлайн допомагають, коли потрібно створити багато вузлів. Фреймворки автоматично об’єднують оновлення, якщо ви цього дозволяєте; протидія фреймворку за допомогою прямих циклів innerHTML у критичних частинах коду знову призводить до витрат, які ці бібліотеки існують для того, щоб приховати. Краще використовувати оновлення списків за ключами, щоб незмінні рядки не знищувалися та не створювалися заново.
Макет може стати дорожчим за вашу функцію
Читання властивостей макету змушує браузер обчислювати геометрію:
const height = element.offsetHeight;
Запис стилів та їх подальше читання спричиняють зайві операції:
element.style.height = `${height + 20}px`;
const newHeight = element.offsetHeight;
Спочатку читання пакетами, потім запис пакетами. Уникайте поєднання процедур вимірювання та змін у критичних частинах коду, таких як обробники прокрутки та анімації.
Примусове синхронне формування інтерфейсу є поширеною проблемою у користувацьких підказках, фіксованих заголовках та HTML-елементах, вирівняних за канвасом. Якщо бібліотека мусить вимірювати щось, робіть це один раз на кадр та зберігайте результати в кеші. requestAnimationFrame є кращим способом планування, ніж прямі слухачі подій прокрутки, які читають геометрію щоразу після кожної події.
Не робіть основну схему виконання своїм особистим списком завдань
Важкі операції перетворення з великих масивів блокують прийом введення та відображення контенту:
function processLargeDataset(data) {
return data.map(expensiveTransformation);
}
Запитайте себе, чи справді ця робота мусить виконуватися на основній схемі виконання. Робочі процеси дозволяють відволікати завдання, що потребують великих обчислень, від інтерфейсу користувача:
const worker = new Worker("processor.js");
worker.postMessage(largeDataset);
worker.onmessage = (event) => {
updateUI(event.data);
};
Пам’ятайте про витрати на обмін даними; свідомо передавайте або структуровано копіюйте великі об’єми даних.
Робітники змінюють власника CPU, а не складність продукту. Серіалізація може скасувати переваги при виконанні дуже коротких завдань. Використовуйте її для парсингу, шифрування, обробки зображень чи великих агрегацій — а не для мікозавдань, які виконуються швидше, ніж за один цикл postMessage. SharedArrayBuffer та об’єкти, які можна передати, допомагають, коли об’єм даних великий та це дозволяють заголовки безпеки.
Lazy loading — це функція продуктивності
Не завантажуйте щось, поки вам це не потрібно. Динамічний імпорт відкладає виконання коду для рідко використовуваних маршрутів:
async function openAnalytics() {
const { Analytics } = await import("./analytics.js");
const analytics = new Analytics();
analytics.open();
}
Користувачі, які ніколи не відкривають розділ аналітики, ніколи не завантажують цей модуль. Поєднуйте його з розділенням коду на рівні маршрутів, щоб початкова навігація залишалась легкою.
Розділення за маршрутами має відповідати шляхам користувача, а не лише межам папок. Попередньо завантажуйте ймовірні наступні маршрути під час наведення курсору або у стані очікування, щоб переваги затриманого завантаження не перетворювалися на затримку під час клікання. Слідкуйте за дублюваними залежностями між частинами коду, які після розділення збільшують загальний об’єм.
Розмір пакету — це не все
Менші пакети допомагають, але крихітний пакет, який все одно блокує основну схему виконання або здійснює послідовні запити, може бути гіршим за трохи більший пакет, який спочатку передає критичні елементи інтерфейсу. Вимірюйте час до початку взаємодії та затримки під час неї, а не лише кілобайти.
Продуктивність мережі — це також продуктивність JavaScript
Послідовні запити з використанням await ускладнюють працю системи через затримки:
const user = await fetch("/api/user");
const posts = await fetch("/api/posts");
const notifications = await fetch("/api/notifications");
Незалежні запити мають виконуватися одночасно:
const [user, posts, notifications] = await Promise.all([
fetch("/api/user"),
fetch("/api/posts"),
fetch("/api/notifications")
]);
Послідовні запити можуть „ховатися“ всередині функцій async так само легко, як і всередині неправильно розташованих тегів скриптів.
Найдорожчим запитом є той, якого вам не потрібно
На початкових екранах часто потрібен невеликий основний набір даних, тоді як додаток активно запитує додаткові панелі. Краще спочатку завантажити основну панель керування, а решту відкласти:
await loadDashboard();
requestIdleCallback(() => {
loadSecondaryData();
});
Не кожне завдання заслуговує на однаковий пріоритет. Вільні виклики, запити нижчого пріоритету та чіткі дії користувача — це інструменти для відповідності роботи ступеню терміновості.
Кешування може бути важливішим за мікрооптимізації
Кеші існують на рівнях HTTP, сервісних працівників, пам’яті та похідних станів. Правильний ключ кешу та політика його анулювання можуть зменшити затримки більше, ніж переписування алгоритму сортування. Документуйте правила свіжості, щоб застарілий інтерфейс не став прихованою помилкою продукту.
Заголовки кешування HTTP та ETags зменшують кількість повторних завантажень; кеш у пам’яті зменшує кількість повторного парсингу JSON; буфери користувацького інтерфейсу зменшують кількість повторних обчислень. Кожен рівень потребує чіткого власника для скасування даних. Без такого власника команди або завантажують зайву інформацію „на всякий випадок“, або відображають застарілі дані, що підриває довіру.
Віртуалізація повністю змінює проблему
Швидке відображення тисяч рядків зазнає невдачі:
rows.forEach(renderRow);
Віртуальні списки відображають лише те, що видно. Складність переходить від „зробити кожен рядок дешевшим“ до „не створювати рядки поза екраном“, що зазвичай є правильним компромісом.
Бібліотеки для роботи з вікнами жертвують простотою заради правильного скроллювання: необхідно стежити за змінною висотою рядків, керуванням фокусом та стійкими колонками. Проте для великих наборів даних вони краще справляються, ніж складні трюки CSS. Поєднуйте їх із пагінацією на сервері, щоб клієнт ніколи не зберігав мільйони записів лише для їх віртуалізації.
Інструменти для оцінки продуктивності кращі за здогадки
Коли щось працює повільно, виміряйте це. Chrome DevTools відображає стан мережевих операцій, тривалі завдання на основній смузі виконання, проблеми з макетуванням та зростання обсягу пам’яті. Здогадки призводять до оптимізації неправильної функції.
Просте дослідження продуктивності
Слідуйте переліку перевірок, замість того щоб поспішати з непродуманими мікрооптимізаціями:
- Чи повільна мережа?
- Чи завантажується забагато JavaScript?
- Чи блокує JavaScript основну смугу виконання?
- Чи є процес відображення складним?
- Чи ми постійно виконуємо однакові завдання?
- Чи зростає обсяг пам’яті?
- Чи виникає проблема лише при великих обсягах даних?
Відповідайте на основі даних, а не здогадок.
Найпоширеніша пастка продуктивності
Команди витрачають тижні на оптимізацію коду, який не є причиною проблеми — намагаючись скоротити час виконання операцій на мікросекунди, тоді як повільна обробка даних тривалістю чотири секунди або необмежений список елементів погіршують користувацький досвід. Спочатку потрібно визначити причину уповільнення.
Сучасний JavaScript надає кращі інструменти
Використовуйте функції платформи, які зменшують потребу у власних рішеннях:
structuredClone(data);
const result = await Promise.allSettled(tasks);
const module = await import("./feature.js");
const controller = new AbortController();
fetch("/api/search", {
signal: controller.signal
});
У користувацькому інтерфейсі пошуку часто потрібно можливість скасування активних запитів, щоб застарілі відповіді не впливали на швидкість роботи. Один ефективний підхід:
javascript
javascript performance
let controller;
async function search(query) {
controller?.abort();
controller = new AbortController();
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(query)}`,
{ signal: controller.signal }
);
return await response.json();
} catch (error) {
if (error.name !== "AbortError") {
throw error;
}
}
}
AbortController, динамічний імпорт та Promise.allSettled замінюють багато ручно створених механізмів керування чергами та флагами.
Продуктивність має продуктовий вимір
Перевага у 100 мс не є автоматично значущою; важлива локація. Зменшення часу роботи допоміжного процесу з 2 мс наполовину рідко змінює сприйняття. Проте скорочення часу першого завантаження з чотирьох секунд до двох секунд справді має значення. Миттєва зворотна відгук про набір тексту може бути важливішою за трохи швидший фоновий процес. Плавне прокручування може мати більше значення, ніж скорочення часу обробки даних через API. Продуктивність — це той досвід, який створює система, а не елегантність мікротесту.
Припиніть переслідувати цифри, які не відображають реальних користувачів
Синтетичні тести допомагають, але швидкий ноутбук у локальній мережі з примітивними умовами не є реальним середовищем експлуатації. Тестуйте репрезентативні пристрої, мережі, розміри даних та шаблони взаємодії — інакше ви оптимізуєте вигаданий додаток.
Як показники лабораторних тестів, так і показники роботи в реальних умовах мають значення. Інструмент Lighthouse виявляє регресії під час автоматизованих тестувань; моніторинг справжніх користувачів виявляє відхилення через особливості пристроїв та географічне розташування. Коли їхні дані суперечать одне одному, для визначення пріоритетів слід довіряти даним з реальних умов, а для відтворення проблем — даним з лабораторних тестів. Бюджети на продуктивність, пов’язані з показниками LCP, INP та тривалими завданнями, допомагають у конкретних обговореннях.
Мислення про продуктивність
Краще використовувати таку послідовність запитань: Яку роботу можна уникнути? Що може статися пізніше? Що може траплятися рідше? Що може відбуватися в іншому місці? Що насправді має значення для користувача? Відповідями можуть бути використання механізмів debounce, кешування, розділення коду, віртуалізація, використання worker-процесів, зміни API чи виправлення запитів — або ж видалення невикористовуваних функцій.
Заключні міркування
Найважливішим уроком щодо продуктивності JavaScript є те, що це не просто окремий API. Швидкий код — це не завжди код, який виконується швидко. Іноді він взагалі не виконується, виконується пізніше, виконується один раз замість п’ятдесяти, залишає основну смугу виконання або припиняє запитувати інші системи про зайву роботу. Перестаньте вважати „меншу кількість рядків“ показником швидкості. Намагайтеся зменшити кількість зайвої роботи там, де це відчувають користувачі.
Видалення невикористовуваних елементів — це спосіб покращення продуктивності. Кожна неактивна бібліотека діаграм, автовідтворення медіаконтенту чи спекулятивне попереднє завантаження конкурують за пропускну здатність та час основної смуги виконання. Відділи продукту та інженерії повинні переглянути, які функції все ще варто підтримувати. Найшвидший JavaScript — це той, який ви ніколи не завантажуєте, ніколи не парсуєте та ніколи не виконуєте.
Обробка подій у реальному часі: пошук, автозбереження та зміна розміру
Пошук в реальному часі є прикладом для навчання, але та сама дисципліна графіка застосовується й в інших ситуаціях. Функція автозбереження, яка записує дані при кожному натисканні клавіші, може перевантажити сервер; краще чекати на паузу під час введення тексту, що з точки зору користувача часто є ідентичним. Обробники подій зміни розміру вікна та прокрутки, які перераховують макет після кожної події, є класичними джерелами затримок; обмеження кількості таких операцій до кадрів анімації чи фіксованого інтервалу допомагає зберегти реактивність основної смуги виконання. Основна ідея полягає не у додаванні бібліотеки lodash, а у розділенні наміру (користувач хоче отримати відфільтровані результати) від графіка (частота, з якою ми можемо виконувати обчислення).
Під час використання механізму дебаунсингу обирайте часові інтервали відповідно до сприйняття користувача: 150–300 мс підходять для пошуку, який здається миттєвим після паузи; довші інтервали краще підходять для дорогих аналітичних засобів. Скасовуйте поточну роботу, коли новий запит замінює старий, щоб пізні відповіді не призводили до використання застарілих даних. Цей крок скасування є настільки ж важливим, як і таймер.
Стратегії кешування за межами Map
Map у оперативній пам’яті, ключована за id, — це лише один рівень кешування. HTTP-кеші дотримуються параметрів Cache-Control та ETags, тож повторні переходи пропускають передачу даних. Сервісні роботи можуть кешувати ресурси та результати API-запитів з використанням чітких правил очищення кешу. Склади додатків можуть зберігати нормалізовані дані, щоб списки та детальні відображення використовували один і той самий запис. Виведені моделі даних — відсортовані чи відфільтровані версії — слід перераховувати при зміні вхідних даних, а не під час кожного оновлення пов’язаного елемента інтерфейсу.
Скасування даних — це складна частина. Параметр часу життя є простим та однозначним. Скасування на основі подій („користувач оновив профіль“) є точним, коли ви контролюєте процес запису даних. Гібридні підходи використовують короткий час життя даних разом із прямими сигналами про помилки під час змін. Опишіть, який шар відповідає за які дані, щоб два кеш-системи не почали працювати в розрізі одна з одною.
Групування операцій DOM та взаємодія фреймворків
Прямі цикли типу innerHTML += є антипатерном навіть поза межами фреймворків. Використання документних фрагментів, методу replaceChildren чи створення рядка лише один раз значно зменшує навантаження на розмітку. У React, Vue чи Svelte краще використовувати оновлення стану, які дозволяють бібліотекам синхронізувати списки за ключами. Вимірювання значення offsetHeight під час процесу відрендерування знову спричиняє примусову переробку розмітки; краще виконувати ці вимірювання під час ефектів розмітки або за допомогою спостерігачів, запланованих після процесу малювання.
CSS containment та content-visibility можуть обмежити роботу з областями за межами екрана, коли віртуалізація є занадто обтяжливою. Вони не є чарівним рішенням: тестуйте їх у браузерах, які ви підтримуєте, та стежте за наслідками для доступності, коли контент пропускається.
Бюджетування основної смуги обробки
Намагайтеся тримати обробники подій у межах бюджету кадру — приблизно 16 мс при 60 Гц, менше, якщо ви ділите кадр з іншими завданнями. Розділяйте великі операції трансформації на частини за допомогою requestIdleCallback або асинхронних генераторів, які періодично виробляють дані. Для обробки JSON розміром у кілька мегабайт використовуйте стрімингові парсери або окремі працівники. Аналізуйте виконання завдань за допомогою панелі Performance та функції відстеження довгих завдань; якщо таймінг перевищує 50 мс, користувачі на повільніших пристроях це відчують.
Робітники потребують чіткого визначення власника: чисті функції, залежні від CPU, — так; все, що потребує DOM, — ні. Витрати на структуроване клонування зростають з розміром графа об’єктів; коли це можливо, використовуйте ArrayBuffers. Витрати на старт робітників мають значення для одноразових дрібних завдань — іноді краще використовувати підхід з роботою на основному потоці.
Мережеві послідовності та пріоритети
await у послідовності є фактором, який приховано збільшує затримку. Незалежні запити слід виконувати за допомогою Promise.all. Залежні запити повинні передавати лише необхідні ідентифікатори, а не всю інформацію, щоб зменшити обсяг подальших відповідей. Мультиплексування HTTP/2 допомагає, але не усуває час обробки на сервері; скоротіть кількість поїздок за допомогою API, які при можливості повертають усе необхідне на екран одразу.
Пріоритет має значення: спочатку контент-герой, потім додаткові віджети. Підказки щодо пріоритету функції fetch, попереднє завантаження посилань та калебури при бездіяльності виражають цю мету. Використовуйте попереднє завантаження при наведенні миші для ймовірних переходів, якщо це не коштує занадто багато ресурсів. Уникайте спекулятивного попереднього завантаження великих даних, підтверджених автентифікацією, які користувач може ніколи не відкрити.
Віртуалізація та формат даних
Віртуальні списки передбачають приблизно однакову висоту рядків або ретельне зберігання даних про їхню висоту. Рядки з різною висотою вимагають більше обробки, але все одно кращі за відображення десяти тисяч елементів DOM. Поєднуйте клієнтську віртуалізацію з серверною пагінацією або API курсора, щоб обсяг пам’яті залишався стабільним. Функція нескінченного прокручування повинна призупинятися, коли вкладка прихована, та повідомляти про статус завантаження для технологій допомоги користувачеві.
Культура вимірювання
Інструменти в лабораторії виявляють регресії під час автоматизованих тестувань; інструменти в реальних умовах виявляють особливості роботи пристроїв. Якщо показник INP низький, перевіряйте обробники подій та тривалі завдання, а не лише мережу. Якщо показник LCP низький, перевіряйте пріоритезацію головного зображення та час відгуку сервера. Якщо показник CLS низький, виділяйте місце для контенту, який відображається пізніше. Пов’язуйте кожен показник із відповідальною особою та бюджетом, щоб робота над продуктивністю не перетворювалася на безкінечне вдосконалення нерелевантних елементів.
Синтетичні мікротести чистих функцій рідко дозволяють передбачити користувацький досвід. Краще використовувати записи реальних взаємодій: введення тексту у пошук, відкриття модального вікна, прокручування щільної таблиці. Зберігайте ці записи як докази під час подання звітів про баги, щоб дискусії залишалися конкретними.
Компроміси у продукті
Не кожен мілісекунда є однаковою. Миттєва зворотна відгук на набір тексту чи перетягування має більшу цінність, ніж відмова від щотижневого звіту на тлі. Іноді найкращою оптимізацією є видалення функції, яка споживає пропускну здатність та увагу без значної користі. Продуктивність — це рішення, прийняте на рівні продукту та реалізоване через інженерну роботу.
Записати цей підхід на наклейку
Уникайте роботи. Відкладайте її. Об’єднуйте завдання. Відводьте роботу з критичного шляху. Вимірюйте те, що відчувають користувачі. Ці п’ять рядків кращі, ніж сотня передчасних мікрооптимізацій. Продуктивність JavaScript полягає не стільки у написанні меншої кількості коду, скільки у забезпеченні того, щоб код, який залишається, справді мав місце на пристрої користувача.