浏览器的内部结构
如下图所示,8个子系统组合构成了浏览器。
- 用户界面:包括地址栏、后退/前进按钮、书签目录等
- 浏览器引擎:查询及操作渲染引擎的接口
- 渲染引擎:显示请求的内容,比如请求内容为 HTML,则负责解析 HTML 、CSS,并将解析后的结果显示出来
- 网络子系统:完成网络调用,比如 HTTP 请求,它具有平台无关的接口,可以在不同平台工作
- Javascript 解释器:解释执行 Javascript 代码
- XML 解析器:解析 XML
- 显示后端:绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口
- 数据持久性子系统:属于持久层,浏览器需要在硬盘中保存类似 Cookie 的各种数据
其中页面加载和渲染,离不开浏览器引擎、渲染引擎、网络子系统、JavaScript 解释器
进程与线程
- 进程是操作系统资源分配的基本单位,进程中包含线程
- 线程是由进程所管理的,为了提升浏览器的稳定性和安全性,浏览器采用了多进程模型
前端开发平常工作离不开 Chrome 浏览器,现以其为例介绍进程和线程
Chrome 多进程和多线程架构
Chrome 浏览器主要包括4个进程
-
浏览器进程:处理选项卡之外的内容,用于控制用户可见的 UI 部分(比如地址栏,书签,后退、前进按钮)和用户不可见的隐藏部分(比如网格请求和文件访问),支持多线程
- UI 线程:绘制浏览器的按钮和输入字段
- 网络线程:发送请求,接收数据
- 存储线程:控制对文件的访问
-
GPU 进程:处理图像,3d 绘制,提高性能
-
渲染进程:每个选项卡都有单独的渲染进程,核心用于渲染页面,支持多线程
- GUI 渲染线程:渲染浏览器界面
- JavaScript 引擎线程:解析执行 JavaScript;与 GUI 渲染线程互斥
- 浏览器定时触发线程:setTimeout、setInterval 相关的线程
- 浏览器事件触发线程:处理浏览器事件,事件触发后需执行的代码传递给 JavaScript 引擎线程执行
-
插件进程:管理 Chrome 中安装的插件
浏览器中页面渲染过程
页面导航过程
用户输入 URL,浏览器进程进行请求和准备处理,流程图(包含依赖渲染器进程的过程)如下:
页面渲染过程
获取到资源后,渲染器进程处理选项卡内容的渲染,渲染过程如下图:
渲染过程详解:
解析
- 在解析前,会执行预解析操作,会预先加载 CSS、JS 等文件
- HTML 解析器解析 HTML,生成 DOM 树
- CSS 解析器解析 CSS,产生 CSS 规则树
- 解析 JS 脚本,该过程中需要等待 JS 执行完成才继续解析 HTML
从 HTML 到 DOM
- 字节流解码
浏览器通过 HTTP 协议接收到的文档内容是字节数据,通过算法确定字符编码,根据字符编码将字节数据解码成字符数据(即开发编写的代码)
- 输入流预处理
将上一步得到的字符数据进行统一格式化,比如将换行符转换成统一格式
- 令牌化
将字符数据转化为令牌(Token),不同状态下接收同样的字符数据会产生不同的结果
- 构建 DOM 树
浏览器创建解析器的同时,会创建一个 Document 对象
在树构建阶段,Document 作为根节点,不断地被修改和扩充,树构建器接收到某个令牌后,创建该令牌对应的 DOM 元素,并将该元素插入到 DOM 树中
为纠正元素标签嵌套错位,以及处理未关闭的元素标签,树创建器创建的新 DOM 元素还会被插入到一个开放元素栈中
从 CSS 到 CSSOM
渲染引擎解析 CSS 过程与解析 HTML 步骤一致,都会生成树状结构
不同点在于,CSS 在被转换成浏览器能识别的 document.styleSheets 后,还需要操作:
- 转换样式表中的属性值,使其标准化:比如颜色值都转换为 rgb 格式,em、rem 转换成 px
- 先继承父节点样式,然后进行补充和覆盖
解析 JS
浏览器解析 HTML,当遇到 script 标签时,会立即执行 JS 脚本,停止解析文档,因为 JS 可能改动 DOM 和 CSS
-
如果遇到的是 script 标签内联代码,解析过程暂停,执行权限给到 Javascript 引擎,执行完再给渲染引擎继续解析
-
如果遇到的是外链脚本,会等待脚本下载完毕,再继续解析文档,为了减少时间损耗,可以借助 script 标签的2个属性
- async 属性:立即请求文件,不阻塞渲染引擎,而是文件加载完后阻塞渲染引擎并立即执行文件内容
- defer 属性:立即请求文件,不阻塞渲染引擎,等解析完 HTML 后再执行文件内容
布局
通过解析之后,浏览器需要进一步渲染页面,就需要进行布局
构建渲染树
DOM 树和 CSSOM 树合并成一棵渲染树
- 遍历 DOM 树的根节点,在 CSSOM 树上找到每个节点对应的样式
- 忽略不需要渲染(比如脚本标记、元标记)和不可见的节点(比如设置了 display:none )
- 添加需要显示的伪类元素到渲染树
计算元素布局
生成渲染树后,需要计算元素的大小及位置,包括字体大小、换行位置等,方便后续绘制过程,获取到每个元素的确切位置和大小
绘制
渲染器线程遍历渲染树,判断元素渲染层级顺序,创建绘制记录
若渲染树发生变化,浏览器触发:
-
重绘:重画一部分屏幕,元素几何尺寸不变,下面这些操作会导致重绘:
- 改变 color、background 相关属性
- 改变 outline 相关属性
- 改变 border-radius、visibility、box-shadow 等属性
-
重排:元素几何尺寸改变,重新验证和计算渲染树,成本比重绘高,下面这些操作会导致重排:
- 浏览器窗口大小发生变化
- 元素内容、尺寸、位置、字体大小等发生变化
- 查询某些属性或者调用某些方法
- 添加或者删除可见的 DOM 元素
为避免降低性能,尽量减少重绘与重排,可通过如下措施
- 操作 DOM 时,尽量在低层级的 DOM 节点进行操作
- 不要使用 table 布局
- 不要频繁操作元素样式,对于静态页面,可以修改类名,而不是样式
- 避免频繁操作 DOM,可通过创建 documentFragment,在其应用 DOM 操作后,再添加到文档
- 将元素先设置为 display:none,操作结束后再将其显示
光栅化
将计算后的信息(比如文档结构、元素样式、绘制顺序)转换为屏幕上的像素
页面布局变更,触发重排、重绘,则重新进行光栅化,影响性能;为此,浏览器采用合成方法,页面拆分成若干层,分别进行栅格化,再通过合成器线程合成页面,其过程如下:
- 主线程创建合成层,确定绘制顺序,将信息传递给合成器线程
- 合成器线程栅格化每个图层,将每个图块传递给光栅线程
- 光栅线程栅格化每个瓦片,将其存储在 GPU 内存
- 合成器线程通过 IPC 提交给浏览器进程,合成器帧传递给 GPU 进程,处理并显示在屏幕上
总结
- 浏览器由8个子系统组成
- 本文以 Chrome 浏览器为例,介绍其多进程和多线程架构
- 掌握浏览器页面渲染过程,有助于前端开发优化页面性能