§ 4.6 · Section

浏览器渲染

The Critical Rendering Path

上一步服务器送回了一份 HTML 文本。但那还只是一堆字符——屏幕上此刻仍然什么都没有。这一节讲最后一段路:浏览器怎么把这堆字符变成你看得见的画面。前五节都在"等来回",这一节完全不同——它一个网络包都不发,纯粹在算

生活场景
🔨 你收到的是一箱散件加一本说明书

你网购了一个书柜,快递终于送到门口。但你现在还不能用它——箱子里是几十块板材、一袋螺丝,和一本装配说明书。

你要做的事有明确的顺序,一步都不能跳:
① 拆箱清点——把所有板材摊开,看清有几块、每块是干什么的(解析 HTML,建 DOM
② 读说明书——搞清哪块拼哪块、间距多少、朝哪个方向(解析 CSS,建 CSSOM
③ 把两者对上——手上这块板,说明书上说它该怎么装(合成渲染树
④ 量位置——在这块墙面上,每块板的具体坐标和尺寸是多少(布局 Layout
⑤ 真的装上去——上螺丝、贴饰面,让它有颜色有质感(绘制 Paint
⑥ 摆到房间里——和已有的家具一起构成你看到的整个房间(合成 Composite

这一节最重要的知识点就藏在第 ④ 和第 ⑤ 步的区别里:换个板子的颜色(重绘),和挪动一块板导致后面所有板都要重新量位置(重排)——后者的代价大得多。这个区别是网页性能优化的核心,本节会反复讲。

上一步刚完成了什么,这一步要干什么

接力棒交到这一节时,手里有的是一份 HTML 文本(§ 4.5 的产物)加上响应头里说明的类型与编码。除此之外什么都没有——屏幕上还是空白

这一节要干的事:把这份文本变成屏幕上一个个具体的像素。它和前五节有三个根本不同,理解这三点,你就理解了这一节的性质:

前五节(网络阶段)这一节(渲染阶段)
时间花在哪等来回——绝大部分是干等——CPU 和 GPU 在真的忙
快慢由什么决定物理距离(改不了)与对方架构你的设备性能与页面本身的复杂度
怎么优化减少来回次数、缩短距离(CDN)少干活儿——减少元素、减少重排、少写复杂样式
发生在哪横跨半个地球完全在你手里这台设备上
失败会怎样打不开、报错能打开但卡——这是最难查的一类问题

干完之后交出的东西就是你眼前的画面——整段旅程到此结束。

另外要说清本节的边界:这一节讲的是"浏览器怎么组织内容并算出位置和颜色",也就是渲染管线的上半段。至于"算出来的颜色值怎么变成屏幕上真正发光的像素点"——那属于图形与显示的范畴,更深入的渲染管线见界面篇 § 2.2,那里讲了位图与矢量、像素与分辨率这些更底层的东西。两处内容衔接在一起才是完整的画面生成过程。

先把这一节的术语翻译成人话

这一节的术语有个特点:缩写多、而且长得像(DOM、CSSOM、CSS、CRP)。先用一张表把它们分清:

术语换成大白话生活里对应的东西
HTML(HyperText Markup Language,超文本标记语言)说白了就是"用标签把内容标出结构的一份文本"一份带层级编号的笔记:一级标题、二级标题、正文、附图
DOM(Document Object Model,文档对象模型,读作"道姆")说白了就是"把那份文本变成一棵可以被程序操作的树"把散装的板材货架式摆好,每块贴上编号,随手就能取
CSS(Cascading Style Sheets,层叠样式表)说白了就是"一份规定每样东西长什么样的说明书"装修的施工图:哪面墙什么颜色、瓷砖多大、留多少缝
CSSOM(CSS Object Model,CSS 对象模型)说白了就是"把那份说明书也变成一棵树,方便查"把施工图整理成一本索引清晰的手册
渲染树(Render Tree)说白了就是"内容和样式对上号之后的那份最终清单"每块板旁边都贴好了对应的施工要求
布局(Layout,也叫 Reflow 回流)说白了就是"算每样东西的位置和大小"拿尺在墙上量:这块板放在离地 80 公分、宽 1 米 2
绘制(Paint)说白了就是"填颜色、画边框、上阴影"刷漆、贴饰面板
合成(Composite)说白了就是"把分开画的几层叠在一起成为最终画面"照片后期把几张透明胶片叠起来看
重排(Reflow / Relayout)说白了就是"位置得重新算一遍"挪动一块板,后面所有板的位置全得重量
重绘(Repaint)说白了就是"位置没变,只是颜色变了"把一块板重新刷个颜色,尺寸位置一点没动
关键渲染路径(Critical Rendering Path,CRP)说白了就是"从收到文本到画出第一屏,必须走完的那几步"从收货到"书柜能站起来"这条最短装配流程
阻塞渲染(Render-Blocking)说白了就是"这东西没到位,我什么都不敢画"施工图还没送到,工人不敢乱砌墙——砌错了得砸
视口(Viewport)说白了就是"屏幕上那块能看见内容的区域"透过电梯门缝看外面:世界很大,你只看到那一条

这张表里最要紧的是把"重排"和"重绘"分清重排要重新量位置,而且一处改动可能牵连一大片;重绘只是换颜色,位置完全不动。前者贵、后者便宜,而这个差价决定了一个网页是流畅还是卡顿。这一节后面会专门用一整段讲它。

Analogy · 渲染管线就是一条装修流水线

打个比方,把整条渲染管线放到装修一间屋子这件事上,每一步都严丝合缝地对应:

① 清点材料 = 解析 HTML 建 DOM。货车把材料卸在门口,你得先把它们分门别类摊开:这堆是地板、这堆是踢脚线、这几块是柜门。摊清楚了才知道手上有什么。

② 读施工图 = 解析 CSS 建 CSSOM。施工图告诉你每样材料该怎么用:地板铺哪个房间、缝留 2 毫米、墙漆用哪个色号。注意——图纸和材料是两份独立的东西,可以同时送到、同时处理。

③ 对号入座 = 合成渲染树。你把图纸上的要求标注到每样材料上。这一步会做一个重要的筛选:图纸上写着"备用间暂不施工"的部分,对应材料就直接搬走不管了——这就是为什么设了 display: none 的元素压根不进渲染树

④ 放线量尺 = 布局。拿激光水平仪在真实墙面上标出每一块的确切位置和尺寸。这一步是纯计算,最耗时,而且有强连锁性——第一块地板的起始位置挪 1 厘米,后面整个房间所有地板的位置全变。

⑤ 施工 = 绘制。真的刷漆、真的贴板。这时候才产生"看得见的东西"。

⑥ 家具入场 = 合成。各个工种分头做完的成果(墙、地、家具、灯)最终叠成你走进去看到的那个房间。现代浏览器会把某些元素单独放在一个"图层"上处理,好让它移动时不必惊动其他所有东西——相当于把那台带轮子的推车单独看待,推它的时候不用重新铺地板。

整条流水线最重要的性质是:越靠后的步骤越便宜,越靠前的步骤越贵。改一下家具位置(合成)几乎免费;重新量整个房间的尺寸(布局)代价很大;而如果发现材料清点错了(DOM 变化),那可能整条线都要重走。所以性能优化的第一原则就是:让改动尽量发生在流水线的末端。

第一步:解析 HTML,建起 DOM 树

浏览器拿到那份 HTML 文本,第一件事是把它读成一棵树。为什么必须是树?因为 HTML 本身就是嵌套结构——标签套标签,天然有层级关系。

这份 HTML:

<html>
  <head><title>某款商品</title></head>
  <body>
    <h1>商品名称</h1>
    <div class="price">¥199</div>
  </body>
</html>

会被读成这棵树(这就是 DOM):

html
├── head
│    └── title
│         └── "某款商品"(文本节点)
└── body
     ├── h1
     │    └── "商品名称"
     └── div.price
          └── "¥199"

为什么要转成树,而不是直接照着文本画?因为程序需要能随机访问和修改其中任何一部分。文本是线性的,你要找"第三个 div"就得从头数;而树结构可以直接跳到任意节点,还能问它"你的父节点是谁""你有几个子节点"。

换成大白话:这相当于把散装堆在地上的板材,改成上了编号、分门别类摆在货架上。堆在地上你也知道东西都在,但要取"第 7 块侧板"得翻一遍;上了架,伸手就拿。DOM 就是这个货架——它是"让内容可被程序操作"的形式。

解析这一步有三个值得知道的细节:

第二条那个"宽容"是 HTML 一个很有意思的历史性质。它宽容是因为它面对的是人手写的、几十年积累下来的、几十亿个页面——严格一点就会有大批老网页打不开。说白了:一个必须兼容全世界既有内容的格式,只能选择宽容。这个取舍带来的代价是解析规则复杂无比,好处是几乎没有网页会因为写错标签而完全打不开。

第二步:解析 CSS,建起 CSSOM 树

与解析 HTML 并行的另一条线,是解析 CSS 建 CSSOM。它的结构也是树,因为样式有继承关系——父元素的字体、颜色会往下传给子元素。

这份 CSS:

body      { font-size: 16px; color: #333; }
.price    { color: red; font-weight: bold; }
h1        { font-size: 24px; }

会被组织成(CSSOM,简化示意):

body(字号 16、颜色 #333)
├── h1     (字号 24 ← 自己声明的;颜色 #333 ← 从 body 继承)
└── .price (颜色 red ← 自己声明的;字号 16 ← 继承)

注意这里的继承h1 自己没写颜色,它就用了从 body 那儿继承来的 #333相当于装修图纸上写着"全屋统一用暖白墙漆",然后单独注明"儿童房用天蓝"——没被单独注明的房间,就沿用全屋的规定。这样图纸不用为每个房间重复写一遍。

这里有一个极重要的性质:CSS 会阻塞渲染。意思是——CSSOM 没建好之前,浏览器一个像素都不敢画。

为什么这么保守?因为如果先按默认样式画出来,等 CSS 到了再重画一遍,用户会看到页面"闪一下"、字体大小和颜色突变。那种体验比多等一会儿更糟。

换成大白话:这相当于工人拿到了材料但施工图还在路上。他有两个选择:凭感觉先砌(图纸到了发现错了,砸掉重砌,还得让业主看着难受),或者先等图纸(多等一会儿,但一次做对)。浏览器选择等。

这就推出了一条非常实际的优化原则:CSS 要尽早发出、尽量小、放在文档头部。因为它在关键路径上——它慢一秒,用户就多看一秒白屏。这也是为什么很多网站会把"第一屏必需的少量样式"直接内嵌在 HTML 里,剩下的样式表异步加载:把关键路径上的东西压到最小。

第三步:两棵树合成渲染树

DOM 有了、CSSOM 有了,浏览器把它们对上号,生成渲染树(Render Tree)。这一步做的事很简单,但有一个筛选动作值得注意。

DOM(我有什么)  +  CSSOM(每样该长什么样)  =  渲染树(要画什么、怎么画)

★ 筛选规则:
  · 不可见的东西不进渲染树
      · <head> 里的内容(它本来就不显示)
      · 设了 display: none 的元素 —— 完全不进
  · 但设了 visibility: hidden 的元素「进」渲染树
      —— 因为它虽然看不见,却仍然占着位置!

这个区别很实际:
  display: none      = 这块板搬走了,后面的板会往前挪补上空位
  visibility: hidden = 这块板还在原位,只是变透明了,位置照占

这个区分是前端最常考也最容易记混的一点,用装修的说法就一目了然:display: none 是"把柜子搬走",visibility: hidden 是"用一块透明布把柜子罩住"。后者你看不见柜子,但你也放不了别的东西进那个位置。

还有第三种情况值得一起记:opacity: 0(完全透明)。它比 visibility: hidden 更"轻"——元素不仅占位,而且仍然能被点击。相当于那块透明布不但罩着柜子,还是可以透过去开柜门的。

写法看得见吗占位置吗能点击吗装修里对应
display: none柜子搬走了,后面的东西往前补位
visibility: hidden柜子罩了不透明布,位置照占
opacity: 0柜子罩了透明布,还能伸手开门

为什么要花篇幅讲这三个?因为它们的性能代价差别很大:改 display 会触发重排(位置全变),改 opacity 往往只需要重新合成(最便宜)。这一点马上就要讲到。

第四步:布局——算出每样东西的确切位置

渲染树告诉浏览器"要画什么、每样长什么样",但还缺最要紧的一件事:每样东西到底在屏幕上的哪个位置、多宽多高。算这件事的过程叫布局(Layout,在部分文档里也叫 Reflow 回流)。

这一步为什么难?因为网页的尺寸是相对的、互相依赖的。你写 width: 50%,那 50% 到底是多少像素?取决于父元素多宽。父元素又写了 width: auto,那它多宽?取决于它父元素,以及它自己里面装了多少内容。这是一张互相牵连的计算网络,不是一次简单的赋值。

一个具体的连锁例子:

屏幕宽 390px
  └─ body(宽度 auto → 390px)
       └─ .container(padding 16px → 内容区 358px)
            └─ .card(width 50% → 179px)
                 └─ 一段文字(要换行吗?取决于 179px 装得下几个字)
                      → 换了 3 行 → .card 高度 = 3 × 行高 + 内边距
                           → .container 高度也跟着变
                                → 它后面所有元素的位置全部下移

★ 注意最后那三行的连锁:一段文字多换了一行,
  可能导致这个元素之后的全部内容位置改变。
  这就是布局的「连锁性」——也是它昂贵的根本原因。

换成大白话:这相当于在墙上贴一排瓷砖。第一块的位置定了,第二块跟着它、第三块跟着第二块……你把第一块往左挪一厘米,后面几十块的位置全部要重新量。而如果你只是把第五块换个颜色,其他块一动不动。

这个区别就是本节最重要的知识点,下面单独讲。但先记住布局这一步的两个特点:

第五步与第六步:绘制与合成

位置算好了,终于可以画。绘制(Paint)这一步做的是:按照渲染树上的样式,把每个元素的颜色、边框、背景、阴影、文字真的画出来。

但现代浏览器不是"在一张画布上从头画到尾",而是分层画,然后再叠起来。这最后一步叫合成(Composite)。

为什么要分层?看这个场景:

页面上有一个固定在顶部的导航栏,用户在滑动下面的内容。

不分层的做法:
  用户每滑动一点,整个页面重新画一遍
  (导航栏明明没变,也要重画)

分层的做法:
  图层 1:导航栏      ← 完全不动,画一次就够
  图层 2:主内容      ← 只需要改变它的位置偏移
  图层 3:一个动画元素 ← 单独动,不影响其他层

  滑动时只需要重新「叠」这几层,
  而不需要重新「画」任何一层。

★ 而"叠图层"这件事可以交给 GPU 做,它极其擅长这个。
  这就是为什么某些动画特别流畅——它们只触发合成,不触发绘制和布局。

打个比方,这相当于旧时代的动画制作方式。画一个人在固定背景前走路,不需要每一帧都把背景重画一遍——背景画一张固定的,人物画在透明胶片上,每帧只换胶片,叠起来拍。一百帧动画,背景只画了一次。

了解这一层有个非常实际的用处:做动画时,用某些属性会比用另一些属性流畅得多。原因就是它们触发的管线阶段不同:

你改动什么会触发哪些阶段代价装修里对应
宽高、边距、字号、增删元素布局 → 绘制 → 合成(全走一遍)最贵挪动一块板,后面全部重新量尺、重贴、重摆
颜色、背景色、阴影、边框色绘制 → 合成(跳过布局)中等位置不动,只重新刷一遍漆
transform(位移/缩放/旋转)、opacity只合成最便宜那块板本来就在带轮子的架子上,直接推一下

这张表是"网页动画为什么有的流畅有的卡"的完整答案:能用 transform 实现的位移,就不要用改 left 来实现。两者视觉效果一样,但一个只需重新叠层、一个要重新量整个房间。

说白了:同样是把柜子挪到墙角,一种做法是推一下带轮子的柜子,另一种做法是把整间屋子的地砖重铺一遍好让柜子落在新位置上。视觉结果相同,工作量差了几个数量级。

重排与重绘:本节最该记住的一个区别

现在正式讲这一节的核心。页面不是画一次就完了——用户滚动、点击、输入,脚本增删元素、改样式,都会让浏览器重新走一遍管线的一部分。走多少,取决于你动了什么。

重排(Reflow / Relayout)重绘(Repaint)
中文含义重新算位置和尺寸重新上颜色
触发什么改宽高、边距、字号、位置、增删元素、改 display、读某些尺寸属性改颜色、背景、边框颜色、阴影、visibility
要走管线的哪几步布局 → 绘制 → 合成绘制 → 合成(跳过布局)
影响范围可能牵连一大片——甚至整个页面通常只影响改动的那一块
代价相对便宜
关系重排必然引起重绘(位置变了当然要重画)重绘不一定引起重排(换个颜色不影响位置)
装修里对应挪动一块地板 → 后面所有地板重新放线、重铺把一面墙重新刷个色 → 尺寸位置一点没动

第六行那个单向关系值得抄下来记住:重排必然带来重绘,重绘不一定带来重排。所以如果你只能记住一句话,就记:能只改颜色就别改尺寸,能只推图层就别改颜色。

而在这个基础上,还有一个更隐蔽、也更容易写出来的性能陷阱,叫强制同步布局(也常被叫作"布局抖动")。它的成因是:浏览器本来会把多次改动攒在一起,一次性算完;但如果你在改动之后立刻去"读"某个尺寸,它就被迫马上算一遍给你。

糟糕的写法(伪代码示意):

  for (每个元素) {
      元素.宽度 = 元素.当前宽度 + 10     ← 改一下
      读取 元素.当前高度                  ← 立刻又读!
  }

  ★ 每一轮"改完立刻读",都强迫浏览器重新算一次布局。
    一百个元素 = 一百次完整布局计算。

好的写法:

  for (每个元素) { 读取所有需要的尺寸,先存起来 }   ← 先全部读完
  for (每个元素) { 用存下来的值统一改 }             ← 再全部改完

  ★ 读写分开,浏览器只需算一次。

换成大白话:这相当于装修时的两种做法。糟糕的做法是"挪一块板 → 立刻拿尺量整个房间 → 再挪一块 → 再量一遍整个房间",量了一百次。好的做法是先把一百块板全部挪到位,最后量一次。量房这个动作本身很贵,所以要攒着一起做,而不是每动一下就量一遍。

这类问题在实践中极其常见,而且难查——因为代码看起来完全正常,功能也完全正确,只是慢。而"慢"这个症状不会报错、不会崩溃,只会让用户觉得"这网站不太行"。

关键渲染路径:从收到文本到看见第一屏

把前面六步串起来,就得到关键渲染路径(Critical Rendering Path)——"从拿到 HTML 到画出第一屏,必须走完的最短流程"。理解它有个很实际的用处:你能判断出一个网页白屏时间长的原因,到底卡在哪一环。

关键渲染路径(必须依次完成,不能跳):

 收到 HTML
   │
   ├─ 解析 HTML ────────────┐
   │     (遇到脚本会暂停)    │
   │                         ├→ 两条线并行进行
   ├─ 下载并解析 CSS ────────┘
   │     ★ 这一步会阻塞渲染:CSSOM 没好,什么都不画
   │
   ├─ DOM + CSSOM → 渲染树
   │     (display:none 的元素在这一步被剔除)
   │
   ├─ 布局:算出每个元素的坐标与尺寸
   │     ★ 纯计算,最耗时,有连锁性
   │
   ├─ 绘制:真的填颜色、画边框、渲文字
   │
   └─ 合成:把各图层叠成最终画面 → 用户终于看见了

★ 优化关键渲染路径,就三件事:
  ① 减少这条路上必须的资源数量(少几个阻塞的 CSS 和脚本)
  ② 减少它们的体积(压缩、只留首屏必需的样式)
  ③ 减少要算的元素数量(DOM 别太深太大)

这三条优化里,第一条效果最直接。因为关键路径上的资源是串行的:少一个阻塞资源,就少等一份下载时间。相当于装修队开工前要等三份图纸,你把其中两份改成"可以后补",工期立刻缩短。

这里也能看出本章前后的呼应:§ 4.5 讲的"CSS 和同步脚本会阻塞渲染",在这里得到了完整解释——它们阻塞的正是这条关键路径。而 § 4.2 讲的 preconnect、§ 4.3 讲的连接复用,作用都是让这条路上的资源更早到达。说白了,六节讲的所有优化手段,最终服务的都是同一件事:让用户早一点看到第一屏。

为什么"看到内容"和"能操作"是两件事

有一个体验你一定遇到过:页面明明已经显示出来了,你点按钮却毫无反应,过了一两秒才突然能点。这不是错觉,也不是网络问题,是渲染与脚本执行的分工造成的。

原因在于:浏览器只有一个主线程在同时负责"画页面"和"跑脚本"。这两件事抢同一个人的时间。

时间轴:

[画出第一屏]  用户看到内容了 → 他开始尝试点击
     │
[跑一大段脚本]  主线程被占满,忙着执行 JS
     │           ★ 用户的点击被排进队列,但没人处理
     │
[脚本跑完]    主线程终于空了 → 才开始处理刚才那些点击
     │
[响应用户]    按钮终于有反应了

★ 用户的感受:「卡了一下」「点了没反应」
  真实情况:主线程在忙别的,你的点击一直在排队等着

换成大白话:这相当于一家只有一个店员的店。他既要摆货又要收银。货摆好了(页面画出来了),顾客拿着东西来结账,但店员正在后面搬箱子——顾客站在收银台前干等,还以为店没人。

这个机制解释了两个常见现象:

而这也解释了前面讲的"只触发合成的动画更流畅"为什么这么有价值:因为合成这件事可以交给 GPU,不占主线程——所以即使主线程在忙,那个动画照样能流畅地动。相当于推那台带轮子的车不需要店员动手,顾客自己就推走了。

五种真实的"能打开但慢",以及各自的病根

这一节的排查价值集中在这里。渲染阶段的问题和网络阶段完全不同:它不报错、不打不开,只是慢或卡。下面五种最常见,症状各不相同:

症状病根在哪一步怎么确认怎么治
白屏一两秒,然后整页突然出来关键渲染路径上有阻塞资源开发者工具里看有几个阻塞渲染的 CSS/脚本首屏样式内联,其余异步;脚本加 defer
内容出来了但排版跳动几次布局被反复重算(常因图片没写尺寸、字体后到)录一段加载过程逐帧看给图片写明宽高,给容器留好位置
看得见但点不动主线程被长时间的脚本占住Performance 面板里看有没有很长的一段任务把大任务切小、挪到空闲时做
滚动或动画一卡一卡动画触发了布局或绘制,不只是合成Performance 面板看每帧有没有 Layout/Paint改用 transformopacity 做动画
页面越用越卡元素越来越多,或者反复触发强制同步布局看 DOM 节点数是否持续增长及时清理不用的节点;读写分离

第二行那个"排版跳动"值得多说一句,因为它是最影响体验、也最容易修的一种。成因是图片没有声明尺寸:浏览器不知道这张图多大,只能先当它不占地方,等图下载完知道尺寸了,才把它下面的内容全部往下推。

这就是为什么你正要点一个按钮,页面突然一跳,你点到了别的东西。相当于装修师傅不知道那台冰箱多宽,先把橱柜都排好了,等冰箱送到发现放不下,只能把整排橱柜往旁边挪——而如果一开始图纸上就写明"此处预留 60 公分",什么都不用挪。

修法极其简单:给每张图写明宽高。这样浏览器一开始就能按比例留出正确的空位,图片到了直接填进去,一点都不用挪。说白了:提前留位置,比事后挪东西便宜得多。

渲染阶段与网络阶段:两种"慢"的完整对照

这一章走到这里,可以把两类性能问题彻底摆在一起对比了。"网站慢"这三个字底下藏着两种完全不同的病,而绝大多数人分不清。

网络阶段慢(§ 4.1–4.5)渲染阶段慢(本节)
本质原因路太远、来回太多活太多、算不完
换个更好的网络有用吗有用(尤其换到离服务器近的网络)完全没用
换个更快的手机有用吗几乎没用有用
典型症状白屏干等,什么都不出内容出来了但卡、跳、点不动
怎么看出来开发者工具 Timing 里的 DNS/连接/TLS/TTFB 数字大Timing 数字都很小,但 Performance 面板里主线程很忙
网站方怎么治用 CDN、减少来回、启用新协议版本精简页面、减少阻塞资源、少触发重排
用户侧能做什么换网络、换 DNS几乎无能为力(只能换设备)
生活里对应去很远的店买东西,路上花了两小时店就在楼下,但店员一个人要摆一百箱货

第二行和第三行的对照是这张表最有用的部分:网络阶段慢,换手机没有任何帮助;渲染阶段慢,换网络没有任何帮助。而人们抱怨"网站慢"时,第一反应几乎总是重启路由器——那只对其中一种病有效。

换成大白话:一个是"路远",一个是"活多"。修路解决不了人手不够,加人也缩不短路程。诊断的第一步永远是先分清这两个。

动手实验:亲眼看完整条管线

这一节讲的每一步在浏览器里都能看到,而且看得非常清楚。三个实验,做完你对整章的理解会完全落地。

第一件:看渲染管线的每一步。

F12 → Performance(性能)面板 → 点录制 → 刷新页面 → 停止录制

你会看到一条时间轴,上面用不同颜色标着:
  紫色 / Layout    ← 布局(本节第四步)
  绿色 / Paint     ← 绘制(第五步)
  绿色 / Composite ← 合成(第六步)
  黄色 / Scripting ← 脚本执行(抢主线程的那个)

★ 值得留意的三件事:
  ① 找出最长的那一条黄色块 —— 那就是让页面"点不动"的元凶
  ② 看有没有大量重复出现的紫色块 —— 那是反复触发布局(布局抖动)
  ③ 对比一个简单网页和一个复杂网页 —— 差距会非常直观

第二件:亲手制造一次重排与一次重绘,看代价差别。

在 Console(控制台)里试这两段(在任意网页上都能跑):

// 只触发重绘 —— 改颜色,位置不变
document.body.style.backgroundColor = '#f0f0f0';

// 触发重排 —— 改字号,整页文字位置全变
document.body.style.fontSize = '20px';

★ 在 Performance 面板录制时分别执行这两句,
  你会看到第二句多出了一大块 Layout 时间。
  这就是"重排比重绘贵"这句话的具体形状。

第三件:看 DOM 到底长什么样。在 Console 里执行 document.body.children,浏览器会把 DOM 树展开给你看,可以一层层点进去。这一下能把"DOM 是一棵树"这句抽象的话变成一个你亲手翻过的结构。

另外还可以试 document.querySelectorAll('*').length——它告诉你这个页面一共有多少个元素。拿几个网站对比一下你会很有感触:简洁的页面可能只有几百个,而一些复杂的商业页面能到几千甚至上万个。相当于一个房间摆五件家具和摆五百件家具,量房这件事的工作量根本不是一个量级。

全章收官:把七个阶段串成一句话

这是本章最后一节,值得把整条链子完整地过一遍。你按下回车之后发生的全部事情,压缩成一张图:

你点了 https://shop.example.com/item/8848
  │
① 本机准备(§ 4.1)
  判断意图 → 补全协议 → 查 HSTS → 拆成七段 → 查各级缓存
  ★ 缓存命中就直接跳到 ⑦,一个包都不发
  │
② 查号(§ 4.2)
  浏览器缓存 → 系统缓存/hosts → 递归解析器 → 根 → 顶级域 → 权威
  产出:一个 IP 地址
  │
③ 接线(§ 4.3)
  分配临时源端口 → 三次握手(一个 RTT)→ 双方各记一笔
  产出:一条可靠通道 + 一个四元组
  │
④ 验身份与上锁(§ 4.4)
  ClientHello → 证书链 → 验四项 → 各自算出同一把会话密钥
  产出:一条已验明身份的加密通道(TLS 1.3 一个 RTT)
  │
⑤ 说正事(§ 4.5)
  请求行 + 十来条头 → 服务器过 CDN/负载均衡/网关/应用/数据库
  产出:状态码 + 响应头 + 一份 HTML
  │
⑥ 画出来(§ 4.6)
  解析 HTML→DOM ┐
                 ├→ 渲染树 → 布局 → 绘制 → 合成 → 像素
  解析 CSS→CSSOM ┘
  │
⑦ 你看见了页面

时间去了哪:
  ①、⑥ 是「算」—— 由你的设备决定
  ②、③、④、⑤ 是「等来回」—— 由物理距离决定
  ★ 所以优化只有两条路:少等几个来回,或者少算一些活。

如果整章只让你带走一句话,那就是最后那两行:网络阶段的时间花在"等",渲染阶段的时间花在"算"。前者靠减少来回和缩短距离来治,后者靠少干活儿来治。

而如果让你带走第二句,那就是这个:这七个阶段严格串行,前一步的产物就是后一步的输入。正因如此,任何一环出问题都表现为"网站打不开"——但每一环的病症、查法、治法完全不同。本章六节各自末尾的那张故障表,合起来就是一份完整的排查手册。

最后回到本章开头那个问题:"点一下就开了"这句话,中间被省略掉的是什么?现在你知道了——被省略掉的是七个阶段、四层协议、至少三个来回、一次证书链验证、几十个并行请求,以及一条从字符到像素的完整流水线。而它们全都发生在你察觉不到的半秒钟里。

为什么要分成 DOM 和 CSSOM 两棵树

有个很自然的疑问:既然最后要合成一棵渲染树,为什么中间要先建两棵?直接一边读 HTML 一边套样式不行吗?

不行,而且理由很实在。因为一个元素最终长什么样,可能被后面才出现的规则改掉。

假设浏览器读到一半就急着上样式:

  读到 <div class="price">¥199</div>
  此刻已知的规则只有:.price { color: red }
  → 于是它按红色画了

  再往下读,样式表后半段还有:
  body .price { color: green }   ← 这条更具体,应该赢

  ★ 结果:刚画的红色白画了,还得改成绿色。
    用户会看到颜色闪一下。

CSS 的名字里那个"层叠"(Cascading)就是在说这件事:同一个元素可能被好几条规则同时命中,最终按"谁更具体、谁更靠后"来决胜。而"谁更具体"这个判断,必须等所有规则都读完才能下结论

换成大白话:这相当于装修图纸有好几页,后面几页会修正前面的说法。第 3 页写"全屋刷白",第 17 页写"客厅那面墙改成灰"。如果工人看到第 3 页就动手刷白,那第 17 页一到就得重刷——所以正确做法是把整本图纸看完,汇总出一份最终结论,再开工。

这解释了三件事,串起来看很清楚:

而 HTML 恰恰相反——它可以边读边建,因为内容的结构不会被后面的内容改掉(前面写的那个 div 就是那个 div,不会因为后面出现别的标签而变性质)。一个可以增量处理,一个必须整体汇总——这就是它们被分成两棵树的根本原因。

渲染是"一直在发生"的,不是画完就结束

最后要纠正一个容易形成的错觉:看完前面六步,你可能以为渲染是"一次性完成的开机动作"。其实不是。只要页面还开着,这条管线就一直在反复运转。

页面打开后,仍会持续触发管线的原因:

  · 你滚动页面           → 至少要重新合成
  · 你把鼠标放在按钮上   → 样式变了 → 重绘
  · 你在输入框里打字     → 内容变了 → 可能重排
  · 页面上有个动画在跑   → 每秒要走管线几十次
  · 脚本定时更新某个数字 → 每次更新都触发一小段管线
  · 图片陆续加载完成     → 每张图到位都可能引起重排
  · 你旋转手机           → 视口变了 → 整页重新布局

★ 所以"页面卡不卡"考的不是那一次首屏渲染,
  而是它在你使用过程中反复走管线时够不够省。

这个视角很重要,因为它解释了为什么有些页面"打开很快但用起来很难受":首屏优化做得好,但每次交互都触发大范围重排。相当于一家店开门特别快,但你每买一件东西,店员都要把整个仓库重新盘一遍点。

为了让这件事不失控,浏览器有个重要机制:它把管线的运转对齐到屏幕的刷新节奏上——屏幕多久刷一次,它就最多算多少次。

换成大白话:屏幕就像一台每秒拍几十张的相机,只在快门开的那一瞬间取画面。浏览器算得再快也没意义——快门没开,画出来也没人看到。所以它索引把工作节奏和快门对齐,不做无用功。

这也带出一个很实际的判断标准:每一帧留给浏览器干活的时间是有硬上限的。如果某一帧里的活儿没干完,那一帧就"丢了"——用户看到的就是卡顿。所以性能优化的目标从来不是"越快越好",而是"每一帧都能在配额内干完"。

相当于一条按固定节拍前进的流水线你不需要比节拍更快,你只需要每一拍都别掉队。某一个工位偶尔慢半拍,整条线上的产品就少一件——这正是"掉帧"的形状。

Recap · 收束

这一节是全章唯一不发一个网络包、纯粹在算的阶段。它的快慢由你的设备与页面复杂度决定,和网速毫无关系。
关键渲染路径六步:解析 HTML 建 DOM → 解析 CSS 建 CSSOM(这两条并行)→ 合成渲染树display:none 的元素在这一步被剔除)→ 布局(算位置,纯计算,有连锁性,最耗时)→ 绘制(填颜色,才第一次产生可见结果)→ 合成(把各图层叠成画面,可交给 GPU)。
本节最该记住的区别 · 重排与重绘:重排是重新算位置(走布局→绘制→合成,可能牵连一大片),重绘只是换颜色(跳过布局)。重排必然引起重绘,重绘不一定引起重排。三档代价从贵到便宜:改宽高/增删元素(全走一遍)→ 改颜色(跳过布局)→ 改 transform/opacity(只合成,最便宜)。所以做动画一律优先用 transform
两个隐蔽的陷阱:强制同步布局——改完立刻读尺寸,会强迫浏览器马上重算一遍,正确做法是读写分离(先全部读、再全部改);② 图片不写尺寸导致排版跳动——浏览器不知道图多大只能先不留位,图到了再把下面内容全推走,修法就是给图写明宽高。
"看得见"不等于"能操作":主线程同时负责画页面和跑脚本,脚本太重时页面已经显示但点击一直在排队——相当于一个店员既要摆货又要收银。
本节的边界:这里讲的是"怎么组织内容并算出位置与颜色"。更深入的渲染管线(颜色值怎么变成屏幕上发光的像素、位图与矢量、分辨率与像素密度)见界面篇 § 2.2。
全章收束:七个阶段严格串行——本机准备 → 查号(DNS)→ 接线(TCP)→ 验身份上锁(TLS)→ 说正事(HTTP)→ 画出来(渲染)→ 你看见了前面几步的时间花在"等来回"(由距离决定),最后一步花在"算"(由设备决定)——所以优化只有两条路:少等几个来回,或者少算一些活。而"点一下就开了"这句话被省略掉的,正是这七个阶段、四层协议、至少三个来回、一次证书链验证、几十个并行请求,和一条从字符到像素的完整流水线。
下一步:这一章走的是"数据在两端之间怎么流"。而路上那些替它转发、分流、挡住坏人的设备——路由器、交换机、网关、防火墙、负载均衡——是第 5 章的内容。

Xue Hai Wu Ya · Network · § 4.6 · 浏览器渲染