这篇文章是 Google 官方 Web 开发者网站(web.dev)中关于 Web 性能优化 的核心教程,主要讲解了"关键渲染路径(Critical Rendering Path,简称 CRP)"的概念。
简单来说,它解释了:当浏览器拿到 HTML 代码后,到底经历了哪些步骤,才把一个空白的屏幕变成用户能看到的网页,以及我们如何优化这个过程让网页打开得更快。
以下是这篇文章的核心干货总结:
1. 什么是关键渲染路径 (CRP)?
关键渲染路径是指浏览器在首次在屏幕上渲染(画出)网页内容之前,必须执行的一系列步骤。 它的核心目的在于平衡:既不能太早渲染(导致网页格式错乱、空白或破损),也不能太晚渲染(让用户看着白屏等太久)。
2. 浏览器渲染网页的 8 个步骤
当浏览器下载完 HTML 后,会按顺序执行以下操作:
- 构建 DOM 树:把 HTML 文本解析成浏览器看得懂的网页结构(文档对象模型)。
- 构建 CSSOM 树:把 CSS 样式表解析成样式结构(CSS 对象模型)。
- 执行 JavaScript:运行会修改 DOM 或 CSSOM 的 JS 代码。
- 合并渲染树 (Render Tree) :把 DOM 和 CSSOM 结合起来,确定哪些节点需要显示、各自应用什么样式。
- 布局 (Layout) :计算每个元素在屏幕上的具体位置和大小(排版)。
- 绘制 (Paint) :在内存中把元素的像素(颜色、边框、背景等)画出来。
- 合成 (Composite) :如果层级有重叠,把不同的图层合并。
- 显现 (Display) :最终把像素真正绘制到用户的屏幕上。
3. 谁在拖慢网页的首屏加载?
并不是所有资源都会阻止网页的第一次渲染。文章把资源分成了两类:
🚫 会阻塞的资源(必须等它们下载并处理完,网页才能显示)
- HTML 的基础部分。
<head>中的 CSS:默认会"阻塞渲染"。浏览器不看完 CSS,绝对不敢轻易画页面,怕画出来的奇丑无比。<head>中的同步 JavaScript:默认会"阻塞解析器"和"阻塞渲染"。因为 JS 随时可能删改网页内容,浏览器遇到 JS 只能停下手里所有的 HTML 解析工作,等 JS 执行完。
✅ 不会阻塞初始渲染的资源(可以边渲染边加载)
- 图片、字体:它们加载慢,浏览器会先给它们留出空白,等加载完了再填进去(但如果不提前写好尺寸,图片加载出来后可能导致页面突然"跳动",这被称为 CLS 布局偏移)。
- 非关键 JS/CSS :比如放在页面最底部的脚本,或者带有
defer、async属性的 JS。
4. 延伸:内容渲染路径 (LCP)
过去大家只关注"第一次出画面(First Paint)",但现在更关注 LCP(最大内容渲染) ------即用户真正关心的核心大图或大段文字什么时候出来。优化关键路径,最终就是为了让用户更早看到有用的信息。
提问
非关键 JS/CSS:比如放在页面最底部的脚本,或者带有 defer、async 属性的 JS。这个的话,也会改变页面的元素吧?比如css,先加载完head的css,再加载body的css,会不会出现那种颜色转变的情况
你直觉非常敏锐!你的推断完全正确:确实会出现这种情况。 在前端开发中,这种因为 CSS 加载滞后导致页面样式突然跳变或闪烁的现象,有一个专门的术语叫做 FOUC(Flash of Unstyled Content,无样式内容闪烁) 。
下面我们具体拆解一下,你提到的非关键 JS 和 CSS 延迟加载时,页面究竟会发生什么:
1. 延后加载 CSS:确实会导致"颜色转变"或"结构跳动"
正如你所说,如果把一部分 CSS 放在 <body> 底部,或者通过 JavaScript 异步加载,浏览器处理的过程是这样的:
- 初始渲染 :浏览器解析完
<head>里的关键 CSS,发现页面结构已经有了,于是启动"关键渲染路径",把网页画出来。此时,页面用的是<head>里的临时样式(或者干脆是浏览器的默认白底黑字)。 - 异步 CSS 下载完成:当页面已经显示在用户眼前时,底部的非关键 CSS 终于下载并解析完了(比如你提到的改变背景色的新样式)。
- 重新渲染(重绘/重排) :浏览器发现样式表更新了,被迫重新计算受影响元素的样式。
结果就是: 用户会看到网页先是 A 颜色(或没有颜色),突然闪烁一下,变成了 B 颜色。如果这个 CSS 还包含字体大小、宽高的改变,整个页面甚至会像地震一样"抖动"一下。
优化共识 :为了避免这种糟糕的视觉闪烁,行业内的标准做法是:绝对不要把会影响首屏视觉(如头部导航、首屏大图、主体背景色)的 CSS 延后加载。 只有用户滚动页面才能看到的"首屏下方"的样式,才适合做非关键 CSS 延迟加载。
2. 带有 defer 或 async 的 JS:也会改变元素吗?
会,而且它们的核心工作往往就是改变元素。 你可能会问: "既然 JS 会改变 DOM 元素,那浏览器为什么还敢让它们延迟(defer/async)执行,而不怕页面闪烁呢?"
这就是 defer 和 async 的精妙之处,它们对待"改变元素"的态度截然不同:
🛠️ defer(延迟执行)------ 稳健的网页改造者
- 特点 :它在后台下载,但一定会等到整个 HTML 解析完成(DOM 树构建完毕)之后,并在
DOMContentLoaded事件触发之前执行。 - 执行时机:此时,浏览器可能还没有进行第一次正式的像素绘制(Paint)。JS 在这时候修改元素、绑定点击事件,用户是完全感知不到"变化过程"的,因为当用户看到画面的那一刻,JS 已经悄悄把网页改造完了。
- 适用场景:绝大多数需要操作 DOM 元素的业务逻辑脚本。
⚡ async(异步执行)------ 乱序的野蛮生长者
- 特点 :它在后台下载,什么时候下载完,就立刻暂停 HTML 解析并执行。它的执行时机是完全不可控的。
- 执行时机:如果它下载得很慢,此时页面可能已经渲染好并呈现给用户了。一旦它突然执行并修改了页面上的元素(比如插入一个广告、改变一段文字),用户就会看到页面内容突然"无中生有"或发生跳变。
- 适用场景 :它不适合 用来做依赖页面元素的交互。它通常用于与页面样式无关的独立脚本,比如 百度统计、谷歌分析(Analytics)代码、广告追踪脚本 等。即使它们迟到或者突然执行,也不会影响核心用户体验。
总结
你的顾虑非常高瞻远瞩。在实际的 Web 性能优化中,我们面临的正是这种两难选择:
- 如果把资源都放在
<head>里,页面载入慢(白屏时间长),但一旦显示就是完美的。 - 如果把资源都往后挪(异步化),页面载入极快,但很容易出现你提到的颜色转变、内容闪烁或布局跳动。
优秀的开发者所做的,就是在这两者之间找到平衡:只把决定首屏长相的最少 CSS 留在 <head> 中,其余的全部延后。
那同步的css就不会吗?
同步的 CSS 也可能会,但它的"闪烁"机制和异步 CSS 完全不同。 这里有一个非常精妙的浏览器保护机制。我们先给出一个核心结论:
- 异步 CSS 导致的闪烁 :是网页从"丑(无样式)"突然变成"美(有样式)"。
- 同步 CSS 导致的闪烁 :通常是网页在加载过程中,由于代码写得不够好,导致元素从"位置 A"突然跳到 "位置 B" ,或者从 "没加载好"变成"加载好"。
为了让你彻底明白,我们需要拆开来看同步 CSS 在 <head> 中和在 <body> 中分别是怎么表现的:
1. 如果同步 CSS 放在 <head> 里(标准做法)
结论:绝对不会出现"颜色突然转变"的闪烁,但可能会有短暂的白屏。
这是因为浏览器有一个铁律: <head> 里的同步 CSS 会彻底阻塞网页的首次渲染。
当浏览器解析到 <head> 里的 <link rel="stylesheet"> 时,它会做两件事:
- 暂停把后面的
<body>渲染到屏幕上(保持白屏)。 - 全速下载并解析这个 CSS 文件,构建出完整的 CSSOM 树。
只有当 <head> 里的同步 CSS 全部下载并解析完成后,浏览器才会把合并好的"渲染树"画到屏幕上。因此,用户看到网页的第一眼,它就是带着正确颜色的、完美的。它跳过了"丑陋"的阶段,代价是用户在下载 CSS 时需要多看一会儿白屏。
2. 如果同步 CSS 错放在 <body> 中间(非标准做法)
结论:一定会发生闪烁!而且会造成严重的"逐步渲染闪烁"。
虽然它是"同步"的,但由于它被放在了 HTML 内容的中间,浏览器的处理会变得非常尴尬。现代浏览器(如 Chrome 和 Firefox)为了不让用户看太久白屏,通常会采用"分段阻断"或"强制闪烁"的策略。
想象一下这个场景:
HTML
xml
<body>
<div class="header">我是头部(黑色背景)</div>
<link rel="stylesheet" href="main.css">
<div class="content">我是主体内容</div>
</body>
当浏览器从上往下解析这段 HTML 时:
- 渲染上半部分 :浏览器先看到了
.header,此时还没看到后面的 CSS。为了不让用户干等,浏览器会先把.header按照默认样式(可能没有背景色)画在屏幕上。 - 遇到同步 CSS :浏览器突然读到了
main.css。因为它是同步的,浏览器必须停下来下载它。 - 样式覆盖与闪烁 :当
main.css下载完后,浏览器发现:"噢!原来.header是黑色背景啊!"于是浏览器被迫重新绘制.header。
结果: 用户会眼睁睁看着 .header 先跳出来(无样式),然后随着 main.css 下载完毕,它突然闪烁一下变成了黑色。
总结:为什么我们要追求"关键 CSS 内联"?
不管是同步还是异步,只要不符合浏览器的渲染天性,都会出问题:
- 全部同步放
<head>:页面不闪烁,但如果 CSS 文件太大,用户会面对长时间的白屏,体验极差。 - 异步加载 / 放底部 :页面秒开,但会发生你提到的样式突然转变和闪烁(FOUC) ,体验同样很差。
为了解决这个两难问题,目前前端性能优化的最高境界叫做 关键 CSS(Critical CSS) :
找出决定网页首屏(用户一眼看到的区域)长相的最少 CSS 代码 (通常只有几 KB),直接内联(Inline)写在
<head>的<style>标签里。 而剩下的大部队 CSS(比如滚动下去才能看到的底部样式、弹窗样式),则使用异步加载。
这样既保证了首屏秒开(解决白屏),又保证了首屏出来时样式就是完美的(解决闪烁),两全其美!