Советы и рекомендации
Использование браузерных API

Осторожное использование браузерных API

В данной статье представлены распространенные ошибки разработчиков различных PBC, которые так или иначе приводят к снижению производительности работы пользователя и утечкам памяти:

⚠️

Статья дополняется

Производительность браузерных API: критические аспекты

С точки зрения производительности работа с браузерными API требует особого внимания к следующим механизмам:

Ключевые механизмы влияния на производительность:

  1. Event Loop и Main Thread
  • Браузер использует один главный поток для рендеринга и JavaScript
  • Долгие синхронные операции блокируют интерфейс
  • Web API работают асинхронно, но колбэки выполняются в main thread
  1. Browser Rendering Pipeline
  • Style → Layout → Paint → Composite
  • Некоторые API форсируют синхронный layout (layout thrashing)
  • Неоптимизированные анимации вызывают дорогие этапы рендеринга
  1. Garbage Collection
  • Утечки памяти заставляют GC работать чаще и дольше
  • Крупные объекты (изображения, данные) увеличивают паузы GC

5 самых распространенных ошибок неверного использования браузерных API

1. Layout Thrashing (Дробление макета)

Что происходит: Многократное чередование чтения и записи геометрических свойств, вызывающее принудительные синхронные пересчеты макета.

// ❌ НЕПРАВИЛЬНО - вызывает multiple reflows
const elements = document.querySelectorAll('.item');
for (let i = 0; i < elements.length; i++) {
    // Чтение (чтение геометрии) -> форсирует recalc
    const width = elements[i].offsetWidth;
 
    // Запись (изменение стиля) -> форсирует recalc
    elements[i].style.width = (width + 10) + 'px';
 
    // Снова чтение -> снова recalc!
    const height = elements[i].offsetHeight;
    elements[i].style.height = (height + 10) + 'px';
}

Исправление:

// ✅ ПРАВИЛЬНО - batch read/write
const elements = document.querySelectorAll('.item');
 
// Фаза чтения - все измерения сразу
const measurements = [];
for (let i = 0; i < elements.length; i++) {
    measurements.push({
        width: elements[i].offsetWidth,
        height: elements[i].offsetHeight
    });
}
 
// Фаза записи - все изменения сразу
for (let i = 0; i < elements.length; i++) {
    elements[i].style.width = (measurements[i].width + 10) + 'px';
    elements[i].style.height = (measurements[i].height + 10) + 'px';
}

API-виновники: offsetWidth, offsetHeight, getBoundingClientRect(), scrollTop, clientWidth, getComputedStyle()

2. Неуправляемые слушатели событий (Event Listener Leaks)

Что происходит: Слушатели событий не удаляются, создавая утечки памяти и выполняя ненужную работу.

// ❌ НЕПРАВИЛЬНО - утечка памяти
class Component {
    constructor() {
        this.handleClick = this.handleClick.bind(this);
        document.addEventListener('click', this.handleClick);
    }
 
    handleClick() {
        console.log('Clicked');
    }
 
    // remove() вызывается, но слушатель не удаляется!
    remove() {
        // Забыли removeEventListener!
    }
}
 
// Использование:
const comp = new Component();
comp.remove(); // Утечка - слушатель всё еще висит

Исправление:

// ✅ ПРАВИЛЬНО - явное управление слушателями
class Component {
    constructor() {
        this.handleClick = this.handleClick.bind(this);
        document.addEventListener('click', this.handleClick);
    }
 
    handleClick() {
        console.log('Clicked');
    }
 
    destroy() {
        // Явное удаление
        document.removeEventListener('click', this.handleClick);
    }
}
 
// Или с использованием AbortController (современный подход)
class ModernComponent {
    constructor() {
        this.controller = new AbortController();
        document.addEventListener('click', this.handleClick.bind(this), {
            signal: this.controller.signal
        });
    }
 
    destroy() {
        this.controller.abort(); // Автоматически удаляет все слушатели
    }
}

3. Неоптимизированные наблюдатели (Observer Overuse)

Что происходит: Создание множества наблюдателей без оптимизации, выполняющих тяжелые операции.

// ❌ НЕПРАВИЛЬНО - дорогой MutationObserver
const observer = new MutationObserver((mutations) => {
    mutations.forEach(mutation => {
        // Выполняется на КАЖДОЕ микро-изменение
        heavyDOMOperation();
        updateAnalytics();
        recalculateLayout();
    });
});
 
observer.observe(document.body, {
    childList: true,
    subtree: true,
    attributes: true,
    characterData: true // Слишком широкое наблюдение!
});

Исправление:

// ✅ ПРАВИЛЬНО - дебаунсинг и целевое наблюдение
function debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
        const later = () => {
            clearTimeout(timeout);
            func(...args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    };
}
 
const optimizedOperation = debounce(() => {
    heavyDOMOperation();
    updateAnalytics();
}, 100); // Объединяем изменения
 
const observer = new MutationObserver(optimizedOperation);
 
// Наблюдаем только за нужной областью
const targetNode = document.getElementById('specific-area');
observer.observe(targetNode, {
    childList: true, // Только то, что действительно нужно
    attributes: false,
    subtree: false
});

4. Неэффективная работа с DOM

Что происходит: Множественные точечные изменения DOM вместо batch-операций.

// ❌ НЕПРАВИЛЬНО - N операций перерисовки
function addItems(items) {
    const container = document.getElementById('container');
 
    items.forEach(item => {
        const div = document.createElement('div');
        div.textContent = item.name;
        div.className = 'item';
        container.appendChild(div); // Каждый appendChild - потенциальная перерисовка
    });
}

Исправление:

// ✅ ПРАВИЛЬНО - DocumentFragment для batch-вставки
function addItems(items) {
    const container = document.getElementById('container');
    const fragment = document.createDocumentFragment();
 
    items.forEach(item => {
        const div = document.createElement('div');
        div.textContent = item.name;
        div.className = 'item';
        fragment.appendChild(div);
    });
 
    // ОДНА операция вставки - ОДНА перерисовка
    container.appendChild(fragment);
}
 
// Или через innerHTML (осторожно с XSS!)
function addItemsFast(items) {
    const container = document.getElementById('container');
    const html = items.map(item =>
        `<div class="item">${escapeHTML(item.name)}</div>`
    ).join('');
 
    container.innerHTML += html; // Одна операция
}

5. Злоупотребление таймерами и анимациями

Что происходит: Использование setInterval вместо requestAnimationFrame и создание избыточных таймеров.

// ❌ НЕПРАВИЛЬНО - setInterval для анимаций
function animateElement() {
    const element = document.getElementById('animated');
    let position = 0;
 
    setInterval(() => {
        position += 2;
        element.style.transform = `translateX(${position}px)`;
        // Не синхронизировано с кадрами браузера
        // Работает даже когда страница невидима
    }, 16); // ~60fps, но нет гарантии
}

Исправление:

// ✅ ПРАВИЛЬНО - requestAnimationFrame
function animateElement() {
    const element = document.getElementById('animated');
    let position = 0;
    let lastTime = null;
 
    function step(timestamp) {
        if (!lastTime) lastTime = timestamp;
        const delta = timestamp - lastTime;
 
        // Плавная анимация с учетом времени
        position += (delta / 16) * 2; // Нормализация к 60fps
        element.style.transform = `translateX(${position}px)`;
 
        lastTime = timestamp;
        if (position < 1000) {
            requestAnimationFrame(step); // Синхронизация с рендерингом
        }
    }
 
    requestAnimationFrame(step);
}
 
// Для периодических задач - современный подход
const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            // Запускаем анимацию только когда элемент видим
            startAnimation();
        }
    });
});

Диагностика

Инструменты для обнаружения проблем:

// Измерение производительности
function measurePerformance(operationName, operation) {
    const startTime = performance.now();
    operation();
    const endTime = performance.now();
    console.log(`${operationName} took ${endTime - startTime}ms`);
}
 
// Мониторинг FPS
let frameCount = 0;
let lastTime = performance.now();
 
function checkFPS() {
    frameCount++;
    const currentTime = performance.now();
    if (currentTime - lastTime >= 1000) {
        console.log(`FPS: ${frameCount}`);
        frameCount = 0;
        lastTime = currentTime;
    }
    requestAnimationFrame(checkFPS);
}

Ключевые метрики для наблюдения:

  • FPS (должен быть стабильно ~60)
  • Memory Usage в Chrome DevTools
  • Layout/paint times в Performance tab
  • Long Tasks (>50ms блокирующие операции)