整体流程大概可描述为:
- 用户从浏览器里输入请求信息
- 网络进程发起 URL 请求
- 服务器响应 URL 请求之后,浏览器进程开始准备渲染进程
- 渲染进程准备完毕后,向渲染进程提交页面数据(提交文档阶段)
- 渲染进程接受完文档信息后,开始解析页面和加载子资源,完成页面渲染
从输入 URL 到页面展示详细过程
用户输入
用户在地址栏输入关键字后,地址栏会根据关键字来判断是搜索内容
还是请求 URL
。
- 若判断为搜索内容,地址栏使用默认搜索引擎,合成新的带搜索关键字的 URL(q 后为搜索关键字)
https://www.google.com/search?q=google&xxxxxx
- 若判断内容符合 URL 规则,则地址栏会根据输入内容,合称为完整的 URL
www.baidu.com => https://www.baidu.com/
输入关键字后回车,标签上的图标变成 loading 状态,此时页面没有立即替换为目标页面;需要等待提交文档阶段结束,页面内容才会被替换。
URL 请求过程
页面请求资源过程,浏览器进程通过 IPC(进程间通信) 把 URL 请求发送至网络进程,网络进程收到后发起真正的 URL 请求。具体过程如下:
- 查找本地有无缓存资源。有,返回给浏览器进程。
- 没有,直接发起网络请求。
- DNS 解析来获取请求域名的服务器 IP 地址。
- 若请求协议是 HTTPS,还需要建立 TLS(安全传输层协议) 连接。
- 用 IP 地址和服务器建立 TCP 连接。(同一域名同时最多只能建立 6 个连接,超过的排队等待)(通过传输层、网络层等加上相应的头)
- 建立连接后,构建请求头、行等信息,将相关 cookie 等数据附加到请求头中,然后发送给服务器
- 服务器接受到请求信息后,根据请求信息生成响应数据,发给网络进程。(响应数据又顺着应用层——传输层——网络层——网络层——传输层——应用层的顺序返回到网络进程)
- 网络进程接收了响应行、头后便开始解析响应内容。
- 若返回的状态时是 301 或 302,则浏览器需要重定向到其他 URL,一切从头开始。
- 浏览器根据响应头中的
Content-Type
字段判断响应体的数据类型;若是application/octet-stream
字节流类型,浏览器会按下载类型来处理,该请求会被提交给浏览器的下载管理器,流程结束。若是 HTML,则进入下个流程:准备渲染进程
准备渲染进程 & 提交文档
不同主域名的页面使用不同的渲染进程,主域名相同的使用同一个渲染进程;
- 渲染进程准备完毕后,
浏览器进程
发出提交文档
的消息。 - 渲染进程收到后,会和网络进程建立传输数据的管道。
- 传输完毕后,渲染进程会返回“确认提交”的消息给浏览器进程。
- 浏览器进程收到确认的消息后,会更新浏览器界面状态(安全状态、URL、前进后退的历史状态)并更新 web 页面。(此时 web 页面是空白页面)
- 导航流程结束,进入渲染阶段。
渲染阶段
按照渲染的时间顺序,流水线可分为:
构建 DOM 树 -> 样式计算 -> 布局 -> 分层 -> 绘制 -> 分块 -> 光栅化 -> 合成等阶段。
每个阶段的流程如下:接收输入的内容 -> 处理 -> 输出内容
构建 DOM 树
接收 html 文件 -> html 解析器解析 -> 输出树状解构的 DOM
样式计算
大体流程:接受 css 文本 -> 计算出 dom 节点的具体样式 -> 输出计算完成后的 DOM 样式
具体步骤如下:
- 接收 css 文本,将其转为浏览器可理解的结构:stylesheets
css 三种来源:内联、外部引入、style - 转换 stylesheets 中属性值,使其标准化
body { font-size: 2em } p {color:blue;} div { font-weight: bold} // 标准化后 body { font-size: 32px } p {color: rgb(0,0,255);} div { font-weight: 700}
- 计算出 DOM 树中每个节点的具体样式
计算遵守 css 继承、层叠两个规则。所有子节点都继承父节点的样式。计算完后输出每个 DOM 节点的样式。
布局
经过上面过程,有了 DOM 树和树中元素的样式,但每个元素的位置还不确定,不足以显示页面。
Chrome 在布局阶段的任务:创建布局树与布局计算。
- 创建布局树
遍历 DOM 中的可见节点,将其添加到布局树中。不可见的节点会被忽略,比如:head 标签下的内容、display 为 none 的节点。 - 布局计算
计算布局树节点的坐标位置,计算完执行布局时会把运算的结果会重新写入布局树中,所以布局树既是输入内容也是输出内容;
分层
布局完之后不会立即绘制,渲染引擎会为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)
。在开发者工具下的 Layers 标签可以看见页面的分层情况,每个节点都有图层,若没有,就属于它父节点的图层。
- 什么情况下,渲染引擎会为特定节点创建新图层?
- 拥有层叠上下文的属性会被提升为单独的一层。
层叠上下文让 HTML 具有三维概念,按照自身属性优先级垂直分布在 z 轴上。
2. 需要剪裁(clip)的地方也会被创建为图层。
文字内容比较多,超出显示区域,这时就产生裁剪。渲染引擎会为文字部分单独创建一个层,若出现滚动条,滚动条也会被提升为单独的层。
图层绘制
画图例子:
给你一张纸,画一个背景蓝色,中间为红色的圆,再在圆上画一个绿色三角形,如何画?
- 绘制蓝色背景
- 在中间绘制红色圆
- 在圆上绘制绿色三角形
图层绘制与之类似,将一个图层拆分成很多小的绘图指令
,把指令按顺序组成列表去绘制。(每个元素背景、前景、边框都需要单独的绘图指令)
开发者工具 Layers 标签,选中 document 层,可重现绘制过程.
栅格化(raster)操作
绘制列表只用来记录绘制顺序与指令,真正的绘制操作由渲染引擎中的合成线程来完成。如上图,绘制列表准备好后,主线程会把它提交给合成线程。
有的图层很大,页面要使用滚动条滚好久才能到底部;通过视口,用户只能看到一小部分,这种情况下如果绘制所有图层内容,会产生很大开销且没必要。
合成线程如何处理?
合成线程会将图层划分成图块(tile),大小通常为(256x 256, 512 x 512)。
合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作由栅格化(将图块转为位图)
执行。图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有图块栅格化都在线程池内执行。
栅格化过程会使用 GPU 来加速生成,生成的位图被保留在 GPU 内存中。(GPU 操作在 GPU 进程中,栅格化在渲染进程中,这个过程涉及跨进程操作。)
合成与显示
当所有图块都被栅格化,合成进程会生成一个绘制图块的命令DrawQuad
,然后将该命令提交给浏览器进程。
浏览器进程里有个 viz 组件,接收到 DrawQuad 命令后,根据其内容,将页面绘制到内存,最后再将内存显示到屏幕上。
完整渲染流程总结
- 渲染进程将 HTML 内容转换为能够读懂的DOM 树结构。
- 渲染引擎将 CSS 样式表转化为浏览器可以理解的styleSheets,计算出 DOM 节点的样式。
- 创建布局树,并计算元素的布局信息。
- 对布局树进行分层,并生成分层树。
- 为每个图层生成绘制列表,提交给合成线程。
- 合成线程将图层分成图块,并在栅格化线程池中将图块转化为位图。
- 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
- 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。
重排、重绘、合成
- 重排
更新元素几何属性,开销最大
- 重绘
更新元素颜色
- 合成
直接合成,避开重排重绘。
减少重排、重绘的方法?
- 使用 class 操作样式,而不是频繁操作 style
- 避免使用 table 布局
- 批量dom 操作,例如 createDocumentFragment,或者使用框架
- Debounce window resize 事件
- 对 dom 属性的读写要分离
- will-change: transform 做优化
参考资料
浏览器工作原理与实践