Представь, что ты запускаешь SPA на React или Vue. Один из самых частых багов на старте — обращение к DOM до того, как он готов. Или запуск аналитики после того, как пользователь уже ушёл. Жизненный цикл страницы — это четыре события, которые определяют, в какой момент что можно делать.
Без понимания жизненного цикла код инициализации запускается «вслепую». Скрипт в <head> пытается обратиться к элементам, которых ещё нет в DOM. Или приложение ждёт события load для запуска роутера, хотя DOM готов уже на этапе DOMContentLoaded — и страница отображается пустой лишние 500ms.
beforeunload использует тот же паттерн event.preventDefault()Парсинг HTML
↓
DOMContentLoaded ← DOM готов, JS может работать с элементами
↓
Загрузка картинок, CSS, шрифтов...
↓
load ← всё загружено, включая ресурсыСрабатывает когда браузер полностью разобрал HTML и построил DOM-дерево. Картинки и стили ещё могут загружаться.
document.addEventListener('DOMContentLoaded', () => {
// DOM готов — можно безопасно работать с элементами
const menu = document.getElementById('main-menu')
initNavigation(menu)
const form = document.querySelector('form')
initFormValidation(form)
})Это основное место для инициализации большинства приложений. Не нужно ждать загрузки картинок — они не нужны для работы логики.
Срабатывает когда загружено всё: картинки, стили, шрифты, iframe.
window.addEventListener('load', () => {
// Всё загружено — можно работать с размерами изображений
const img = document.querySelector('img#banner')
console.log(img.naturalWidth) // реальная ширина загруженной картинки
console.log(img.naturalHeight) // реальная высота
hideLoadingSpinner()
initAnimations()
})Срабатывает когда пользователь пытается покинуть страницу (закрыть вкладку, перейти по ссылке). Позволяет показать диалог подтверждения.
window.addEventListener('beforeunload', (event) => {
if (hasUnsavedChanges()) {
// Стандартный способ показать диалог подтверждения
event.preventDefault()
event.returnValue = '' // для совместимости со старыми браузерами
// Браузер покажет: "Вы уверены, что хотите покинуть страницу?"
}
})Текст диалога нельзя изменить — браузеры намеренно игнорируют returnValue как строку из соображений безопасности.
Срабатывает когда страница окончательно закрывается. Последний шанс что-то сделать, но с ограничениями:
window.addEventListener('unload', () => {
// Успевает выполниться только быстрый синхронный код
// fetch и XMLHttpRequest здесь ненадёжны!
// Правильный способ отправить аналитику при закрытии:
navigator.sendBeacon('/api/analytics', JSON.stringify({
sessionDuration: Date.now() - sessionStart,
lastPage: location.pathname,
}))
})Отражает текущее состояние загрузки документа:
console.log(document.readyState)
// 'loading' — HTML ещё парсится
// 'interactive' — DOM готов (как DOMContentLoaded), ресурсы ещё грузятся
// 'complete' — всё загружено (как load)// Универсальная инициализация — работает в любой момент
function initWhenReady(callback) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', callback)
} else {
// DOM уже готов — выполняем сразу
callback()
}
}
initWhenReady(() => {
console.log('Приложение инициализировано')
})Событие readystatechange срабатывает при каждом изменении readyState:
document.addEventListener('readystatechange', () => {
console.log('readyState:', document.readyState)
})
// loading → interactive → completeБез атрибутов:
HTML ─── стоп ─── скрипт ─── продолжение HTML ───► DOMContentLoaded
defer:
HTML ──────────────────────────────── DOMContentLoaded
└─ скрипт (после парсинга, до DOMContentLoaded)
async:
HTML ─────────── (продолжает) ──────────────────────
└─ скрипт (немедленно после загрузки, порядок не гарантирован)// defer — скрипт выполняется после парсинга HTML, сохраняет порядок
// Лучший вариант для большинства скриптов
<script defer src="app.js"></script>
// async — скрипт загружается параллельно, выполняется сразу после загрузки
// Подходит для независимых скриптов (счётчики, чаты)
<script async src="analytics.js"></script>1. Инициализация вне DOMContentLoaded — DOM ещё не готов
// ПЛОХО — скрипт в <head>, DOM ещё не построен
const btn = document.getElementById('submit-btn') // null!
btn.addEventListener('click', handleSubmit) // TypeError: Cannot read properties of null
// ХОРОШО
document.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('submit-btn') // элемент найден
btn.addEventListener('click', handleSubmit)
})2. Использование fetch в unload — запрос не успевает отправиться
// ПЛОХО — браузер может убить страницу до завершения fetch
window.addEventListener('unload', () => {
fetch('/api/session-end', { method: 'POST', body: JSON.stringify(stats) })
// Этот запрос, скорее всего, не дойдёт до сервера
})
// ХОРОШО — sendBeacon гарантированно отправляется даже при закрытии
window.addEventListener('unload', () => {
navigator.sendBeacon('/api/session-end', JSON.stringify(stats))
})3. Игнорирование readyState при динамическом добавлении скриптов
// ПЛОХО — если скрипт добавлен после DOMContentLoaded, событие уже не придёт
document.addEventListener('DOMContentLoaded', init) // пропустит!
// ХОРОШО — universальная проверка
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init)
} else {
init() // уже готов — запускаем сразу
}main.js всегда ждёт DOMContentLoaded или монтируется после него через deferasync — не блокирует загрузку страницыbeforeunload для предупреждения при уходе с несохранёнными изменениямиnavigator.sendBeacon в unloadМашина состояний document.readyState и паттерн инициализации приложения
// Симуляция жизненного цикла страницы без DOM
// Моделируем переходы readyState и порядок событий
class PageLifecycleSimulator {
constructor() {
this._readyState = 'loading'
this._listeners = { DOMContentLoaded: [], load: [], beforeunload: [], unload: [] }
this._log = []
}
// Подписка на события (аналог document/window.addEventListener)
onDocument(event, handler) {
if (this._listeners[event]) {
this._listeners[event].push(handler)
}
}
_emit(eventName) {
this._log.push(`[событие] ${eventName}`)
this._listeners[eventName]?.forEach(fn => {
try { fn() } catch (e) { this._log.push(`[ошибка] ${e.message}`) }
})
}
// Симулируем этапы загрузки страницы
simulateLoad() {
this._log.push('[состояние] loading — HTML парсится...')
// HTML разобран → DOM готов
this._readyState = 'interactive'
this._log.push('[состояние] interactive — DOM готов')
this._emit('DOMContentLoaded')
// Ресурсы загружены → страница полностью готова
this._readyState = 'complete'
this._log.push('[состояние] complete — все ресурсы загружены')
this._emit('load')
}
simulateUnload(hasUnsavedData) {
this._log.push('[событие] пользователь уходит со страницы...')
let cancelled = false
const beforeunloadEvent = {
preventDefault: () => { cancelled = true },
returnValue: '',
}
this._listeners.beforeunload.forEach(fn => fn(beforeunloadEvent))
if (cancelled) {
this._log.push('[диалог] "Вы уверены что хотите покинуть страницу?"')
if (!hasUnsavedData) {
this._log.push('[результат] пользователь подтвердил уход')
this._emit('unload')
} else {
this._log.push('[результат] пользователь остался на странице')
return
}
} else {
this._emit('unload')
}
}
getLog() { return this._log }
get readyState() { return this._readyState }
}
// === Использование симулятора ===
const page = new PageLifecycleSimulator()
// Регистрируем обработчики — как в реальном приложении
const sessionStart = 1700000000000 // мок-время
page.onDocument('DOMContentLoaded', () => {
console.log('1. DOMContentLoaded: инициализируем маршрутизатор')
console.log('2. DOMContentLoaded: рендерим навигацию')
console.log('3. DOMContentLoaded: подключаем обработчики форм')
})
page.onDocument('load', () => {
console.log('4. load: инициализируем lazy-загрузку изображений')
console.log('5. load: запускаем мониторинг производительности')
const loadTime = Date.now() - sessionStart
console.log(`6. load: время загрузки страницы ~ ${loadTime > 0 ? loadTime : 800}ms`)
})
page.onDocument('beforeunload', (event) => {
const unsaved = true // симулируем несохранённые данные
if (unsaved) {
event.preventDefault()
event.returnValue = ''
}
})
page.onDocument('unload', () => {
console.log('7. unload: отправляем аналитику через navigator.sendBeacon')
console.log(' POST /api/session-end { duration: 45000 }')
})
// Симулируем полный жизненный цикл
console.log('=== ЗАГРУЗКА СТРАНИЦЫ ===')
page.simulateLoad()
console.log('\nreadyState после загрузки:', page.readyState) // 'complete'
console.log('\n=== УХОД СО СТРАНИЦЫ (несохранённые данные) ===')
page.simulateUnload(true) // пользователь остаётся
console.log('\n=== ЛОГ ЖИЗНЕННОГО ЦИКЛА ===')
page.getLog().forEach(entry => console.log(entry))
// Функция initWhenReady — реальный паттерн
console.log('\n=== Паттерн initWhenReady ===')
function initWhenReady(currentState, callback) {
if (currentState === 'loading') {
console.log('DOM ещё не готов — подписываемся на DOMContentLoaded')
} else {
console.log('DOM уже готов — выполняем callback немедленно')
callback()
}
}
initWhenReady('complete', () => {
console.log('Инициализация выполнена!')
})Представь, что ты запускаешь SPA на React или Vue. Один из самых частых багов на старте — обращение к DOM до того, как он готов. Или запуск аналитики после того, как пользователь уже ушёл. Жизненный цикл страницы — это четыре события, которые определяют, в какой момент что можно делать.
Без понимания жизненного цикла код инициализации запускается «вслепую». Скрипт в <head> пытается обратиться к элементам, которых ещё нет в DOM. Или приложение ждёт события load для запуска роутера, хотя DOM готов уже на этапе DOMContentLoaded — и страница отображается пустой лишние 500ms.
beforeunload использует тот же паттерн event.preventDefault()Парсинг HTML
↓
DOMContentLoaded ← DOM готов, JS может работать с элементами
↓
Загрузка картинок, CSS, шрифтов...
↓
load ← всё загружено, включая ресурсыСрабатывает когда браузер полностью разобрал HTML и построил DOM-дерево. Картинки и стили ещё могут загружаться.
document.addEventListener('DOMContentLoaded', () => {
// DOM готов — можно безопасно работать с элементами
const menu = document.getElementById('main-menu')
initNavigation(menu)
const form = document.querySelector('form')
initFormValidation(form)
})Это основное место для инициализации большинства приложений. Не нужно ждать загрузки картинок — они не нужны для работы логики.
Срабатывает когда загружено всё: картинки, стили, шрифты, iframe.
window.addEventListener('load', () => {
// Всё загружено — можно работать с размерами изображений
const img = document.querySelector('img#banner')
console.log(img.naturalWidth) // реальная ширина загруженной картинки
console.log(img.naturalHeight) // реальная высота
hideLoadingSpinner()
initAnimations()
})Срабатывает когда пользователь пытается покинуть страницу (закрыть вкладку, перейти по ссылке). Позволяет показать диалог подтверждения.
window.addEventListener('beforeunload', (event) => {
if (hasUnsavedChanges()) {
// Стандартный способ показать диалог подтверждения
event.preventDefault()
event.returnValue = '' // для совместимости со старыми браузерами
// Браузер покажет: "Вы уверены, что хотите покинуть страницу?"
}
})Текст диалога нельзя изменить — браузеры намеренно игнорируют returnValue как строку из соображений безопасности.
Срабатывает когда страница окончательно закрывается. Последний шанс что-то сделать, но с ограничениями:
window.addEventListener('unload', () => {
// Успевает выполниться только быстрый синхронный код
// fetch и XMLHttpRequest здесь ненадёжны!
// Правильный способ отправить аналитику при закрытии:
navigator.sendBeacon('/api/analytics', JSON.stringify({
sessionDuration: Date.now() - sessionStart,
lastPage: location.pathname,
}))
})Отражает текущее состояние загрузки документа:
console.log(document.readyState)
// 'loading' — HTML ещё парсится
// 'interactive' — DOM готов (как DOMContentLoaded), ресурсы ещё грузятся
// 'complete' — всё загружено (как load)// Универсальная инициализация — работает в любой момент
function initWhenReady(callback) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', callback)
} else {
// DOM уже готов — выполняем сразу
callback()
}
}
initWhenReady(() => {
console.log('Приложение инициализировано')
})Событие readystatechange срабатывает при каждом изменении readyState:
document.addEventListener('readystatechange', () => {
console.log('readyState:', document.readyState)
})
// loading → interactive → completeБез атрибутов:
HTML ─── стоп ─── скрипт ─── продолжение HTML ───► DOMContentLoaded
defer:
HTML ──────────────────────────────── DOMContentLoaded
└─ скрипт (после парсинга, до DOMContentLoaded)
async:
HTML ─────────── (продолжает) ──────────────────────
└─ скрипт (немедленно после загрузки, порядок не гарантирован)// defer — скрипт выполняется после парсинга HTML, сохраняет порядок
// Лучший вариант для большинства скриптов
<script defer src="app.js"></script>
// async — скрипт загружается параллельно, выполняется сразу после загрузки
// Подходит для независимых скриптов (счётчики, чаты)
<script async src="analytics.js"></script>1. Инициализация вне DOMContentLoaded — DOM ещё не готов
// ПЛОХО — скрипт в <head>, DOM ещё не построен
const btn = document.getElementById('submit-btn') // null!
btn.addEventListener('click', handleSubmit) // TypeError: Cannot read properties of null
// ХОРОШО
document.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('submit-btn') // элемент найден
btn.addEventListener('click', handleSubmit)
})2. Использование fetch в unload — запрос не успевает отправиться
// ПЛОХО — браузер может убить страницу до завершения fetch
window.addEventListener('unload', () => {
fetch('/api/session-end', { method: 'POST', body: JSON.stringify(stats) })
// Этот запрос, скорее всего, не дойдёт до сервера
})
// ХОРОШО — sendBeacon гарантированно отправляется даже при закрытии
window.addEventListener('unload', () => {
navigator.sendBeacon('/api/session-end', JSON.stringify(stats))
})3. Игнорирование readyState при динамическом добавлении скриптов
// ПЛОХО — если скрипт добавлен после DOMContentLoaded, событие уже не придёт
document.addEventListener('DOMContentLoaded', init) // пропустит!
// ХОРОШО — universальная проверка
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init)
} else {
init() // уже готов — запускаем сразу
}main.js всегда ждёт DOMContentLoaded или монтируется после него через deferasync — не блокирует загрузку страницыbeforeunload для предупреждения при уходе с несохранёнными изменениямиnavigator.sendBeacon в unloadМашина состояний document.readyState и паттерн инициализации приложения
// Симуляция жизненного цикла страницы без DOM
// Моделируем переходы readyState и порядок событий
class PageLifecycleSimulator {
constructor() {
this._readyState = 'loading'
this._listeners = { DOMContentLoaded: [], load: [], beforeunload: [], unload: [] }
this._log = []
}
// Подписка на события (аналог document/window.addEventListener)
onDocument(event, handler) {
if (this._listeners[event]) {
this._listeners[event].push(handler)
}
}
_emit(eventName) {
this._log.push(`[событие] ${eventName}`)
this._listeners[eventName]?.forEach(fn => {
try { fn() } catch (e) { this._log.push(`[ошибка] ${e.message}`) }
})
}
// Симулируем этапы загрузки страницы
simulateLoad() {
this._log.push('[состояние] loading — HTML парсится...')
// HTML разобран → DOM готов
this._readyState = 'interactive'
this._log.push('[состояние] interactive — DOM готов')
this._emit('DOMContentLoaded')
// Ресурсы загружены → страница полностью готова
this._readyState = 'complete'
this._log.push('[состояние] complete — все ресурсы загружены')
this._emit('load')
}
simulateUnload(hasUnsavedData) {
this._log.push('[событие] пользователь уходит со страницы...')
let cancelled = false
const beforeunloadEvent = {
preventDefault: () => { cancelled = true },
returnValue: '',
}
this._listeners.beforeunload.forEach(fn => fn(beforeunloadEvent))
if (cancelled) {
this._log.push('[диалог] "Вы уверены что хотите покинуть страницу?"')
if (!hasUnsavedData) {
this._log.push('[результат] пользователь подтвердил уход')
this._emit('unload')
} else {
this._log.push('[результат] пользователь остался на странице')
return
}
} else {
this._emit('unload')
}
}
getLog() { return this._log }
get readyState() { return this._readyState }
}
// === Использование симулятора ===
const page = new PageLifecycleSimulator()
// Регистрируем обработчики — как в реальном приложении
const sessionStart = 1700000000000 // мок-время
page.onDocument('DOMContentLoaded', () => {
console.log('1. DOMContentLoaded: инициализируем маршрутизатор')
console.log('2. DOMContentLoaded: рендерим навигацию')
console.log('3. DOMContentLoaded: подключаем обработчики форм')
})
page.onDocument('load', () => {
console.log('4. load: инициализируем lazy-загрузку изображений')
console.log('5. load: запускаем мониторинг производительности')
const loadTime = Date.now() - sessionStart
console.log(`6. load: время загрузки страницы ~ ${loadTime > 0 ? loadTime : 800}ms`)
})
page.onDocument('beforeunload', (event) => {
const unsaved = true // симулируем несохранённые данные
if (unsaved) {
event.preventDefault()
event.returnValue = ''
}
})
page.onDocument('unload', () => {
console.log('7. unload: отправляем аналитику через navigator.sendBeacon')
console.log(' POST /api/session-end { duration: 45000 }')
})
// Симулируем полный жизненный цикл
console.log('=== ЗАГРУЗКА СТРАНИЦЫ ===')
page.simulateLoad()
console.log('\nreadyState после загрузки:', page.readyState) // 'complete'
console.log('\n=== УХОД СО СТРАНИЦЫ (несохранённые данные) ===')
page.simulateUnload(true) // пользователь остаётся
console.log('\n=== ЛОГ ЖИЗНЕННОГО ЦИКЛА ===')
page.getLog().forEach(entry => console.log(entry))
// Функция initWhenReady — реальный паттерн
console.log('\n=== Паттерн initWhenReady ===')
function initWhenReady(currentState, callback) {
if (currentState === 'loading') {
console.log('DOM ещё не готов — подписываемся на DOMContentLoaded')
} else {
console.log('DOM уже готов — выполняем callback немедленно')
callback()
}
}
initWhenReady('complete', () => {
console.log('Инициализация выполнена!')
})Напиши функцию initApp(readyState), которая принимает строку readyState ("loading", "interactive" или "complete") и возвращает объект { actions: string[], canAccessDOM: boolean, canAccessImages: boolean }. Для каждого состояния верни список действий которые можно выполнить и флаги доступности DOM и изображений.
case "loading": canAccessDOM = false, canAccessImages = false. case "interactive": canAccessDOM = true, canAccessImages = false. case "complete": canAccessDOM = true, canAccessImages = true. actions — любой разумный массив строк для каждого состояния.