Осторожное использование браузерных API
В данной статье представлены распространенные ошибки разработчиков различных PBC, которые так или иначе приводят к снижению производительности работы пользователя и утечкам памяти:
Статья дополняется
Производительность браузерных API: критические аспекты
С точки зрения производительности работа с браузерными API требует особого внимания к следующим механизмам:
Ключевые механизмы влияния на производительность:
- Event Loop и Main Thread
- Браузер использует один главный поток для рендеринга и JavaScript
- Долгие синхронные операции блокируют интерфейс
- Web API работают асинхронно, но колбэки выполняются в main thread
- Browser Rendering Pipeline
- Style → Layout → Paint → Composite
- Некоторые API форсируют синхронный layout (layout thrashing)
- Неоптимизированные анимации вызывают дорогие этапы рендеринга
- 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 блокирующие операции)