天了噜,为什么外链 CSS 要放在头部,JS 要放在尾部?
Category(分类): JavaScript Status: 已整理
早期前端教程经常建议“CSS 放在 <head>,JS 放在 <body> 尾部”。这条经验有历史背景,但不能当作现代网页的绝对规则。理解它之前,需要把三个阶段分开:
- HTML 解析:HTML parser 读取并构造 DOM;
- 脚本获取与执行:脚本可能阻塞 parser,也可能异步执行;
- 样式计算与渲染:CSSOM、渲染树、布局和绘制需要适用的样式资源。
不同资源对这三个阶段的影响不同。现代页面通常优先使用头部 defer 或 type="module" 脚本,并结合实际性能数据决定资源顺序。
为什么关键外链 CSS 通常放在 <head>?
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/assets/app.css">
</head>
普通、适用的外链样式表通常不会让 HTML parser 停在 <link> 处;浏览器可以继续解析后面的 HTML,并行获取 CSS。但是 CSSOM 和最终渲染需要样式规则,浏览器通常会在关键样式准备好前推迟初始渲染,以减少无样式内容闪烁和样式突然变化。
因此,把关键 CSS 尽早放在 <head> 的主要目的,是让浏览器尽早发现并并行下载它,缩短 render-blocking 时间,而不是让 HTML 解析停下来。把 CSS 放在文档尾部可能导致首屏渲染等待更久;是否出现 FOUC 取决于浏览器、样式的发现方式、资源是否关键以及页面是否动态插入样式,不能写成“必然先显示无样式页面”。
CSS 的三个边界
- render-blocking:适用的普通样式表通常会影响初始渲染;媒体条件不匹配的样式表可能不阻塞当前渲染。
- parser-blocking:样式表通常不直接暂停 HTML parser。
- script-blocking:某些已被 parser 发现、且位于脚本之前的适用样式表,可能让后续 parser-inserted classic script 等待,以避免脚本读取尚未生效的样式。
CSS 规则建议:
- 关键 CSS 使用普通
<link rel="stylesheet">放在<head>; - 避免用大量 CSS
@import形成串行请求链; - 非关键样式可以使用合适的
media、拆分文件或经过测试的加载策略,但要评估 FOUC、无障碍和首屏体验; rel="preload" as="style"只是资源提示,必须正确转化为 stylesheet 并验证 CSP、缓存和失败回退,不是无条件的性能优化。
为什么传统教程让 script 放在尾部?
没有 async/defer 的 parser-inserted classic script 通常会让 HTML parser 等待脚本获取和执行:
<script src="/legacy/app.js"></script>
大致过程是:
- 浏览器解析 HTML,并可能由 preload scanner 提前发现资源;
- parser 遇到普通 classic script;
- 浏览器获取脚本(如果尚未获取);
- parser 等待脚本执行完成;
- 脚本执行结束后继续解析 HTML。
脚本执行期间会占用当前 JavaScript agent,长任务会延迟输入、布局和绘制。网络获取和脚本执行不是同一个阶段,不能一概说浏览器“假死”;真正很容易造成长时间阻塞的通常是同步脚本执行、庞大依赖和主线程长任务。
把脚本放在 </body> 前可以让大部分 DOM 先被解析,因而是早期没有模块化加载策略时的实用经验:
<body>
<main id="app"></main>
<script src="/legacy/app.js"></script>
</body>
但现代页面不必把所有脚本都放到尾部。只要脚本使用合适的加载模式,放在 <head> 也可以兼顾资源早发现和解析性能。
async 与 defer
这两个属性主要改变外部 classic script 的获取和执行方式;它们不是“只有放在 <head> 才有效”的属性。
defer
<head>
<script defer src="/vendor.js"></script>
<script defer src="/app.js"></script>
</head>
对 parser-inserted 外部 classic script,defer 通常表示:
- HTML parser 继续工作,脚本可以并行获取;
- HTML 解析完成后执行;
- 多个
deferclassic script 按文档顺序执行; - 执行通常发生在
DOMContentLoaded之前,因此DOMContentLoaded会等待它们。
defer 适合有顺序关系、需要 DOM 或作为页面主应用入口的脚本。它放在 body 尾部仍有语义,只是提前发现资源的收益可能变小。
async
<head>
<script async src="https://analytics.example.test/analytics.js"></script>
</head>
async 脚本获取时不阻塞 HTML parser,脚本一旦准备好就可能执行;它不保证多个脚本之间的顺序,也不保证 DOM 已经解析完成。它适合相互独立的统计、广告或功能探测脚本,但脚本本身必须处理执行时 DOM 尚未出现的情况。
type="module"
<head>
<script type="module" src="/src/main.js"></script>
</head>
模块脚本默认具有类似 defer 的非阻塞解析特征,并且还会按依赖图获取模块。模块是严格模式,支持静态 import;不能把模块脚本与 classic script 的所有细节混为一谈。
defer对 module script 没有额外意义;<script type="module" async>会让模块依赖图准备好后尽快执行,顺序不再等同于默认 module/defer;- 模块脚本的跨源加载、CORS、MIME 类型和 CSP 也必须正确配置;
- 动态创建的脚本有自己的异步行为,不能简单说“动态脚本的 async/defer 都无效”。如果需要动态 classic 脚本按插入顺序执行,可在插入前设置
script.async = false,但它仍不是 parser-blocking。
选择表
| 场景 | 常见选择 | 主要语义 |
|---|---|---|
| 页面主入口、脚本有依赖且需要 DOM | defer classic 或默认 module | 解析期间获取,解析后按规则执行 |
| 互相独立的统计/第三方脚本 | async | 谁先准备好谁先执行,不保证顺序 |
| 现代模块应用 | type="module" | 默认类似 defer,使用模块依赖图 |
| 传统无构建页面、脚本很少 | body 尾部 classic | 仍可用,但需自己处理依赖和长任务 |
<head> 中 script 与 CSS 的顺序
原文“JS 最好放在外链 CSS 前面”不能作为通用结论。若是一个普通 parser-inserted classic script,并且脚本可能读取计算样式,通常更安全的顺序是先让浏览器发现适用 CSS,再遇到脚本:
<head>
<link rel="stylesheet" href="/assets/app.css">
<script src="/assets/read-layout.js"></script>
</head>
这样,位于脚本之前的 script-blocking stylesheet 可能让脚本等待,脚本读取布局时更有机会看到已经准备好的样式。如果把普通同步脚本放在 CSS 前面,后面的样式表不会追溯性地阻塞已经遇到的脚本,脚本可能在该样式生效前读取布局。
不过这不意味着应该把所有脚本改成同步脚本:
<head>
<link rel="stylesheet" href="/assets/app.css">
<script defer src="/assets/app.js"></script>
</head>
对于 defer/module 脚本,应根据依赖图、资源优先级和实际测量决定 CSS 与脚本的声明顺序;对于 async 或动态脚本,更不能套用“CSS 全部下载后才执行”的绝对规则。脚本是否读取样式、样式表是否适用、是否动态插入,都会影响时序。
一个现代的基础模板
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/assets/app.css">
<script type="module" src="/assets/main.js"></script>
<script async src="/assets/analytics.js"></script>
</head>
<body>
<main id="app"></main>
</body>
</html>
这里关键 CSS 尽早发现;主应用使用默认 defer 特征的 module script;独立统计脚本使用 async。实际项目仍应通过浏览器 Performance、网络面板和 Core Web Vitals 验证,而不是把模板当作性能银弹。
小结
- CSS 放
<head>的重点是尽早发现关键样式并缩短初始 render-blocking 时间;CSS 通常不直接阻塞 HTML parser。 - 普通 classic script 会阻塞 parser;body 尾部是历史策略,现代页面可使用
defer或 module。 async不保证顺序和 DOM 就绪,deferclassic 通常按文档顺序并在DOMContentLoaded前执行。- module script 默认类似 defer,
asyncmodule 则改变为依赖图就绪即执行。 - 脚本与 CSS 的关系要区分 render-blocking、parser-blocking 和 script-blocking,不能用“JS 一定放 CSS 前”概括。
- 资源加载、长任务、依赖图和真实网络环境都需要测量。