浏览器是怎么把代码变成页面的?

7 阅读9分钟

你在地址栏输入一个URL,敲下回车,页面就出现了。但浏览器内部到底经历了什么?HTML、CSS、JS是如何变成你看到的页面的?

今天用**"装修房子"**的故事,聊聊浏览器的渲染原理。


原文地址

墨渊书肆/浏览器是怎么把代码变成页面的?


从URL到页面:渲染总览

当你在浏览器输入URL并回车,浏览器内部经历了:

浏览器地址栏
├── URL输入
├── DNS解析
   └── 域名  IP地址
├── TCP连接
   └── 三次握手
├── HTTP响应
   └── 服务器返回HTML/CSS/JS
└── 渲染进程处理
    ├── 构建阶段:HTML解析 + CSS解析
    └── 绘制阶段:布局  分层  绘制  合成

渲染流水线可以分为构建阶段绘制阶段

构建阶段(并行):
┌─────────────┐     ┌─────────────┐
  HTML解析          CSS解析    
   生成DOM           生成CSSOM  
└──────┬──────┘     └──────┬──────┘
                           
       └────────┬───────────┘
                
          渲染树构建
                
绘制阶段:
```yaml
绘制阶段
├── 布局计算
   └── 计算每个元素的位置、大小、边距
├── 分层
   └── 哪些元素需要独立图层(fixed/动画/视频)
├── 绘制
   └── 生成绘制指令(矩形、文字、线条)
└── 合成输出
    └── GPU合并图层  显示到屏幕

解读

  • 构建阶段:HTML和CSS解析同时进行(并行),完成后合并成渲染树
  • 绘制阶段:按顺序执行布局、分层、绘制、合成,最终输出画面
阶段输入输出
HTML解析HTML字符串DOM树
CSS解析CSS字符串CSSOM树
渲染树构建DOM + CSSOM渲染树
布局渲染树盒模型信息
分层布局信息图层树
绘制图层绘制指令
合成图层+指令画面

第一步:HTML解析 → DOM树

浏览器收到HTML响应后,首先要解析HTML,构建DOM树

DOM是什么?

DOM(Document Object Model,文档对象模型)是HTML/XML文档的编程接口。浏览器把HTML文档解析成一棵树状结构,每个HTML标签都是树上的一个节点

<html>
  <head>
    <title>标题</title>
  </head>
  <body>
    <h1>欢迎</h1>
    <p>这是段落</p>
  </body>
</html>

DOM树结构:

html
├── head
   └── title  "标题"
└── body
    ├── h1  "欢迎"
    └── p  "这是段落"

HTML解析过程

解析器从上到下读取HTML,遇到<head>标签创建head节点,遇到<body>标签创建body节点,遇到嵌套标签创建子节点...

HTML解析器:逐行读取  创建节点  构建DOM树
<html>  html节点
<head>  head节点  title节点  文本节点  关闭title  关闭head
<body>  body节点  h1节点  文本节点  关闭h1  p节点  文本节点  关闭p  关闭body  关闭html
 DOM树构建完成

遇到JS会怎样?

HTML解析器遇到<script>标签时会暂停解析,先执行JS:

解析HTML  构建DOM  完成
    
遇到<script>:暂停  执行JS  继续

因为JS可能document.write()修改DOM,所以HTML解析器必须等JS执行完成才能继续。

这就是为什么把JS放在body底部可以加快首屏渲染——让HTML先解析完,显示内容,JS最后再执行。


第二步:CSS解析 → CSSOM树

HTML解析的同时,浏览器也在解析CSS,构建CSSOM树(CSS Object Model)。

CSSOM是什么?

CSSOM是CSS样式表的树状结构,描述了每个元素的样式信息。

body { font-size: 16px; }
h1 { color: red; font-size: 24px; }
p { color: blue; }

CSSOM树结构:

body
├── font-size: 16px
├── color: (inherited)
└── children
    ├── h1
       ├── color: red
       └── font-size: 24px
    └── p
        └── color: blue

CSS解析特性

与HTML不同,CSS解析是上下文相关的

标签选择器:p { color: blue; }      所有<p>生效
类选择器:.title { ... }          class="title"生效
ID选择器:#header { ... }        id="header"生效

CSS解析器需要考虑选择器优先级(ID > 类 > 标签)、层叠规则、继承规则等。


第三步:渲染树(Render Tree)

DOM树 + CSSOM = 渲染树(Render Tree)

渲染树只包含可见节点——display: none的元素不会出现在渲染树中。

DOM + CSSOM = 渲染树

DOM节点CSSOM样式渲染树
display:none✗ 不显示
容器样式body
├─h1color:redh1(red)
├─pdisplay:none✗ 不显示
└─spancolor:greenspan(green)

注意<p style="display: none">不会生成渲染树节点,但<p style="visibility: hidden">会生成(只是不可见)。


第四步:布局(Layout)

渲染树构建完成后,浏览器计算每个元素的几何信息:位置、大小、边距、边框等。

布局计算

渲染树  布局计算  盒模型信息
元素1:x=0, y=0, width=200, height=50
元素2:x=0, y=50, width=200, height=30
元素3:x=0, y=80, width=100, height=80
 每个元素都有精确的位置和大小

盒模型(Box Model)

CSS中的盒模型定义了元素的空间占用:

┌─margin─────────────────────────────┐
  ┌─border───────────────────────┐  
    ┌─padding──────────────────┐   
      ┌─content─────────────┐    
         width × height       
      └─────────────────────┘    
    └──────────────────────────┘   
  └───────────────────────────────┘  
└─────────────────────────────────────┘
属性说明
content内容区域(width × height)
padding内边距,内容与边框之间的空间
border边框,围绕内边距的线条
margin外边距,边框与其他元素之间的空间

回流(Reflow)

当元素的几何信息发生变化时,浏览器需要重新计算布局,这称为回流(Reflow)

触发回流的操作:

  • 添加/删除可见DOM元素
  • 元素位置/尺寸变化
  • 浏览器窗口大小变化
  • 获取元素的offsetWidth/Height(强制触发计算)
回流过程:
修改DOM  重新计算布局  重绘(耗时操作)

回流比重绘更昂贵,因为它需要重新计算整棵布局树。


第五步:分层(Layer)

布局完成后,浏览器根据一定规则把页面分成多个图层(Layer)

为什么要分层?

分层可以让页面的不同部分独立绘制和合成,避免互相影响。

分层示意:
Layer 3: 固定定位的导航栏(最顶层)
Layer 2: 主体内容
Layer 1: 背景图片
Layer 0: 页面根元素(最底层)

哪些元素会生成独立图层?

生成独立图层的触发条件:

  • position: fixed(固定定位)
  • will-change: transform(transform动画)
  • <video><canvas>元素
  • 3D变换:transform: translate3d()
  • CSS动画:@keyframes + transform
  • 加速属性:opacitytransform

浏览器会为这些元素创建独立的合成层(Compositing Layer),让它们的渲染不影响其他图层。

CSS Containment

contain属性可以告诉浏览器元素内容独立于页面其他部分,帮助浏览器优化:

.container {
  contain: content;  /* 布局、样式、绘制都独立 */
}

第六步:绘制(Paint)

分层后,每个图层内部需要绘制,生成绘制指令。

绘制顺序

浏览器按从后到前的顺序绘制各图层:

绘制顺序:
1. 背景色(最底层)
2. 背景图片
3. 边框
4. 内容(从左上到右下)
5. 伪元素
6. 轮廓(最顶层)

绘制指令

绘制不是直接画像素,而是生成绘制指令列表(Paint Records):

绘制指令示例:
1. drawRect(x=0, y=0, w=100, h=50)  矩形
2. drawText("Hello", x=10, y=30)   文字
3. drawRect(x=0, y=50, w=200, h=1)  分割线

这些指令会交给**光栅线程(Raster)**执行,将指令转换为实际像素。

重绘(Repaint)

当元素的外观改变但不影响布局时,触发重绘:

触发重绘(不改布局):改变颜色、改变可见性、改变边框样式
改变样式  重绘  完成(比回流快)

重绘比回流快,因为它不需要重新计算布局。


第七步:合成(Composite)

绘制完成后,所有图层提交给GPU,GPU将各图层合成成最终画面。

合成过程

Layer 0(背景层)
Layer 1(内容层)
Layer 2(浮动层)
    
GPU合成  输出到屏幕

为什么需要合成层?

  1. 滚动流畅:合成层有自己的GPU加速,滚动不经过主线程
  2. 动画流畅:transform/opacity动画在合成线程执行,不被JS阻塞
  3. 分离更新:只有一个图层内容变化,只需重绘该图层
传统渲染(无合成层)
└── JS修改  重排  重绘  合成  输出
    └── 主线程执行(可能被JS阻塞)

现代渲染(有合成层)
├── JS修改  重排  重绘  合成  输出
└── 合成线程独立执行(不受JS阻塞)

关键渲染路径(Critical Rendering Path)

关键渲染路径是浏览器从接收HTML到首次绘制页面的最短路径

优化关键渲染路径

想让页面更快显示?优化关键渲染路径:

优化目标说明
减少关键资源数量合并文件,减少请求
减少关键资源大小压缩文件,删除注释空格
缩短关键路径长度内联CSS、JS放底部、懒加载

回流与重绘:性能杀手

浏览器渲染过程中最怕什么?频繁的回流和重绘

强制回流/重绘

某些CSS属性和方法会强制触发回流或重绘:

// 读取以下属性会强制触发回流
element.offsetWidth;     // 布局信息
element.offsetHeight;
element.scrollTop;
element.clientWidth;
getComputedStyle(element).width;

// 修改DOM结构
element.appendChild(child);
element.removeChild(child);

批量读写原则

读写分离,避免交叉触发回流:

// 错误:每次读取触发一次回流
element.width = element.offsetWidth * 2;
element.height = element.offsetHeight * 2;
element.marginTop = element.offsetTop * 2;

// 正确:先读后写,写只触发一次回流
const width = element.offsetWidth;
const height = element.offsetHeight;
const marginTop = element.offsetTop;
element.style.width = width * 2;
element.style.height = height * 2;
element.style.marginTop = marginTop * 2;

requestAnimationFrame

对于需要连续动画的场景,使用requestAnimationFrame代替setTimeout/setInterval

// 不推荐:可能在帧之间执行
setTimeout(() => {
  element.style.transform = 'translateX(100px)';
}, 16);

// 推荐:在下一帧开始前执行
requestAnimationFrame(() => {
  element.style.transform = 'translateX(100px)';
});

总结:渲染流水线

阶段输入输出耗时
HTML解析HTML字符串DOM树
CSS解析CSS字符串CSSOM树
渲染树构建DOM + CSSOM渲染树
布局渲染树盒模型信息
分层布局信息图层树
绘制图层绘制指令
合成图层+指令画面

核心思想:浏览器渲染页面如同装修房子——先搭骨架(DOM),再刷墙(CSS),然后布局家具位置(Layout),最后上色绘制(Paint),不同房间(Layer)可以同时施工,最后统一验收(Composite)。

理解渲染原理,才能写出性能更好的页面。


扩展阅读

概念说明
虚拟DOMReact等框架用JS对象模拟DOM,减少真实DOM操作
增量更新只更新变化的部分,不全量重渲染
Content-visibilityCSS新属性,跳过屏幕外内容的渲染
渲染性能指标LCP(最大内容绘制)、CLS(布局偏移)、FID(首次输入延迟)