Ваш сайт тормозит? Время заняться профилированием JavaScript
Вы когда-нибудь чувствовали эту боль: сайт загружается, анимации плавные, но стоит пользователю начать активно взаимодействовать со страницей — всё встаёт колом. Кнопки не реагируют, скролл дёргается, а в консоли тишина. Знакомо? Скорее всего, проблема в производительности JavaScript. Этот язык — душа современного веба, но без должной оптимизации он может превратить ваш шедевр в медлительного монстра.
В этой статье мы не будем читать скучные лекции. Мы возьмём реальный код, разберём его по косточкам с помощью профилирования и покажем, как выжать максимум из каждого цикла. Готовы превратить свой сайт в болид Формулы-1? Погнали!
Почему JavaScript тормозит: корень зла
Прежде чем хвататься за инструменты, давайте разберёмся, что именно крадёт производительность. Чаще всего виноваты три вещи:
- Блокировка основного потока — JavaScript работает в одном потоке (Event Loop). Если вы запустили тяжёлую синхронную операцию, интерфейс замрёт до её завершения.
- Утечки памяти — забытые обработчики событий, замкнутые ссылки и глобальные переменные. Память растёт, сайт замедляется, а потом падает.
- Неэффективные алгоритмы — двойные циклы по огромным массивам, частые манипуляции с DOM, неоптимальные структуры данных.
Но хватит теории. Давайте посмотрим, как это выглядит на практике.
Инструменты профилирования: ваш детектор лжи
Chrome DevTools — швейцарский нож профилировщика
Откройте любую страницу, нажмите F12 и перейдите на вкладку Performance. Нажмите кнопку записи, выполните действие на странице (например, кликните по кнопке) и остановите запись. Вы увидите временную шкалу, где каждая секунда расписана по кадрам. Красные полоски — это ваши враги. Они означают, что браузер не успел отрисовать кадр за 16.6 мс (60 FPS).
// Пример: простая, но проблемная функция
function heavyCalculation() {
let result = 0;
for (let i = 0; i < 100000000; i++) {
result += Math.sqrt(i) * Math.sin(i);
}
return result;
}
document.getElementById('btn').addEventListener('click', () => {
const start = performance.now();
heavyCalculation(); // Интерфейс замрёт на 2-3 секунды!
console.log('Время:', performance.now() - start);
});
Запустите профилирование и кликните по кнопке. Вы увидите, что основной поток занят вызовом heavyCalculation на целую секунду. Это и есть bottleneck.
Memory — охота на утечки
Перейдите на вкладку Memory и сделайте снимок кучи. Затем выполните действие на странице несколько раз и сделайте второй снимок. Сравните их. Если количество объектов одного типа растёт без остановки — у вас утечка.
// Классическая утечка: забытый обработчик
function createButton() {
const btn = document.createElement('button');
btn.textContent = 'Click me';
document.body.appendChild(btn);
// Обработчик держит ссылку на btn, даже если btn удалён
btn.addEventListener('click', function onClick() {
console.log('Clicked');
});
// Удаляем кнопку, но обработчик остаётся в памяти
document.body.removeChild(btn);
}
Техники оптимизации: от теории к практике
1. Разгружаем основной поток с помощью async/await
Если у вас есть тяжёлая задача, не блокируйте поток. Используйте Web Workers или разбивайте задачу на микрозадачи. Но самый простой способ — setTimeout с нулевой задержкой или requestIdleCallback.
// Плохо: синхронная загрузка
function loadBigData() {
const data = [];
for (let i = 0; i < 1000000; i++) {
data.push({ id: i, value: Math.random() });
}
render(data);
}
// Хорошо: асинхронная обработка с разбивкой
async function loadBigDataAsync() {
const data = [];
const chunkSize = 10000;
for (let i = 0; i < 1000000; i += chunkSize) {
// Даём браузеру передышку
await new Promise(resolve => setTimeout(resolve, 0));
for (let j = i; j < i + chunkSize && j < 1000000; j++) {
data.push({ id: j, value: Math.random() });
}
}
render(data);
}
2. Debounce и Throttle — контроль частоты событий
События скролла, ресайза или ввода генерируются сотни раз в секунду. Если на каждое вешать тяжёлую функцию — goodbye performance. Используйте debounce (выполнить после паузы) или throttle (выполнять не чаще N раз в секунду).
// Реализация debounce
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// Использование
const handleSearch = debounce((query) => {
// Тяжёлый запрос к API
fetch(`/search?q=${query}`);
}, 300);
document.getElementById('searchInput').addEventListener('input', (e) => {
handleSearch(e.target.value);
});
3. Виртуализация списков — рендеринг без тормозов
Если вы выводите на страницу 10 000 элементов, DOM будет перегружен. Решение — виртуализация: рендерить только те элементы, которые видны в окне. Библиотеки вроде react-window или virtual-scroll делают это за вас, но принцип можно реализовать и вручную.
// Упрощённая виртуализация
class VirtualList {
constructor(container, items, itemHeight) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.container.style.overflow = 'auto';
this.container.style.height = '500px';
this.render();
this.container.addEventListener('scroll', () => this.render());
}
render() {
const scrollTop = this.container.scrollTop;
const startIndex = Math.floor(scrollTop / this.itemHeight);
const endIndex = startIndex + Math.ceil(this.container.clientHeight / this.itemHeight) + 1;
this.container.innerHTML = '';
for (let i = startIndex; i < Math.min(endIndex, this.items.length); i++) {
const div = document.createElement('div');
div.style.height = this.itemHeight + 'px';
div.textContent = this.items[i];
div.style.top = i * this.itemHeight + 'px';
div.style.position = 'absolute';
this.container.appendChild(div);
}
}
}
4. Избегаем дорогих операций в циклах
Каждая лишняя операция внутри цикла умножается на количество итераций. Выноси