Производительность JavaScript: как ускорить сайт с помощью профилирования

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Ваш сайт тормозит? Время заняться профилированием 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. Избегаем дорогих операций в циклах

Каждая лишняя операция внутри цикла умножается на количество итераций. Выноси

Блоги

Рекомендаций по книгам