何时运行你的代码:页面加载事件详解
你的 JavaScript 应该在什么时候运行?这是每个前端开发者都会面临的问题,无论你是在原生 JavaScript 中操作 DOM,还是在 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(`图片尺寸: ${img.naturalWidth}x${img.naturalHeight}`);
});
脚本如何影响时机
常规的 <script> 标签会阻塞 DOMContentLoaded——浏览器必须先执行它们才能继续。然而,带有 defer 的脚本在 DOM 解析后但在 DOMContentLoaded 触发前执行。带有 async 的脚本并行加载并在下载完成后立即执行,可能在 DOMContentLoaded 之前或之后。
对于元素特定的资源,使用单独的 load 事件:
const img = new Image();
img.addEventListener('load', () => console.log('图片已就绪'));
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 存在问题
现代浏览器通过后退-前进缓存(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>组件内容</div>;
}
对于 Next.js 或其他 SSR 框架,useEffect 中的代码仅在水合(hydration)后在客户端运行——框架处理服务器与客户端执行时机的复杂性。
选择正确的钩子
对于原生 JavaScript:
- DOM 操作:使用 DOMContentLoaded
- 依赖资源的代码:使用 window load 事件
- 状态持久化:使用 Page Visibility API
- 检查当前状态:使用 document.readyState
对于 SPA 和框架:
- 组件初始化:使用框架生命周期(useEffect、mounted 等)
- 路由变化:使用路由事件
- 后台/前台:仍然使用 Page Visibility API
避免这些模式:
- 不要依赖 beforeunload/unload 进行关键操作
- 不要在 React 组件中使用 DOMContentLoaded
- 不要假设脚本在全新页面加载中运行(考虑 bfcache)
结论
JavaScript 页面加载事件已经超越了简单的 DOMContentLoaded 和 load 处理程序。虽然这些经典事件对于原生 JavaScript 仍然至关重要,但现代开发需要理解 Page Visibility、Lifecycle API 和框架特定的模式。根据你需要的资源以及你是在处理服务器渲染的页面还是构建完整的 SPA 来选择初始化策略。最重要的是,不要依赖像 unload 事件这样的已弃用模式——拥抱为现代 Web 应用程序构建的现代 API。
常见问题
Defer 脚本在 DOM 解析后但在 DOMContentLoaded 之前按顺序执行。Async 脚本在下载完成后立即执行,可能会中断解析。对需要访问 DOM 的脚本使用 defer,对独立脚本(如分析工具)使用 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.