01 浏览器渲染优化
优达学城浏览器渲染优化的学习笔记。
第一课 关键渲染路径
Web网页应该尽量避免不稳定性。
多数设备刷新屏幕的频率为每秒60帧,即60fps
渲染流程
- 生成DOM树,(Parse HTML)
- 生成CSS树
- 合并成为渲染树(Recalculate Styles),计算样式
- 计算布局,生成盒模型(Layour/Reflow)
- 将页面由矢量转为光栅(Paint)
- 处理复合层(compositive layer)
注意,复合层上方的元素也会变为单独的图层(隐式复合)
对于样式改变,导致的渲染流程:
对于JavaScript部分,指的是能够引起外观的变化,不仅可以通过JavaScript来改变页面外观,也可以使用CSS动画或者Web Animation API来实现。
改变外观后可能会需要重新计算Style属性(比如媒体查询等),有可能不需要。
改变的CSS属性不同,后续触发流程也不同。
- 改变宽度、高度、位置等几何结构,会触发后续的Layout、Paint、Composite步骤
- 改变背景图片、颜色、阴影等不涉及几何几何尺寸的属性,不会再触发Reflow,只会触发后续Paint、Composite步骤
- 改变
transform
、opacity
等会生成单独的图层的属性,不会再触发Reflow和Repaint,只需触发Composite步骤
CSS属性的改变应该尽可能触发少量的工作,避免Reflow和Repaint,提高性能。
练习
(1)第一题
渲染树中只包含了最终显示在屏幕上的元素内容,它与DOM树并不完全相同,比如它不会包含display:none
的元素,不会包含<head>
标签中的元素。
所以这道题目的答案是A,而其他的属性,虽然不会占据屏幕空间,但是还是属于页面的一部分,仍然会包含在渲染树中。
(2)第二题
无论是改变body
还是改变其中内部的div
的宽度,浏览器都会做出最坏的打算,就是需要对全部DOM元素的样式进行计算,对整个文档进行Reflow和Repaint。所以答案是C
(3)第三题
题目中是一个弹性部局的容器(flex),但改变容器的大小,内部元素的尺寸会随机发生变化。当新的页面渲染时,浏览器会经历那些步骤?
弹性容器的尺寸发生变化,内部元素的尺寸会发生变化,但是它们的Style并没有发生变化(因为宽度都是弹性确定的,而非固定值,并且不涉及媒体查询短点改变样式的条件),所以不会重新计算Style,但是由于宽度发生了变化,所以后续的Layout、Paint、Composite都会执行。
(4)第四题
会触发Layout+Paint+Composite的属性很多,比如margin
、width
等
触发Paint+Composite的属性有color
、background
等
触发Composite的属性变transform
、opacity
等
可以通过CSS Triggers网站来查找CSS属性分别会触发哪些过程。
第二课 App 生命周期
网络应用的生命周期包含了四个主要阶段:RAIL
按照时间先后顺序:
- Load, 加载阶段,有大概1秒的时间来渲染网页,然后用户的关注级别降低,这个阶段要下载和加载关键资源
- Idle,闲置阶段,加载后进入闲置阶段。这时候适合执行不太重要的工作,确保在此之后出现的任何舞动都能及时作出响应,闲置时间在50毫秒左右,以便在用户开始交互时立刻停止闲置阶段
- Animations,动画阶段,例如用户滚动屏幕或者出现动画,只有16毫秒的时间来渲染一帧,这样才能保证60fps
- Responsiveness,响应阶段,人类大脑可以忍受100毫秒的停顿时间,更久的话就会让人觉得卡顿不流畅。这就意味着应用要以某种方式在100毫秒内对用户输入做出响应。
第三课 卡顿杀伤性武器
学习使用Chrome的Devtools中的性能分析面板Performance(以前的Timeline)
现在的面板和课程讲解中已经发生了很大声变化,但是大体思路是相同的,录制之后再中间的Main
的下拉部分去分析什么导致的卡顿
它是倒火焰图,上方的行为调用了下方的命令,不同的行为有不同的颜色表示,黄色是JavaScript脚本,紫色是重新计算样式,绿色是绘制
可以找到哪个行为事件过长(大于16ms)导致卡顿,然后进行分析、优化
第四课 JavaScript
JavaScript运行时并不是我们编写的样子,而是通过JavaScript解释器提供的即时编译器,编译为更有效率的代码来执行的,所以编写代码时没有必要去做一些while
和for
谁更快之类的微优化。
JavaScript是渲染管道的开始,后续有重新计算样式、重排、重绘、复合的过程。要想不卡顿需要达到60fps,所以每一帧只有16ms的时间,分配给JavaScript部分的也就只有10-12ms。所以在每一帧渲染开始时,应当尽早执行JavaScript函数,这样才能够有足够的时间完成后续的过程。
在执行JavaScript动画时,应当使用requestAnimationFrame
来代替setTimeout
/setInterval
,因为后者不会关注渲染管道的流程,而前者会自动的在每一帧开始时运行JavaScript函数。
1 | function animate() { |
可以使用上节课提到的Chrome的performance
面板分析JavaScript的运行时间和内存占用情况。JavaScript会自动进行内存回收,所以我们不必担心指针、删除对象、局部变量的内存占用问题。可以通过performance
分析是否存在内存泄漏等情况。
当有大量耗时的计算任务时,可以考虑使用Web Worker,将耗时的任务移动到Web Worker线程进行计算,而主线程(Main Thread)负责渲染流畅的UI。
Web Worker可以在不同于主窗口的线程下,完全独立的操作系统线程下手运行JavaScript。主线程与Web Worker线程之间通过postMessage
和onmessage
事件进行通信,各个Worker之间不能通信。
主线程:
1 | const myWorker = new Worker('./scripts/worker.js'); |
Worker线程 worker.js
1 | // 通过importScipts导入其他脚本 |
第五课 样式和布局
重新计算样式(Recalculate Style)造成的性能代价与元素数量基本上是线性增长的关系。
可以采用BEM规则来为CSS的类命名,更加模块化、可复用、可读性好,且新跟那个好(因为使用class选择器的关系)
BEN中的B是Block,指一个UI构成单元,E是Element,是B的后代,M是Modifer,表达状态,比如three、current、active等。
BEM使用连接符连接BEM,但不能使用相同的连接符连接BEM,例如使用
__
连接BE,使用_
连接EM,使用-
做连字符,比如header__item-list_active
第二个CSS选择器是速度快的,它只使用了BEM规则的类选择器,不仅性能最好,而且可读性良好。
选择器性能优化有时候不如良好的布局导致的节点数减少带来的性能提升更高。
当在一个循环中,先访问一些尺寸、位置等会导致Layout的属性,然后再改变尺寸,重新计算样式计算,会导致强制布局,当反复如此,会出现布局抖动。
应当尽量避免强制布局,在循环外读取属性,在循环内改变尺寸,不会造成强制布局。
第六课 合成和绘制
可以使用Chrome的分析工具来分析页面的绘制过程,但是课程中的Show paint rectangles
已经不再原来的位置了
为了分析绘制过程,需要在开发者的工具的More tools
里面找到Rendering
,在打开的Rendering
面板中勾选Paint flasing
选项:
勾选之后,它会告诉你绘制流程在页面上的什么地方发生了,何时发生了。当页面有元素被绘制时,对应的元素会显示为绿色:
还可以使用Paint Profiler来确定页面的那些区域被绘制了,何时绘制的。但是新版本的Chomre开启Paint很麻烦。首先勾选Enable advanced paint instruments(slow)
然后点击Record进行录制,录制结束后找到绿色的Paint块并点击:
点击之后再下方出现了Paint Profiler
的选项:
可以根据左侧的命令结合上方的新的时间线和右侧的绘制结果,查看每一刻的绘制情况和绘制命令。
绘制之后的流程就是合成,就是将多个图层合并成为一个,当改变一个图层时,不会引起其他图层的绘制。
看下面的练习题,左侧是一个导航栏,那些元素应该放在一个图层上?
一起移动的元素,应该放在一个图层上。
Chrome的性能分析记录中有两个与图层合成有关,一个是Update Layer Tree
。当Chrome的引擎需要知道页面的哪个图层时就会出现该记录,查看元素的样式,弄清楚需要多少图层,另一个是Compositie Layers
,浏览器将页面合成到一起发送给屏幕。
图层越多,图层管理和合成花费的时间就越多,所以需要在减少绘制时间和增加图层管理时间二者之间做出权衡。
图层管理大多数情况下是浏览器自动完成的,但是当遇到绘制问题时,可以考虑将某个元素单独放到一个图层中。在创建图层之前,应该看一下看元素是否已经有了自己的图层,在刚才的Rendering
的功能面板在宏,勾选上Layers borders
选项
勾选之后,页面上除了绿色的框框,还会出现褐色的框框,绿色框框是浏览器对图层的划分,我们没有办法控制,橘色的框框表示元素位于自己的合成图层上。
如何创建属于自己的图层呢?
一般来说有两种方式:
will-change
,使用will-change
,属性值可以是transform
、left
、top
等外观属性,浏览器会根据这些提示为这些属性进行布局和绘制流程,由于浏览器创建图层也是要耗费性能的,使用will-change
最大的好处就是避免浏览器匆忙创建图层带来的性能代价。- 使用3D移动
transform: translateZ(0)
或者transform: translate3D(0, 0, 0)
这两种方式是在为静止的元素(不改变其原始位置)创建单独图层,实际上还有一些其他的方式也会创建单独的图层,比如:
transform
的对应移动,transalte
、rotate
、scale
等video
/canvas
/iframe
等元素opacity
改变position: fixed
filter
- 有合成层后代,并且自身
overflow
不为visible
(隐式合成)
Chrome创建层的标准是什么呢?完整的标准:
What else gets its own layer? Chrome’s heuristics here have evolved over time and continue to, but currently any of the following trigger layer creation:
- 3D or perspective transform CSS properties
<video>
elements using accelerated video decoding<canvas>
elements with a 3D (WebGL) context or accelerated 2D context- Composited plugins (i.e. Flash)
- Elements with CSS animation for their opacity or using an animated transform
- Elements with accelerated CSS filters
- Element has a descendant that has a compositing layer (in other words if the element has a child element that’s in its own layer)
- Element has a sibling with a lower z-index which has a compositing layer (in other words the it’s rendered on top of a composited layer)
可以参考这篇文章,讲得不错。
可以使用Layesr
工具来查看页面中有多少个图层:
注意要避免隐式提升,下图中的totes promited
成为单独图层的原因就是它覆盖在了于具有单独图层的元素的上方
所有元素都提升为单独的图层会消耗大量内存,并花费很多时间,在移动设备上问题更加明显。所以将元素提升到图层上时一定要谨慎,因为有可能会不小心由于存在重叠而创建了大量的其他的图层。
总结
如何提升性能:
(1)JS
- 避免多次访问尺寸、更改尺寸,导致布局抖动、强制布局
- 使用
requestAnimation
来代替setTimeout
/setInterval
创建动画 - 使用Web Worker,将复杂的计算过程放到子线程,避免主线程(渲染线程)的卡顿
(2)CSS
- 使用
will-change
提示浏览器将要发生的变化,并且创建单独的图层 - 使用3D变化来创建单独的图层
- 使用
transform
和opacity
来创建单独的图层,实现动画 - 更多的使用类选择器(BEM)来提升选择元素的
- 避免隐式提升创建太多的图层
分析工具:
Performance
Rendering
Laryouts