Избежание подводных камней события resize в JavaScript

Событие resize окна кажется простым — до тех пор, пока ваше приложение не начнет тормозить. Если вы когда-либо задавались вопросом, почему ваш адаптивный JavaScript-код вызывает проблемы с производительностью, скорее всего, вы столкнулись с одной из самых распространенных ловушек события resize в JavaScript, которые преследуют frontend-приложения.
В этой статье рассматривается, почему события resize могут парализовать производительность, как их оптимизировать с помощью throttling и debouncing, и когда полностью обойти JavaScript, используя современные CSS-решения и API.
Ключевые выводы
- События resize срабатывают сотни раз в секунду во время изменения размера окна, вызывая серьезные проблемы с производительностью
- Throttling и debouncing являются важными техниками для ограничения частоты выполнения обработчиков событий
- Современные альтернативы, такие как ResizeObserver API и CSS container queries, часто обеспечивают лучшую производительность
- Правильная очистка слушателей событий предотвращает утечки памяти в production-приложениях
Скрытая цена производительности событий resize в JavaScript
Когда пользователь перетаскивает окно браузера для изменения его размера, событие resize срабатывает не один раз — оно срабатывает непрерывно. Простое перетаскивание окна может вызвать обработчик события сотни раз в секунду, заполняя основной поток вызовами функций.
// Это логирование происходит сотни раз за одно действие изменения размера
window.addEventListener('resize', () => {
console.log(`Window size: ${window.innerWidth}x${window.innerHeight}`);
});
Каждое выполнение события блокирует основной поток, препятствуя браузеру обрабатывать другие критически важные задачи, такие как обновления рендеринга или обработка пользовательских взаимодействий. Результат? Прерывистые анимации, неотзывчивые интерфейсы и разочарованные пользователи.
Почему подводные камни события resize в JavaScript важны для производительности
Чрезмерное срабатывание событий и блокировка основного потока
Событие resize срабатывает при каждом изменении размера на пиксель во время изменения размера окна. Если ваш обработчик выполняет сложные вычисления или манипуляции с DOM, вы фактически запускаете дорогостоящие операции сотни раз в секунду.
Рассмотрим этот распространенный паттерн:
window.addEventListener('resize', () => {
const elements = document.querySelectorAll('.responsive-element');
elements.forEach(el => {
// Сложные вычисления для каждого элемента
el.style.width = calculateOptimalWidth(el);
});
});
Этот код пересчитывает и обновляет несколько элементов непрерывно во время изменения размера, создавая узкое место производительности.
Layout Thrashing: Тихий убийца производительности
Самая коварная ловушка события resize в JavaScript возникает, когда вы считываете размеры элемента и сразу же записываете новые стили. Этот паттерн, называемый layout thrashing, заставляет браузер синхронно пересчитывать макеты:
window.addEventListener('resize', () => {
// Принуждает к вычислению макета
const width = element.offsetWidth;
// Делает макет недействительным
element.style.width = (width * 0.8) + 'px';
// Принуждает к еще одному вычислению макета
const height = element.offsetHeight;
});
Каждое чтение размера вызывает полный пересчет макета, умноженный на сотни событий resize.
Основные техники оптимизации: Throttling и Debouncing
Реализация Throttle для событий resize
Throttling ограничивает частоту выполнения обработчика resize, обычно до 60fps (каждые 16мс) или реже:
function throttle(func, delay) {
let lastExecTime = 0;
let timeoutId;
return function (...args) {
const currentTime = Date.now();
if (currentTime - lastExecTime > delay) {
func.apply(this, args);
lastExecTime = currentTime;
} else {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
lastExecTime = Date.now();
}, delay - (currentTime - lastExecTime));
}
};
}
const throttledResize = throttle(() => {
// Это выполняется максимум раз в 100мс
updateLayout();
}, 100);
window.addEventListener('resize', throttledResize);
Discover how at OpenReplay.com.
Debouncing для завершенных действий изменения размера
Debouncing ждет, пока изменение размера не прекратится, перед выполнением:
function debounce(func, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
const debouncedResize = debounce(() => {
// Выполняется через 250мс после прекращения изменения размера
recalculateLayout();
}, 250);
window.addEventListener('resize', debouncedResize);
// Не забывайте об очистке
// window.removeEventListener('resize', debouncedResize);
Современные альтернативы событиям resize окна
CSS-первые решения: Media Queries и Container Queries
Часто вы можете полностью избежать подводных камней событий resize в JavaScript, используя CSS:
/* Media queries для адаптивности на основе окна */
@media (max-width: 768px) {
.sidebar { display: none; }
}
/* Container queries для адаптивности на основе компонентов */
.card-container {
container-type: inline-size;
}
@container (min-width: 400px) {
.card { grid-template-columns: 1fr 1fr; }
}
Для обнаружения media query на основе JavaScript используйте matchMedia:
const mediaQuery = window.matchMedia('(max-width: 768px)');
mediaQuery.addEventListener('change', (e) => {
// Срабатывает только при пересечении точки останова
if (e.matches) {
showMobileMenu();
}
});
ResizeObserver API: Производительная альтернатива
ResizeObserver обеспечивает мониторинг размера конкретных элементов без потерь производительности:
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
// Браузер предоставляет размер — никакого принудительного reflow
const { width, height } = entry.contentRect;
updateElementLayout(entry.target, width, height);
}
});
resizeObserver.observe(document.querySelector('.responsive-container'));
// Очистка по завершении
// resizeObserver.disconnect();
Лучшие практики для production-готовой обработки resize
- Всегда очищайте слушатели событий для предотвращения утечек памяти
- Выбирайте правильный инструмент: Используйте CSS для стилизации, ResizeObserver для мониторинга элементов, и throttled события resize только при необходимости
- Измеряйте влияние на производительность с помощью панели Performance в Chrome DevTools
- Рассмотрите passive listeners для лучшей производительности прокрутки, где применимо
Заключение
Понимая эти подводные камни событий resize в JavaScript и внедряя соответствующие решения, вы можете создавать адаптивные интерфейсы, которые плавно работают на всех устройствах. Ключ в том, чтобы выбрать правильный подход для вашего конкретного случая использования — будь то CSS-решения, современные API или правильно оптимизированные обработчики событий. Начинайте с CSS, где это возможно, используйте ResizeObserver для мониторинга конкретных элементов, и оставляйте throttled или debounced события resize для случаев, когда мониторинг на уровне окна действительно необходим.
Часто задаваемые вопросы
Throttling ограничивает выполнение до фиксированного интервала во время непрерывного изменения размера, выполняясь регулярно, пока пользователь перетаскивает. Debouncing ждет, пока изменение размера полностью не прекратится, перед однократным выполнением. Используйте throttling для обновлений в реальном времени и debouncing для финальных вычислений.
ResizeObserver имеет отличную поддержку в современных браузерах, включая Chrome, Firefox, Safari и Edge. Для старых браузеров используйте polyfill или возвращайтесь к throttled событиям resize с определением возможностей для обеспечения совместимости.
Используйте container queries, когда стилизация зависит от размера элемента, а не от viewport. Они идеально подходят для компонентных дизайнов, макетов карточек и адаптивной типографики без накладных расходов JavaScript или проблем с производительностью.
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.