defer脚本执行时机严格限定在html解析完成、dom构建完毕后且domcontentloaded事件触发前;此时document.getelementbyid必能获取元素,但getcomputedstyle等可能因cssom未就绪而返回初始值。

不会。 defer 脚本绝不会在 DOM 构建完成前执行——这是它的核心契约,不是“大概率”或“通常”,而是浏览器强制保证的行为。
defer 执行时机的硬性边界在哪里
它被严格限定在两个事件之间:HTML parser 完全停止 之后、DOMContentLoaded 事件触发之前。注意,“parser 停止”不等于“DOM 就绪”;中间存在一个微小但确定的间隙,浏览器必须完成节点创建、父子挂载、文本归并等操作,DOM 树才算真正构建完毕。defer 脚本正是在这个“DOM 已就绪、但事件尚未派发”的窗口内执行。
验证方式很简单:在 defer 脚本第一行写 console.log(document.readyState),结果必为 "interactive",绝不会是 "loading"。
为什么 document.getElementById 在 defer 里总能拿到元素
因为 defer 的执行前提就是所有静态 HTML 标签(包括 <div id="app">)已解析完毕、对应 DOM 节点已创建并挂载到树上。此时调用 <code>document.getElementById 或 document.querySelector 是安全的。
但要注意这些边界情况:
-
getComputedStyle(document.body)可能拿不到最终样式——CSSOM 可能还没合并,这不是 defer 的问题,而是渲染流水线分离导致的 -
offsetHeight或getBoundingClientRect()可能返回0——layout 阶段还没开始,DOM 就绪 ≠ 渲染就绪 - 如果页面中有内联脚本(比如
<script>document.body.innerHTML = ""</script>)写在 defer 之前,defer 脚本看到的就是被清空后的 DOM——这不是 defer 提前了,而是其他脚本改写了 DOM
哪些写法会让 defer “失效”或退化成普通脚本
defer 是个很“娇气”的属性,稍有不合规就会被浏览器静默忽略:
- 只对带
src的外部脚本生效;<script defer>console.log(1)</script>中的defer完全无效 - 必须是无值布尔属性;
defer="false"或defer="defer"都不算,浏览器当没看见 - 同时写了
async和defer,defer直接被丢弃,按async规则执行 - 通过
innerHTML或document.write动态插入的<script defer></script>,defer属性被忽略
容易被忽略的兼容性细节
虽然 defer 在 IE9+ 和所有现代浏览器中都支持,但行为并非完全一致:
- IE10/11 对多个 defer 脚本的顺序保证不如 Chrome 稳定,极端情况下可能出现错序
- 旧版 Safari(如 iOS 12.5 之前)曾把 defer 脚本误放到
load事件之后执行,导致 DOM 操作失败 - 如果你依赖多个 defer 脚本的执行顺序(比如
lodash.js→utils.js),建议加简单检查:在utils.js开头写if (typeof _ === 'undefined') throw new Error('lodash missing')
最常被绕过的点是:很多人以为“parser 结束”就等于“DOM 可用”,其实中间那几十毫秒的节点挂载过程,才是 defer 真正等待的完成信号——它不看 parser,只认 DOM 树是否 ready。











