浏览器渲染原理:从 HTML 到像素的全链路拆解

浏览器渲染原理

什么是渲染?

渲染(render),简单来说就是浏览器将 HTML、CSS、JavaScript 等代码,转化为用户能直观看到的可视化网页界面的全过程

更细一点讲,就是根据 HTML、CSS、JavaScript 等代码,计算浏览器视口区域每个像素点的显示颜色(浏览器牛皮),光想想都知道这个过程时极其复杂的。

渲染的时机

在浏览器地址栏输入网页地址回车之后,浏览器做了些什么?

这是一个很常见的面试题。我们现在就通过这道面试来分析了解浏览器渲染页面的时机。

简单来说就是做了两件事, 拿 HTML (网络线程)和 渲染页面 (渲染主线程)

这篇文章主要是写渲染相关的,网络线程我就稍微简单的一笔带过了

网络线程:解析输入的地址(DNS 域名解析)、建立 TCP 连接、发送 HTTP 请求(请求成功就能拿到 HTML 了)

当网络线程收到 HTML 文档之后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列,在事件循环机制的作用下,渲染主线程会取出并执行渲染任务,开启渲染流程(我的另一篇文章细讲了事件循环相关的知识)。

渲染过程

整个页面渲染的流程分为多个阶段,分别是:

HTML 解析、样式计算、布局、分层、绘制、分块、光栅化、画

每一个阶段都有明确的输入和输出,上一个阶段的输出会成为下一个阶段的输入。

下面我们来逐一了解这些渲染的阶段。

解析 HTML

首先的第一个阶段就是解析HTML,生成 DOM 和 CSSOM,我们拿到的HTML文档其实就是一个字符串,为了后续的操作和计算需要通过 HTML 和 CSS 代码生成对象结构的树。

生成 DOM 树

这个阶段首先会根据 HTML 文档,生成 DOM (Document Object Model) 树,DOM 树文档对象模型(Document Object Model) 的树状表现形式,它是浏览器解析 HTML 文档后,生成的一套跨平台、语言无关的编程接口,本质是把 HTML 里的所有内容(元素、属性、文本等)映射成一个个节点,再按 HTML 的嵌套关系组织成树状的对象结构(对象结构相比字符串更便于操作)。

比如我们这里有一段 HTML 代码:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>DOM & CSSOM 示例</title>
    <style>
        /* 标签选择器 */
        body {
            background-color: #f5f5f5;
            font-size: 16px;
        }
        /* 类选择器 */
        .content-box {
            color: #333;
            padding: 20px;
        }
        /* ID 选择器 */
        #title {
            font-weight: bold;
            color: #0066cc;
        }
    </style>
</head>
<body>
    <div class="content-box">
        <h1 id="title">DOM & CSSOM 演示</h1>
        <p>这是一段测试文本</p>
    </div>
</body>
</html>

对应的 DOM 树结构就是下面这样的(懒得画图了,将就一下)

复制代码
html(根节点,属性:lang="zh-CN")
├── head(子节点)
│   ├── meta(子节点,属性:charset="UTF-8")
│   ├── title(子节点)
│   │   └── 文本节点:"DOM & CSSOM 示例"
│   └── style(子节点,内容为 CSS 代码,仅作为文本存储,不解析)
└── body(子节点)
    └── div(子节点,属性:class="content-box")
        ├── h1(子节点,属性:id="title")
        │   └── 文本节点:"DOM & CSSOM 演示"
        └── p(子节点)
            └── 文本节点:"这是一段测试文本"
遇到 CSS 代码

主线程当解析 HTML 的过程中解析到 <link rel="stylesheet"> (外部资源引用),会触发 CSS 解析器(预解析线程),去加载对应的 CSS 代码,然后交给渲染主线程生成 CSSOM(CSS Object Model)树。

渲染主线程解析 HTML 时,遇到 <style> 标签,会先把 <style> 标签本身解析成 DOM 节点;然后读取标签内的 CSS 文本并解析生成 CSSOM(CSS Object Model)树。

每个样式表都会生成对应的 CSSOM,并放在 StyleSheetList 对象下面。

上面 HTML 解析出来的 CSSOM 结构(一个样式表的)如下(依旧将就):

复制代码
CSSOM 根(隐含,包含所有样式规则)
├── 选择器:body
│   ├── background-color: #f5f5f5
│   └── font-size: 16px
├── 选择器:.content-box
│   ├── color: #333
│   └── padding: 20px
├── 选择器:#title
│   ├── font-weight: bold
│   └── color: #0066cc (优先级高于 .content-box 的 color,最终生效)
遇到 JS 代码

如果主线程解析到 script 标签,会停止解析HTML,转而等待 JS 文件下载完成,并将全局代码解析执行完成之后,才能继续解析 HTML。

HTML 的解析会生成 DOM 和 CSSOM,再根据它们去渲染页面,JS 代码执行则有可能会修改当前的 DOM 和 CSSOM,所以HTML的解析需要暂停,这就是 JS 阻塞 HTML 解析的根本原因。

样式计算

这一步会遍历 HTML 解析生成的 DOM 树的每一个节点进行样式计算,生成一个包含最终样式(Computed Style)的 DOM 树。

我们可以在开发者工具中的 Computed 里面查看选中元素的 Computed Style,里面包含了元素所有样式(不止是我们设置的)。

样式计算的过程也是很复杂的,后续看看再写一篇文章讲一下吧。

布局

这一步依次遍历包含最终样式(Computed Style)的 DOM 树,计算每一个节点元素的 尺寸位置(相对于包含块 ,生成一个 Layout 树(C++对象)

很多情况下DOM 树 和 Layout 树的节点不是一一对应的:

  • 一些隐藏的节点(display: none)没有几何信息(尺寸和位置),Layout 树就不会有这个节点。
  • 如果有伪元素, Layout 树则会有多的元素节点。
  • 由于有 内容必须在行盒中 的规则,Layout 树中块盒文本外会有一层匿名行盒。
  • ......

JS 可以通过布局属性拿到一些布局树的属性。

API / 属性 作用
element.getBoundingClientRect() 返回元素的边界矩形(left/top/right/bottom/width/height)
element.offsetWidth/offsetHeight 返回元素的布局宽度 / 高度
element.offsetLeft/offsetTop 返回元素相对于 offsetParent 的偏移量
element.clientWidth/element.clientHeight 返回元素的内容区宽度 / 高度

分层

这一步浏览器主线程会对布局树进行分层,如果某一个层改变后,仅会对该层进行后续处理,不影响其他层。

滚动条、堆叠上下文、transform、opacity 等样式都会影响分层结果,也是以使用 will-change属性更大程度的影响分层结果。

我们可以再开发者工具的 Layers 中查看页面的分层信息

绘制

这一步浏览器主线程会为每一个层生成绘制指令集,用于描述这一层的内容如何绘制。

主线程的渲染工作到这一步就结束了,后续的步骤会交给其他线程完成。

分块

完成绘制步骤后,主线程会将每一个图层的绘制信息提交给合成线程,剩余工作将交由合成线程完成。

合成线程首先会将每个图层分块,将其划分为更多的小区域。

合成线程会从线程池拿取多个线程来完成分块工作。

光栅化

分块完成之后就会进入光栅化阶段,合成线程会将分块信息交给 GPU 进程完成光栅化。

GPU 进程会开启多个线程来完成光栅化,这个阶段会优先处理靠近视口区域的分块。

光栅化的结果就是每个块的位图(像素点颜色信息)

这个阶段合成线程拿到每个层、每个块的位图后,生成一个个「指引(quad)」信息。

指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。因此, transform 变形的操作是合成线程处理,与渲染主线程无关,这就是 transform 效率高的本质原因。

合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像。

Reflow(回流 / 重排)

当我们修改 DOM 元素的几何属性(位置、尺寸等)时,我们实际修改的是 CSSOM ,如果 DOM 或者 CSSOM变化了,就会重新进行样式计算生成 Layout 布局树,再将元素绘制到页面上,这个过程就是 Reflow。

它是浏览器渲染流水线中开销较高的步骤,频繁触发会显著降低页面性能,甚至造成卡顿。

为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当 JS 代码全部完成后再进行统一计算。所以,改动属性造成的 reflow 是异步完成的。

也同样因为如此,当 JS 获取布局属性时,就可能造成无法获取到最新的布局信息。

所以我们获取几何信息属性时,浏览器会立即 Reflow。

Repaint(重绘)

Repaint(重绘) 是浏览器渲染流水线中紧随回流(Reflow)的核心步骤,指当 DOM 元素的视觉样式属性 发生变化(不影响几何位置和尺寸)时,浏览器仅重新为元素绘制像素内容(如颜色、背景、阴影),无需重新计算布局的过程。

Repaint的本质就是重新根据分层信息计算了绘制指令。

当改动了可见样式后,就需要重新计算,会引发 repaint。

由于元素的布局信息也属于可见样式,所以 Reflow 一定会引起 Repaint。

相关推荐
打小就很皮...2 小时前
React 合同审查组件:按合同原文定位
前端·react.js·markdown
EndingCoder2 小时前
设计模式在 TypeScript 中的实现
前端·typescript
夏天想2 小时前
服务端渲染 (SSR)、预渲染/静态站点生成(SSG)
前端
晚霞的不甘2 小时前
Flutter for OpenHarmony 引力弹球游戏开发全解析:从零构建一个交互式物理小游戏
前端·flutter·云原生·前端框架·游戏引擎·harmonyos·骨骼绑定
春日见2 小时前
Docker中如何删除镜像
运维·前端·人工智能·驱动开发·算法·docker·容器
码农六六2 小时前
前端知识点梳理,前端面试复习
前端
打小就很皮...2 小时前
React 合同审查组件:按合同标题定位
前端·react.js·markdown
CHU7290353 小时前
智慧陪伴新选择:陪诊陪护预约小程序的暖心功能解析
java·前端·小程序·php
奔跑的web.3 小时前
TypeScript namespace 详解:语法用法与使用建议
开发语言·前端·javascript·vue.js·typescript