Когда запускать ваш код: объяснение событий загрузки страницы
Когда именно должен выполняться ваш JavaScript? С этим вопросом сталкивается каждый frontend-разработчик, независимо от того, манипулируете ли вы DOM на чистом JavaScript или управляете жизненным циклом компонентов в React. Ответ зависит от понимания событий загрузки страницы в браузере и выбора правильного хука для ваших конкретных задач.
Ключевые моменты
- DOMContentLoaded срабатывает после завершения парсинга HTML, в то время как load ожидает загрузки всех ресурсов
- Современные браузеры используют Page Visibility и Lifecycle API вместо ненадёжных событий unload
- React и другие фреймворки управляют временем выполнения через методы жизненного цикла компонентов
- Проверяйте document.readyState, чтобы не пропустить уже произошедшие события
Понимание классического жизненного цикла браузера
Браузер генерирует события в определённые моменты во время загрузки страницы. Знание того, когда срабатывает каждое событие и что доступно в этот момент, определяет, где должен находиться ваш код.
DOMContentLoaded vs load: критическое различие
DOMContentLoaded срабатывает, когда HTML полностью распарсен и построено дерево DOM. Внешние ресурсы, такие как изображения, таблицы стилей и iframe, всё ещё загружаются. Это ваша самая ранняя возможность безопасно запрашивать и манипулировать элементами DOM:
document.addEventListener('DOMContentLoaded', () => {
// DOM готов, но изображения могут всё ещё загружаться
const button = document.querySelector('#submit');
button.addEventListener('click', handleSubmit);
});
Событие load ожидает загрузки всего — изображений, таблиц стилей, iframe и других внешних ресурсов. Используйте его, когда вам нужна полная информация о ресурсах:
window.addEventListener('load', () => {
// Все ресурсы загружены - доступны размеры изображений
const img = document.querySelector('#hero');
console.log(`Image size: ${img.naturalWidth}x${img.naturalHeight}`);
});
Как скрипты влияют на время выполнения
Обычные теги <script> блокируют DOMContentLoaded — браузер должен выполнить их перед продолжением. Однако скрипты с атрибутом defer выполняются после парсинга DOM, но до срабатывания DOMContentLoaded. Скрипты с async загружаются параллельно и выполняются сразу после загрузки, потенциально до или после DOMContentLoaded.
Для ресурсов конкретных элементов используйте индивидуальные события load:
const img = new Image();
img.addEventListener('load', () => console.log('Image ready'));
img.src = 'photo.jpg';
Современные подходы: document.readyState и не только
Использование document.readyState
Вместо того чтобы надеяться, что ваш обработчик события подключён вовремя, проверьте текущее состояние:
function initialize() {
// Ваш код инициализации
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
// DOMContentLoaded уже сработал
initialize();
}
Существует три состояния:
'loading'- документ парсится'interactive'- парсинг завершён, DOMContentLoaded вот-вот сработает'complete'- все ресурсы загружены
jQuery-метод $(document).ready() по сути был кроссбраузерной обёрткой для этого паттерна. Сегодня нативные API надёжно обрабатывают это во всех браузерах.
Discover how at OpenReplay.com.
Page Visibility и Lifecycle API: новая реальность
Почему beforeunload и unload проблематичны
Современные браузеры агрессивно оптимизируют производительность с помощью таких функций, как back-forward cache (bfcache). Страницы, попадающие в bfcache, не выгружаются — они приостанавливаются. Это означает, что события beforeunload и unload не срабатывают надёжно, когда пользователи уходят со страницы. Кроме того, браузеры теперь ограничивают или игнорируют пользовательские сообщения в диалогах beforeunload, чтобы предотвратить злоупотребления.
Альтернатива в виде Page Visibility API
Вместо ненадёжных событий unload используйте Page Visibility API для реагирования, когда пользователи переключают вкладки или сворачивают браузер:
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// Страница скрыта - приостановите ресурсоёмкие операции
pauseVideoPlayback();
throttleWebSocket();
} else {
// Страница снова видима
resumeOperations();
}
});
Состояния жизненного цикла страницы
Page Lifecycle API расширяет это состояниями вроде frozen (вкладка приостановлена для экономии памяти) и terminated:
document.addEventListener('freeze', () => {
// Сохраните состояние - вкладка может быть удалена
localStorage.setItem('appState', JSON.stringify(state));
});
document.addEventListener('resume', () => {
// Восстановление после заморозки
hydrateState();
});
React useEffect vs DOMContentLoaded
В React и других современных фреймворках вы редко используете DOMContentLoaded напрямую. Методы жизненного цикла компонентов обрабатывают время инициализации:
// React-компонент
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Выполняется после монтирования компонента и обновления DOM
// Похоже по времени на DOMContentLoaded для этого компонента
initializeThirdPartyLibrary();
return () => {
// Очистка при размонтировании
cleanup();
};
}, []); // Пустой массив зависимостей = выполнить один раз после монтирования
return <div>Component content</div>;
}
Для Next.js или других SSR-фреймворков код в useEffect выполняется только на клиенте после гидратации — фреймворк обрабатывает сложность времени выполнения на сервере и клиенте.
Выбор правильного хука
Для чистого JavaScript:
- Манипуляции с DOM: используйте DOMContentLoaded
- Код, зависящий от ресурсов: используйте событие window load
- Сохранение состояния: используйте Page Visibility API
- Проверка текущего состояния: используйте document.readyState
Для SPA и фреймворков:
- Инициализация компонентов: используйте жизненный цикл фреймворка (useEffect, mounted и т.д.)
- Изменения маршрутов: используйте события роутера
- Фон/передний план: всё ещё используйте Page Visibility API
Избегайте этих паттернов:
- Не полагайтесь на beforeunload/unload для критических операций
- Не используйте DOMContentLoaded в React-компонентах
- Не предполагайте, что скрипты выполняются при свежей загрузке страницы (учитывайте bfcache)
Заключение
События загрузки страницы в JavaScript эволюционировали за пределы простых обработчиков DOMContentLoaded и load. Хотя эти классические события остаются важными для чистого JavaScript, современная разработка требует понимания Page Visibility, Lifecycle API и паттернов, специфичных для фреймворков. Выбирайте стратегию инициализации на основе того, какие ресурсы вам нужны и работаете ли вы с серверно-рендеренной страницей или создаёте полноценное SPA. Самое важное — не полагайтесь на устаревшие паттерны вроде событий unload — используйте современные API, созданные для современных веб-приложений.
Часто задаваемые вопросы
Скрипты с defer выполняются по порядку после парсинга DOM, но до DOMContentLoaded. Скрипты с async выполняются сразу после загрузки, потенциально прерывая парсинг. Используйте defer для скриптов, которым нужен доступ к DOM, и async для независимых скриптов вроде аналитики.
Хотя window.onload работает, предпочтительнее addEventListener, потому что он позволяет использовать несколько обработчиков и не перезаписывает существующие. Само событие load остаётся полезным, когда вам нужно, чтобы все ресурсы были полностью загружены перед выполнением кода.
Используйте useEffect с пустым массивом зависимостей для клиентской инициализации. Это выполняется после завершения гидратации. Для серверного кода используйте специфичные для фреймворка методы вроде getServerSideProps или getStaticProps вместо браузерных событий.
Современные браузеры кэшируют страницы и могут не генерировать beforeunload, когда пользователи уходят со страницы. Вместо этого используйте Page Visibility API для определения, когда пользователи уходят, и сохраняйте критические данные непрерывно, а не при выходе.
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.