要提高网页的交互体验,也就是让用户在网页浏览时感觉不到卡顿,除了在网络上进行优化,也可以从浏览器渲染底层来分析为何复杂的网页会造成卡顿
文章导航
浏览器渲染的几个过程
大概经历这几个过程
- DNS解析
- TCP连接
- HTTP请求
- 解析资源渲染页面
- 断开连接
这次主要分析第四步,浏览器的渲染过程
浏览器的具体渲染流程
浏览器拿到资源后如何进行渲染
Blink(浏览器渲染主线程)
解析dom,生成dom树
由于浏览器无法直接理解HTML字符串,因此将这一系列的字节流转换为一种有意义并且方便操作的数据结构,这种数据结构就是DOM树。DOM树本质上是一个以document为根节点的多叉树
如何解析dom树
HTML 不能使用常见的自顶向下或自底向上方法来进行分析,因此浏览器创造了专门用于解析 HTML 的解析器。解析算法在 HTML5 标准规范中有详细介绍,算法主要包含了两个阶段:标记化(tokenization)和树的构建。
- 标记化
这个算法输入为HTML文本,输出为HTML标记,也成为标记生成器。其中运用有限自动状态机来完成。每一个状态接收来自输入信息流的一个或多个字符,并根据这些字符更新下一个状态。
<div>Hello world</div>
大概流程:遇到字符 < 时,状态更改为“标记打开状态”。接收一个a-z字符会创建“起始标记”,状态更改为“标记名称状态”。这个状态会一直保持到接收 > 字符。表示标记名称记录完成,这时候变为数据状态,接着接收 </div> 中的<,回到标记打开, 接收下一个/后,这时候会创建一个end tag的token。随后进入标记名称状态, 遇到>回到数据状态。
- 构建树
DOM 树是一个以document为根节点的多叉树。因此解析器首先会创建一个document对象。标记生成器会把每个标记的信息发送给建树器。建树器接收到相应的标记时,会创建对应的 DOM 对象。
<html>
<body>
Hello world
</body>
</html>
大概流程:状态为初始化状态。接收到标记生成器传来的html标签,这时候状态变为before html状态。同时创建一个HTMLHtmlElement的 DOM 元素, 将其加到document根对象上,并进行压栈操作。接着状态自动变为before head, 此时从标记生成器那边传来body,表示并没有head, 这时候建树器会自动创建一个HTMLHeadElement并将其加入到DOM树中。标记生成器最后传过来一个html的结束标记, 进入到after after body的状态,表示解析过程到此结束。
解析css,生成css树
- 格式化样式表
浏览器是无法直接识别 CSS 样式文本的,因此渲染引擎接收到 CSS 文本之后第一件事情就是将其转化为一个结构化的对象,即styleSheets,在浏览器控制台能够通过
document.styleSheets来查看这个最终的结构
- 标准化样式属性
有一些 CSS 样式的数值并不容易被渲染引擎所理解,因此需要在计算样式之前将它们标准化,如
em->px,red->#ff0000
,bold->700等等。
- 计算每个节点的样式,两个规则: 继承和层叠。
每个子节点都会默认继承父节点的样式属性,如果父节点中没有找到,就会采用浏览器默认样式,也叫UserAgent样式,然后是层叠规则,CSS 最大的特点在于它的层叠性,也就是最终的样式取决于各个属性共同作用的效果,计算出每个DOM元素的每个样式属性的最终值。这些内容存储在一个名为ComputedStyle的对象中,在Chrome浏览器里的话,就是对应开发者工具的Computed样式属性这一栏。或者是通过getComputedStyle的JS API去获取
生成layout树
现在已经生成了DOM树和DOM样式,接下来要做的就是通过浏览器的布局系统确定元素的位置,也就是要生成一棵布局树(Layout Tree)。
布局树生成的大致工作如下:
- 遍历生成的 DOM 树节点,并把他们添加到布局树中。
- 计算布局树节点的坐标位置。
通过布局算法,将两者进行了结合,我们得到了含有样式信息,布局信息的layout tree。但是注意这个layout tree和DOM不是一一对应的
paint Layer Tree
PaintLayer 这棵树主要用来实现层叠上下文,以保证dom重叠时也能用正确的顺序合成页面,这样才能正确的展示元素的重叠以及半透明元素等等。
comp.assign
我们直接Paint代价是十分昂贵的,于是引入了一个图层合成加速的概念,图层合成加速是把整个页面按照一定规则划分成多个图层,在渲染的时候,只要操作必要的图层,其他的图层只要参与合成就好了,以这种方式提高了渲染的效率,完成这个工作的线程叫做:Compositor Thread(合成器线程),合成器线程还具备处理事件输入的能力,比如滚动事件,但是如果在js中进行了事件的注册和监听,它会把输入事件转发给主线程。
什么是图层合成加速?
- 图层合成加速是把整个页面按照一定规则划分成多个图层,在渲染的时候,只要操作必要的图层,其他的图层只要参与合成就好了,以这种方式提高了渲染的效率
合成层作用:
- 合成层的绘制,渲染会交给GPU处理,比CPU更快
- repaint时,只用repaint自身即可
主线程把页面拆分成多个可以独立光栅化的层,并在另一个线程(合成器线程)中将这些层合并。
这使得某些RenderLayer拥有自己独立的缓存,它们被称作合成图层(Compositing Layer),内核会为这些RenderLayer创建对应的GraphicsLayer。
合成任务包含构建层树的过程。在布局layout之后,绘制paint任务之前,这个过程也可以称为「分层和合成任务」,每一层layer都是独立绘制的,一些属性节点单独为层,比如will-change,3D属性transform之类
prepaint构建属性树
在描述属性的层次结构这一块,之前的方式是使用图层树(GraphicsLayer Tree)的方式,如果父图层具有矩阵变换(平移、缩放或者透视)、裁剪或者特效(滤镜等),需要递归的应用到子节点,这在极端情况下会有性能问题。 于是,为了提高性能,引入了属性树的概念,合成器提供了变换树,裁剪树,特效树等。每个图层都有若干节点id,分别对应不同属性树的矩阵变换节点、裁剪节点和特效节点。这样的时间复杂度就是O
渲染进程合成线程绘制的时候,合成线程里的合成器可以将各种属性应用于其绘制的图层,如变换矩阵,裁剪,滚动偏移,透明度。这些数据储存在属性树里,最后生成property trees
Paint绘制
Paint这个流程会将绘制的操作记录在显示项(display items)列表中。绘制操作可能类似于“在这些坐标处以这种颜色绘制矩形”。每个布局对象可能有多个显示项(display items),对应其视觉外观的不同部分,如背景、前景、轮廓等。
在这个阶段处理GraphicsLayers,把绘制操作放在DisplayItem中。因为Paint阶段也是主线程最后一个步骤,也要与合成器线程对接,所以会把数据再处理为 cc:layer list并和prepaint阶段产生的Property trees(也会处理为cc property trees)提交给 合成器线程
CC(浏览器渲染合成器线程)
commit复制层数据到合成线程
在绘制paint阶段完成后,即绘制指令准备完成后,会进入渲染进程合成线程commit阶段。
commit阶段的核心作用就是更新Layer list(其实一般都叫它layer tree,但是其实他是list)和property trees的副本到合成器线程(compositor线程,也被称为Impl1线程)。简单来说就是commit会拷贝层和属性树生成副本到合成器线程,这里合成线程的commit会阻塞主线程直到commit完成
tiling分块平铺
合成器线程接收到数据之后,不会立即开始合成,而是把图层进行分块,这里涉及到了一个叫做“分块渲染”的技术,分块渲染会将网页的缓存分成一块一块的,比如256*256的块,之后进行分块渲染。
为什么要分块渲染?
- GPU合成通常是使用OpenGL ES贴图实现的,这时候的缓存实际就是纹理(GL Texture),很多GPU对纹理的大小是有限制的,比如长宽必须是2的幂次方,最大不能超过2048或者4096等。无法支持任意大小的缓存。
- 分块缓存,方便浏览器使用统一的缓冲池来管理缓存。缓冲池的小块缓存由所有WebView共用,打开网页的时候向缓冲池申请小块缓存,关闭网页是这些缓存被回收。
tiling图块也是栅格化的基本单位,栅格化会根据图块与可见视口的距离安排优先顺序进行栅格化。离得近的会被优先栅格化,离得远的会降级栅格化的优先级。这些图块拼接在一起,就形成了一个图层。
activation
在准备图块tiles进行栅格化和draw两个阶段渲染进程的合成线程都会参与,但是渲染进程主线程里的layer数据还在不断commit过来。实际上合成线程具有两个树的拷贝副本
- pending tree: 负责接收新的commit并转给栅格化线程池里的栅格化线程执行,完成后进入激活activation阶段,同步复制处理好后的layer副本到active tree里
- active tree: 绘制上一次activation同步复制的layer副本(来自上一个commit)
这里pending tree 和 active tree都是层列表和属性树的结合,不是真的树结构
Draw
现在到了Draw这个步骤,当每个图块都被光栅化之后,合成器线程会为每个图块生成draw quads(在屏幕的指定位置绘制图块的指令,也包含了属性树里面的变换,特效等操作),这些draw quads会被封装在CompositorFrame对象里面,CompositorFrame对象也是Render Process的产物,它会被提交到Gpu Process中,我们平时提到的60fps输出帧率指的帧其实就是CompositorFrame。
Draw指的就是 把光栅化的图块,转换成draw quads的过程。
整体流程
在熟悉了浏览器渲染流程后就知道我们该从哪些方面入手进行相应的一些优化。
will-change:transform为什么能提高性能
CSS 属性will-change 为web开发者提供了一种告知浏览器该元素会有哪些变化的方法,这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作。 这种优化可以将一部分复杂的计算工作提前准备好,使页面的反应更为快速灵敏
在代码层面优秀的css几种写法
- 避免使用table布局。
- 尽可能在DOM树的最末端改变class。
- 将动画效果应用到position属性为absolute或fixed的元素上,能够使用transform满足要求的就别使用position/width/height做动画
- 避免频繁使用 style,而是采用修改class的方式。
- 添加 will-change: tranform ,让渲染引擎为其单独实现一个图层,当这些变换发生时,仅仅只是利用合成线程去处理这些变换,而不牵扯到主线程,大大提高渲染效率。
延展阅读:
如何在Web开发中安全有效地解决电商网站的跨域资源共享(CORS)问题?
咨询方案 获取更多方案详情