技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

天了噜,为什么外链 CSS 要放在头部,JS 要放在尾部?

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/91-天了噜,为什么外链css要放在头部,js要放在尾部?
本文目录7 个章节
  1. 为什么关键外链 CSS 通常放在 <head>?
  2. 为什么传统教程让 script 放在尾部?
  3. async 与 defer
  4. <head> 中 script 与 CSS 的顺序
  5. 一个现代的基础模板
  6. 小结
  7. 参考资料

天了噜,为什么外链 CSS 要放在头部,JS 要放在尾部?

Category(分类): JavaScript Status: 已整理

早期前端教程经常建议“CSS 放在 <head>,JS 放在 <body> 尾部”。这条经验有历史背景,但不能当作现代网页的绝对规则。理解它之前,需要把三个阶段分开:

  1. HTML 解析:HTML parser 读取并构造 DOM;
  2. 脚本获取与执行:脚本可能阻塞 parser,也可能异步执行;
  3. 样式计算与渲染:CSSOM、渲染树、布局和绘制需要适用的样式资源。

不同资源对这三个阶段的影响不同。现代页面通常优先使用头部 defertype="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>

大致过程是:

  1. 浏览器解析 HTML,并可能由 preload scanner 提前发现资源;
  2. parser 遇到普通 classic script;
  3. 浏览器获取脚本(如果尚未获取);
  4. parser 等待脚本执行完成;
  5. 脚本执行结束后继续解析 HTML。

脚本执行期间会占用当前 JavaScript agent,长任务会延迟输入、布局和绘制。网络获取和脚本执行不是同一个阶段,不能一概说浏览器“假死”;真正很容易造成长时间阻塞的通常是同步脚本执行、庞大依赖和主线程长任务。

把脚本放在 </body> 前可以让大部分 DOM 先被解析,因而是早期没有模块化加载策略时的实用经验:

<body>
  <main id="app"></main>
  <script src="/legacy/app.js"></script>
</body>

但现代页面不必把所有脚本都放到尾部。只要脚本使用合适的加载模式,放在 <head> 也可以兼顾资源早发现和解析性能。

asyncdefer

这两个属性主要改变外部 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 解析完成后执行;
  • 多个 defer classic 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。

选择表

场景常见选择主要语义
页面主入口、脚本有依赖且需要 DOMdefer 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 就绪,defer classic 通常按文档顺序并在 DOMContentLoaded 前执行。
  • module script 默认类似 defer,async module 则改变为依赖图就绪即执行。
  • 脚本与 CSS 的关系要区分 render-blocking、parser-blocking 和 script-blocking,不能用“JS 一定放 CSS 前”概括。
  • 资源加载、长任务、依赖图和真实网络环境都需要测量。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS