Back

Когда запускать ваш код: объяснение событий загрузки страницы

Когда запускать ваш код: объяснение событий загрузки страницы

Когда именно должен выполняться ваш 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 надёжно обрабатывают это во всех браузерах.

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.

OpenReplay