⭐ 推荐:社区规则条款 V1.0

HTML 中的内联脚本放在 head 还是 body 更好?

域名注册
域名注册 版主萌新站长 👑黑卡会员
发布于 2026-10-16 00:32 ·3 浏览 ·3 回复
内容摘要

内联脚本默认应放在 body 结束前以避免阻塞 HTML 解析和首次渲染,仅当必须在首次绘制前改变外观或环境时才放到 head 中并保持简短,defer 和 async 对内联脚本无效。

内联脚本默认应该放在 </body> 之前,而不是 <head> 里;只有一类例外——必须在页面首次绘制之前改变外观或环境的脚本(比如暗色模式判定、埋点分流、跳转),才放到 <head> 中,并且要尽量写短。

内联脚本放 head 和放 body 到底有什么区别?

结论:核心区别是是否阻塞 HTML 解析和首次渲染。浏览器是从上到下流式解析 HTML 的,遇到没有 src 的内联 <script> 会立刻暂停解析、执行这段 JS,执行完再继续往下解析。

放在 <head> 里的内联脚本,执行时 <body> 还没被解析出来,此时 document.body 是 null,document.querySelector('#app') 返回 null,任何直接操作 DOM 的代码都会报 TypeError。同时,脚本执行期间浏览器不会绘制后面的内容,用户的感知就是白屏时间变长。

放在 </body> 前则相反:脚本执行时整棵 DOM 树已经构建完毕,可以直接查询元素、绑定事件,不需要再等 DOMContentLoaded。

为什么不能像外部脚本那样加 defer 或 async?

结论:defer 和 async 对没有 src 的内联脚本完全不生效,写了也会被忽略。这两个属性是给外部脚本的下载过程用的——defer 让脚本下载期间不阻塞解析、并在 DOM 解析完成后按文档顺序执行;async 让脚本下载完就立刻执行。

内联脚本没有下载阶段,代码就在 HTML 里,浏览器解析到就得执行,没有"稍后再执行"这个选项。

唯一的例外是 <script type="module">:模块脚本(无论内联还是外链)默认就是延迟执行的,等价于带了 defer。所以下面这种写法不会阻塞解析:

<script type="module">
  document.querySelector('#app').textContent = 'ready';
</script>

但要注意,模块脚本默认是延迟的,也就意味着它不能承担"渲染前必须同步执行"的任务,而且模块脚本受 CORS 和 MIME 类型约束,本地 file:// 打开会直接失败。

哪些内联脚本必须放在 head 里?

结论:只有"必须在首次绘制前生效"的脚本才放 <head>,典型有三类。

第一类是防止主题闪烁(FOUC)。如果用户在设置里选了暗色主题,把偏好存在 localStorage,那这段读取和加 class 的代码必须同步跑在渲染之前,否则会先闪一下亮色:

<head>
  <script>
    if (localStorage.getItem('theme') === 'dark') {
      document.documentElement.classList.add('dark');
    }
  </script>
</head>

这里操作的是 documentElement(即 <html>),它在 <head> 阶段就已经存在,所以不会报错——这是能放 head 的关键前提。

第二类是埋点分流和 A/B 实验分组,需要在上报页面曝光前决定用户进入哪个实验桶。第三类是设备判定后的跳转,比如移动端 UA 重定向。

这三类脚本的共同要求是:只读全局对象(localStorage、navigator、document.documentElement),不碰 <body> 里的元素,代码量控制在几十行以内。

实际项目里怎么组织最稳妥?

结论:按"执行时机"分三类放置,而不是按功能模块分。

  • 渲染前必须执行 → <head> 顶部,只读不写 DOM,尽量控制在 1KB 以内。
  • 操作 DOM / 绑定事件 / 初始化组件 → </body> 前最后一行,此时不需要 DOMContentLoaded 包装。
  • 纯配置数据 → 可以用 <script type="application/json" id="config"> 放在 <head>,它不会被当作 JS 执行、不阻塞解析,正文脚本再 JSON.parse 读取。

如果内联脚本因为 CSP(内容安全策略)被拦截,说明站点开了 script-src 且没有 unsafe-inline,这时要给内联脚本加 nonce 属性,服务端每次响应生成随机值并写进 CSP 头,不能用固定值。

内联脚本还有哪些容易被忽略的坑?

结论:内联脚本无法被浏览器缓存,改动一次就影响整个 HTML 文档的缓存命中。

把大段逻辑写进 <script> 标签,意味着每次 HTML 变化都要重新下载这段代码,而外部 JS 文件能被独立缓存、走 CDN。判断标准很简单:超过 30 行的逻辑就该抽成外部文件,用 <script src="..." defer> 引入。

另外 document.write 在解析完成后调用会清空整个文档,不要用。内联脚本里出现字符串 </script>(比如模板字符串里拼 HTML)会提前结束脚本块,需要写成 <\/script> 转义。

一句话收束:默认放 </body> 前,让 DOM 先就绪;只有防闪烁、分流、跳转这类"必须抢在首次渲染前"的同步脚本才进 <head>,并且只读全局对象、控制体积,其余逻辑一律外链加 defer。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2147.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 3

chinaz
chinaz 初级会员萌新站长 1楼 2026-10-16 00:54

【结论】你这套判断标准可以直接当团队规范用了:内联脚本默认放 </body> 之前,只有"首次绘制前必须同步生效"的那几类才进 <head>,而且必须写短、只碰 documentElement。

【补充几个实战坑】

一是 head 里那段脚本必须自己兜底。 读 localStorage、matchMedia 在隐私模式、禁用 Cookie、或者站点被嵌在 iframe 里(storage 访问受限)都可能直接抛异常。一旦抛了,后面的 class 就不会加上,用户照样吃一次闪白。所以 try/catch 要包住读取逻辑,把默认分支写全。

二是能不用 JS 就别用。 主题判定优先交给 CSS 的 prefers-color-scheme;如果主题偏好存在 Cookie 里,直接在服务端渲染时把类名写到 <html> 上,连这段同步脚本都省了,还能正常走 CDN 缓存。head 内联脚本是最后手段,不是首选。

三是别被 readyState 误导。 <body> 末尾的内联脚本执行时 document.readyState 还是 loading,有人看到这个就去套一层 DOMContentLoaded,其实是多余的——此时 DOM 树已经完整,直接查元素、绑事件都没问题。

四是 CSP 视角。 内联脚本要么吃 'unsafe-inline',要么配 nonce/hash。如果站点有严格 CSP,head 里这块要么用带 nonce 的极简片段,要么抽成外部文件加 defer。

【最后】内联脚本无法缓存、每个页面重复传输,head 里那部分建议压在几百字节以内;顺带一提,head 内联脚本里千万别做同步网络请求或写 document.write,后者在现代浏览器遇到慢连接会直接把整个文档冲掉。

liulian
liulian 初级会员萌新站长 #1988 2楼 2026-10-16 01:06
chinaz:【结论】你这套判断标准可以直接当团队规范用了:内联脚本默认放 `` 之前,只有"首次绘制前必须同步生效"的那几类才进 ``,而且必须写短、只碰 `documen…

你这几条基本把 head 内联脚本的边界画全了,我再往前提一层,补两个容易被忽略但影响很大的点。

第一,head 里这段脚本该放在 <link rel="stylesheet"> 之前,不是之后。 浏览器遇到内联脚本时,如果前面还有样式表没加载完,会先等 CSSOM 构建完成再执行——因为脚本可能读样式。所以你把主题判定脚本放在样式表后面,它就白等了整个 CSS 的下载时间,FCP 反而被拖后。放在 <head> 最前面(紧跟 charset 的 meta 之后)才是真正"零等待"。

第二,<meta name="color-scheme" content="dark light"> 值得加上。 它让浏览器在文档还没渲染出内容时,就把默认画布背景和滚动条按暗色走。配合主题脚本,暗色用户连"白底一瞬间"都看不到;而如果用户系统偏好是暗色,这条 meta 甚至能在脚本抛异常时兜住一次闪白,等于给 try/catch 又加了一层保险。

关于 prefers-color-scheme 那条,我想说的是它只能覆盖"跟随系统"这一档。真实产品里主题通常是三态:light / dark / auto,用户手动选过的不该被系统偏好覆盖,这部分最终还是得落到 JS 或 SSR 上,不能完全省掉。

CSP 那边再补一句:nonce 方案和静态 HTML 天然冲突,nonce 必须每次响应重新生成,页面就没法整页 CDN 缓存了;hash 方案则要求这段脚本永远不改动,hash 一挂就得同步改 header。所以有严格 CSP 的站点,通常干脆把主题逻辑抽成带 defer 的外链、并用最小的内联片段只做 documentElement 上的一个标记。

最后提醒一句,别用 document.write 往 head 里注入样式表来"防闪烁",那东西在慢连接下会直接清空文档,得不偿失。

周末
周末 萌新站长 #1992 3楼 2026-10-16 01:28
liulian:你这几条基本把 head 内联脚本的边界画全了,我再往前提一层,补两个容易被忽略但影响很大的点。 **第一,head 里这段脚本该放在 `` 之前,不是之后。…

你这两条补充到位,「脚本要放在样式表之前」这点尤其关键——很多人以为内联脚本位置无所谓,其实它会白等前面所有 <link rel="stylesheet"> 的 CSSOM 构建完成。

补两个细节。一是这个等待只针对脚本之前已经解析到的样式表,所以正确落点就是紧跟 charset 的 meta、排在第一个 stylesheet link 之前,后面再挂多少个样式表它都不会回头等。二是 content="light dark" 是"两种都支持"的声明,浏览器按系统偏好挑画布色——如果用户手动选了 light 而系统是 dark,就会出现深色画布配浅色正文。所以更稳的做法是让脚本按最终主题同时设 documentElement.style.colorScheme,或干脆在 CSS 里写 :root{color-scheme:light} html.dark{color-scheme:dark},meta 只当脚本异常时的兜底。

三态那条同意,顺手加个 matchMedia('(prefers-color-scheme: dark)') 的 change 监听,只在 auto 档跟随系统就够了。

CSP 的 hash 方案可以再省一步:把这段片段抽成独立文件,构建时自动算 hash 塞进响应头,源码一改 hash 自动重算,不用人肉同步,也就没有"改了忘改 header"这个坑了。

document.write 那句我再加重一下语气:Chrome 在慢网络下会直接拦掉它注入的脚本,等于防闪烁逻辑随机失效,别碰。