window.onload 和 DOMContentLoaded 的区别
Category(分类): JavaScript Status: 已整理(2026)
原文:window onload 和 DOMContentLoaded 的区别。本文保留原文对两个事件、DOM 解析过程、脚本延迟和事件绑定时机的说明,并修正“DOMContentLoaded 完全不受样式表影响”“load 等于所有资源绝对加载完成”等过于绝对的表述。
一、先给结论
| 事件 | 主要含义 | 通常等待什么 | 通常不等待什么 |
|---|---|---|---|
DOMContentLoaded | HTML 已解析成 DOM,且需要等待的 deferred/module 脚本已执行 | HTML 解析、parser-blocking 脚本造成的解析延迟、defer 和普通模块脚本 | 普通图片、视频、iframe 等外部资源 |
window 的 load | 页面及其相关非懒加载资源完成加载 | DOM、样式表、脚本、图片、iframe 等页面资源 | 懒加载资源等不参与当前加载过程的资源 |
DOMContentLoaded 关注“DOM 是否可以使用”;load 关注“页面资源是否基本全部就绪”。因此初始化交互通常不必等待 load,而需要图片尺寸、字体或 iframe 完成时才考虑 load。
二、HTML 文档的大致过程
简化后可以这样理解:
- 浏览器接收并解析 HTML;
- 解析过程中发现样式表、脚本、图片等资源并发起请求;
- 普通 parser-inserted classic script 可能暂停解析,获取并执行后继续;
- HTML 解析完成,文档进入 interactive 状态;
defer和普通模块脚本执行;- 触发
DOMContentLoaded; - 其他页面资源继续或完成加载;
- 触发
window的load,文档通常进入 complete 状态。
这不是所有资源和脚本类型的唯一顺序:async 脚本可以在准备好后自行执行,模块依赖和顶层 await 也会影响评估时间。更准确的说法是:DOMContentLoaded 发生在 DOM 解析完成和需要等待的 deferred/module 脚本执行之后,而 load 还要等待更广泛的资源。
三、DOMContentLoaded 事件
最基本的写法:
document.addEventListener('DOMContentLoaded', () => {
const app = document.querySelector('#app')
app.textContent = 'DOM is ready'
})
它不是 document.ready。后者是 jQuery 曾经提供的便捷 API;原生浏览器没有 document.ready 这个标准事件。
3.1 它不等待普通图片
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>DOMContentLoaded demo</title>
</head>
<body>
<img src="/large-image.jpg" alt="demo">
<script>
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM 已解析,图片可能仍在加载')
})
window.addEventListener('load', () => {
console.log('页面资源加载完成,图片通常已经可用')
})
</script>
</body>
</html>
3.2 样式表的边界
入门文章常写“DOMContentLoaded 不等待样式表”。更精确的理解是:事件本身不把所有样式表都列为直接等待条件,但样式表可能影响脚本执行和事件时机。
例如,parser-blocking classic script 可能要等待此前发现的阻塞样式表;该脚本又会阻塞 HTML 解析,因此会间接推迟 DOMContentLoaded。defer 和模块脚本也可能等待其需要的样式或模块依赖完成。
3.3 同步脚本会延迟 DOM 解析
<script>
document.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded')
})
for (let index = 0; index < 100000000; index += 1) {
// 长时间同步任务会阻塞解析器和主线程
}
</script>
循环期间主线程没有机会继续解析或派发事件,所以 DOMContentLoaded 会等这段同步任务结束。不要在生产代码中使用这种忙等方式;它只用于说明事件为什么会被推迟。
3.4 异步加载脚本可能错过事件
如果脚本以 async、动态插入或动态 import 的方式加载,它运行时 DOMContentLoaded 可能已经发生。稳妥的初始化模式是同时检查 readyState:
function init() {
console.log('开始初始化')
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true })
} else {
init()
}
JavaScript 的 run-to-completion 语义保证了 readyState 检查与添加监听器之间不会被另一个事件任务插入,因此这种写法不存在通常所说的竞态窗口。
四、window.onload 事件
可以使用事件监听器:
window.addEventListener('load', () => {
console.log('页面及相关资源已加载')
})
也可以设置事件处理器属性:
window.onload = () => {
console.log('页面已加载')
}
但 window.onload = ... 后一次赋值会覆盖前一次处理器;需要多个模块同时监听时,应使用 addEventListener。
load 通常等待主文档依赖的样式表、脚本、图片和 iframe 等资源。它不是“网络上所有东西都完成”的保证:懒加载资源、用户交互后才开始的资源以及不属于当前加载过程的资源可能不影响初始 load。
原文提到 Flash;Flash 已停止支持,现代页面应把关注点放在图片、字体、iframe、脚本和其他实际资源上。
五、原文背景色示例的现代写法
原文通过在 window.onload 中查找 #bg 并设置背景色,说明等待页面加载后再操作元素。示例可以整理为:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>load demo</title>
<style>
#bg {
width: 100px;
height: 100px;
border: 2px solid;
}
</style>
</head>
<body>
<div id="bg"></div>
<script>
window.addEventListener('load', () => {
document.querySelector('#bg').style.backgroundColor = 'yellow'
})
</script>
</body>
</html>

图片来源:原文配图。
不过,如果代码只依赖 DOM 元素而不依赖图片等资源,使用 load 可能等待过久。下面的写法通常更合适:
document.addEventListener('DOMContentLoaded', () => {
document.querySelector('#bg').style.backgroundColor = 'yellow'
})

图片来源:原文配图。
甚至可以把脚本放在目标元素之后:
<div id="bg"></div>
<script>
document.querySelector('#bg').style.backgroundColor = 'yellow'
</script>
六、观察事件顺序
<body>
<p>测试事件顺序</p>
<script>
console.log('同步脚本执行')
document.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded 回调')
})
window.addEventListener('load', () => {
console.log('load 回调')
})
</script>
</body>
在没有其他异步因素的简单页面中,通常能看到:
同步脚本执行
DOMContentLoaded 回调
load 回调
使用 defer 的外部脚本一般在 DOM 解析后、DOMContentLoaded 前执行:
<head>
<script defer src="/js/app.js"></script>
</head>
使用 async 时,脚本不保证和 DOMContentLoaded 的相对顺序:
<script async src="/js/analytics.js"></script>
如果 analytics 脚本下载很快,它可能在 DOMContentLoaded 前执行;如果下载很慢,DOMContentLoaded 可能先发生。不要把某一次网络截图当成规范保证。
七、readystatechange 与 readyState
需要观察文档阶段时可以监听 readystatechange:
document.addEventListener('readystatechange', () => {
console.log(document.readyState)
})
常见状态包括:
loading:HTML 仍在解析;interactive:文档已经解析完成,但 deferred/module 脚本和DOMContentLoaded相关流程可能尚未结束;complete:文档及其相关加载过程基本完成,随后会触发或已经触发load。
readyState 适合配合前面的“事件可能已经发生”模式,但它不能替代对图片、字体、iframe 等具体资源的等待。如果只需要某张图片完成,直接监听该元素的 load 更准确:
const image = document.querySelector('img')
if (image.complete) {
console.log('图片已经结束加载过程')
} else {
image.addEventListener('load', () => console.log('图片加载成功'), { once: true })
image.addEventListener('error', () => console.error('图片加载失败'), { once: true })
}
八、如何选择
- 只操作已经写在页面中的 DOM:脚本放在 body 底部、使用
defer/模块脚本,或监听DOMContentLoaded; - 依赖图片尺寸、iframe 或其他页面资源:使用
window的load或具体资源的load; - 异步/动态脚本:检查
document.readyState,避免错过DOMContentLoaded; - 多个监听者:使用
addEventListener,不要反复覆盖window.onload; - 不要为了 DOM 就绪无条件等待所有图片和字体,这会推迟交互;
- 不要把
DOMContentLoaded和 jQueryready、window.onload当作同一个事件。
总结
DOMContentLoaded 表示 DOM 已经解析完成,适合初始化页面结构和绑定交互;window.load 表示页面加载过程中的更广泛资源已经完成,适合依赖外部资源的场景。脚本类型、样式表、模块依赖、async/defer 和动态加载都会改变时机,判断时应从“它需要等待什么资源”出发,而不是死记一个固定的时间顺序。