背景
多进程架构
目前 chrome 浏览器是一个多进程的架构。
图:浏览器多进程架构
可以打开浏览器的任务管理看到
浏览器进程、网络进程、GPU 进程、渲染进程 (控制在 tab 里面展示的所有内容)
如果打开的页面有运行插件的话,还需要插件进程。
图:每个 Tab 对应一个渲染进程
Chrome 的默认策略是,每个标签对应一个渲染进程。
但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点(类似同源)的话,那么新页面会复用父页面的渲染进程。 (可以打开两个网页验证,暂停 JS 执行,另一个网页也会卡住)
渲染
输入URL,浏览器经过一系列导航处理(直接看示意图)。然后请求渲染, 进入渲染阶段。
渲染进程
图: 渲染进程以及内部线程
渲染进程内部包含:
主线程 main thread
合成线程 compositor thread
Worker 线程 worker threads (由主线程创建)
栅格化线程 raster threads (由合成线程创建)
主线程基本上做完大部分的事情,合成线程处理剩下的
渲染流水线
核心:一开始按顺序执行所有步骤,不可跳过。后续可能从某一步开始执行, 并可以跳过某些中间步骤。
图:渲染流水线
大体上有 8 个阶段。
每个子阶段都有其输入的内容,
每个子阶段有其处理过程,
每个子阶段会生成输出内容。
复杂的基本在主线程上,合成线程相对简单。
一:解析 (Parsing) (主线程)
输入是从网络进程传输的 HTML 文档,输出是 DOM 树。
图:html 生成 DOM 树
网络进程接收到响应头之后如果 content-type 的值是“text/html”,就让浏览器进程准备渲染进程。
网络进程与渲染进程建立管道传输数据。
网络进程加载了多少数据,渲染进程中的 HTML 解析器便解析多少数据。(TCP 保证顺序)
同时会有预解析线程,如果 HTML 文档中有 <img>
或 <link>
之类的内容,请求网络进程预下载。(并行解析CSS,生成CSSOM)
图:HTML 解释模型
有什么收获?
- JS 阻塞 DOM 树构建:
// 改变了 DOM 树内容
<html>
<body>
<div>1</div>
<script>
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang'
</script>
<div>test</div>
</body>
</html>
// 这里还需要算上网络因素
//foo.js
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang'
<html>
<body>
<div>1</div>
<script type="text/javascript" src='foo.js'></script>
<div>test</div>
</body>
</html>
- CSS 会阻塞 JS 执行,间接阻塞 dom 树生成
<html>
<head>
<style src='theme.css'></style>
</head>
<body>
<div>1</div>
<script>
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang' //需要 DOM
div1.style.color = 'red' //需要 CSSOM
</script>
<div>test</div>
</body>
</html>
优化:
<script>
标签添加 async
或 defer
属性,浏览器会异步加载运行 JavaScript 代码,不阻塞 DOM 树生成
async 标志的脚本文件一旦加载完成,会立即执行
defer 标记的脚本文件在 DOMContentLoaded 事件之前执行
总结:
CSS 放在头部,并行解析生成 CSSOM
JS 依赖 DOM、CSSOM 放在尾部, 不阻塞页面渲染
二:样式计算 (Style calculation) (主线程)
解析 CSS 以添加 computed style (可以打开 devtool->element->cmputed 面板查看)
- 把 CSS 转换为浏览器能够理解的结构 (可以打印查看 document.styleSheets ,属性还没标准化)
- 转换样式表中的属性值,使其标准化 (如 2em、blue、bold)
- 计算出 DOM 树中每个节点的具体样式 (涉及CSS 的继承规则和层叠规则。可以打开 devtool->element -> Styles 面板查看。)
(Performance main 中的 Parse Stylesheet,Recaculate Style 阶段)
图:生成 computed style
待确认:
第一次加载,这里应该是有另一个线程去做步骤1,2。因为 CSS 是设计为并行加载的。
步骤3才需要用到 DOM 树(主线程)
有什么收获?
-
把 CSS 放在最前面,和 HTML 并行加载解析
三:布局 (Layout) (主线程)
输入是 DOM 树(DOM tree) 和 计算样式(computed styles)。
输出是包含计算布局信息的布局树(layout tree)。
布局阶段需要完成两个任务
1.创建布局树
隐藏不可以见元素、添加元素(伪元素)
2.布局计算
计算出节点的几何属性 (X、Y 坐标,以及盒子大小)
(布局计算很复杂,没找到简单的文章介绍。有专门的引擎 LayoutNG 做这个操作 – BlinkOn 8: Block Layout Deep Dive)
图:生成布局树
这一步元素位置,大小都得到了
有什么收获?
-
修改元素的几何属性会重新触发 Layout 。也就是常说的重排(Reflow)
四:分层 (Layer) (主线程)
输入布局树(layout tree),输出图层树(layer tree)。
核心:为绘制顺序服务,特定的节点生成专用的图层,并非所有元素都有自己的图层。(参考 PhotoShop 图层)
元素 A 和元素 B,
A -> z-index:9,
B 没有-> z-index,
A 将显示在 B 的上面。
图:生成图层树
满足这些条件的元素都有自己的图层:
第一点,拥有层叠上下文属性的元素会被提升为单独的一层。(层叠上下文,z-index 相关知识炳俊之前分享过)
第二点,需要剪裁(clip)的地方也会被创建为图层。(文本换行)
滚动条
五:绘制 (Paint) (主线程)
输入布局树(layout tree)和图层树(layer tree),输出绘制记录(paint records)。
位置、大小,先后 => 绘制记录
图: 生成绘制记录
绘制记录长这样,先画 XXX ,再画 XXX
图:绘制记录
图:layers 面板图层的绘制记录
把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。
在此阶段,输入是布局树和图层树,输出是绘制顺序记录。
(可以打开“开发者工具”的“Layers”标签,选择“document”层,来实际体验下绘制列表)
六:生成图块(Tilling) (合成线程)
输入是绘制记录(paint records)和图层(layers),输出是图块 (tiles) 。
3g模式看看地图渲染,直观感受下图块。(地图图块习惯翻译成瓦片,英文一样都是tile)。openlayers.org/en/latest/e…
图:生成图块
通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。
在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要渲染出所有图层内容的话,就会产生太大的开销,而且也没有必要。
基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是 256×256 或者 512×512 。
图:图层、图块、视口示意图
如上图 ,图块 1、2、3、4 是可视的,优先渲染。
七:栅格化(Raster) (合成线程)
输入是图块(tiles),输出是位图(bitmaps)。(合成线程)
有了图块(tiles)后,合成器线程将创建一个栅格线程池。多个栅格线程同时进行栅格切片(并行)。
栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
当位图准备就绪时,GPU 进程会将它们传送回渲染进程中的合成器线程,以便进行下一步。
八:合成和显示 (合成线程)
输入是位图(bitmaps),输出是合成器帧(compositor frame)。 (合成线程+浏览器进程)
图:生成合成器帧
暂时无法在{app_display_name}文档外展示此内容
图:合成原理
Draw quads: 包含诸如图块在内存中的位置,以及合成时绘制图块在页面中的位置等信息。
Compositor frame: 一个 Draw quads 的集合,代表页面的一帧。
一旦所有图块都被光栅化,合成线程就会调用一个绘制图块的命令——“DrawQuad”,生成合成器帧(Compositor frame)。
接着,合成器帧(Compositor frame)提交给浏览器进程。
此时,可以从 浏览器进程中的 UI 线程或其他插件的渲染进程添加另一个合成器帧(Compositor frame)。这些合成器帧被发送到 GPU 然后在屏幕上显示。如果接收到滚动事件,渲染进程中的合成线程会创建另一个合成器帧(Compositor frame)发送到 GPU。
合成线程创建合成帧,将其发送到浏览器进程
合成线程创建合成帧,将其发送到浏览器进程,再接着发送到 GPU
在现代浏览器中,这 8 个阶段都在 1/60 秒内完成。也就是 16.67 毫秒。(超过这个时间就卡)
总结
了解渲染流水线的相关概念,可以更好的理解常说的重排、重绘、合成是什么。
图:重排(Reflow)
图:重绘(Repaint)
图:合成(Compositing)
最后看一下, Devtools 性能面板(可以用 edge 打开,chrome 去掉了 Raster 光栅指标)
Main 主线程,流水线 1-5步
Compositor 合成线程的任务执行记录 第6、7、8步
Raster 可以看到维护了多少个光栅线程 流水线 第7步
最新 (2021 年 7 月 )
2021 年 7 月 发布新渲染引擎 “RenderingNG“
参考资料
主要参考谷歌官方文档
Inside look at modern web browser (part 1)
Inside look at modern web browser (part 2)
Inside look at modern web browser (part 3)
Inside look at modern web browser (part 4)
How does browser work step by step [latest] — rendering phase (part 3)
2021 年之后