本文摘至网络(Code and Life)
Web页面的性能
我们每天都会浏览很多Web页面, 使用很多基于Web的应用, 这些Web页面看起来不一样, 用途也都各不相同, 有在线视频, 新闻, 邮件客户端, 甚至图形编辑等等, 虽然有不同的用途, 但他们背后的原理都是一样的:
- 用户输入网址
- 浏览器加载HTML/CSS/JavaScript, 图片资源等等
- 浏览器将结果绘制成图形
- 用户通过鼠标或键盘等与页面进行交互
![](https://img.haomeiwen.com/i9890665/75f9c46fcb491159.png)
这些各类繁多的页面, 在用户体验方面也有很大的差异, 有的响应很快, 用户很容易就可以完成自己想要做的事件;有的则很慢, 让焦躁的用户在漫长的等待后拂袖而去. 显而易见, 性能是影响用户体验的一个非常重要的因素, 而影响性能的因素非常多, 从用户输入网址, 到用户最终看到结果, 需要有很多的参与方共同努力. 这些参与方中任何一个环节的性能都会影响到用户体验.
- 网速
- DNS服务器的响应速度
- 服务器的处理能力
- 数据库性能
- 路由转发
- 浏览器处理能力
早在2006年, 雅虎就发布了提升站点性能的指南, Google也发布了类似的指南. 而且有很多工具可以和浏览器一起工作, 对你的Web页面的加载速度进行评估: 分析页面中资源的请求数量, 传输是否采用了压缩, JS, CSS是否进行了精简, 有没有合理的使用缓存等等.
浏览器是如何工作的, 要将一个页面渲染成用户可以看到的图形, 浏览器都需要做什么, 哪些过程比较耗时, 以及如何避免这些过程(或者至少可以更高效的方式)
页面是如何被渲染的
说到性能wdwx,规则之一就是:
If you can't measure it, you can't improve it. - Peter Drucker
根据浏览器的工作原理, 我们可以分别对各个阶段进行度量.
![](https://img.haomeiwen.com/i9890665/3f07cbb2b7abc6bf.png)
像素渲染流水线
- 下载HTML文档
- 解析HTML文档, 生成DOM
- 下载文档中引用的CSS, JavaScript
- 解析CSS样式表, 生成CSSOM
- 将JS代码交给JS引擎执行
- 合并DOM和CSSOM, 生成Render Tree
- 根据Render Tree进行布局Layout(为每个元素计算尺寸和位置信息)
- 绘制(Paint)每个层的元素
- 执行图层合并(Composite Layers)
使用Chrome的DevTools - Timing, 可以很容易的获取一个页面的渲染情况, 比如在Events Log页签上, 我们可以看到每个阶段的耗时细节(清晰起见, 我没有显示Loading和Scripting的耗时)
![](https://img.haomeiwen.com/i9890665/fd8011f0f87444fa.png)
上图中的Activity中, Recalculate Style就是上面的构建CSSOM的过程, 其余Activity都分别于上述的过程匹配.
应该注意的是, 浏览器可能会将Render Tree分成好几个层来分别绘制, 最后合并起来形成最终的结果, 这个过程一般发生在GPU中.
Devtools中有一个选项: Rendering - Layers Borders, 打开这个选项之后, 你可以看到每个层, 每个元素的边界. 浏览器可能会启动多个线程来绘制不同的层的元素.
![](https://img.haomeiwen.com/i9890665/f5f573e73b77c5ed.png)
Chrome还提供一个TimeLine的高级功能, 鼠标放到时间线上可以看到某一时刻页面的渲染效果, 当时了你也可以利用这个特性录制一个页面加载到渲染完成的视频.
常规策略
为了尽快的让用户看到页面内容, 我们需要快速的完成DOM+CSSOM - Layout - Paint - Composite Layers的整个过程. 一切会阻塞DOM生成, 阻塞CSSOM生成的动作都应该尽可能的消除, 或者延迟.
在这个前提下, 常用的做法有两种:
分割CSS
对于不同的浏览终端, 同一终端的不同模式, 我们可能会提供不同的规则集:
![](https://img.haomeiwen.com/i9890665/fcbf82102781770a.png)
如果将这些内容写到统一的文件中, 浏览器需要下载并解析这些内容(虽然不会实际应用这些规则). 更好的做法是, 将这些内容通过对
link
元素的media属性来指定 :![](https://img.haomeiwen.com/i9890665/288c71a9b7136633.png)
这样, print.css和landscape.css的内容不会阻塞Render Tree的建立, 用户可以更快的看到页面, 从而获得更好的体验.
高效的CSS规则
CSS规则的优先级
很多使用SASS/LESS的开发人员, 太过分的喜爱嵌套规则的特性, 这可能会导致复杂的, 无必要深层次的规则, 比如:
![](https://img.haomeiwen.com/i9890665/82aecced14d8cc46.png)
在生成的CSS中, 可以看到:
![](https://img.haomeiwen.com/i9890665/b8e04aa5ecfd2d74.png)
而这个层次可能并非必要的. CSS规则越复杂, 在构建Render Tree时, 浏览器花费的时间越长. CSS规则有自己的优先级, 不同的写法对效率也会有影响. 特别是当规则很多的时候. 这里有一篇关于CSS规则优先级的文章可供参考.
使用GPU加速
很多动画都会定时执行, 每次执行都可能会导致浏览器的重新布局, 比如:
![](https://img.haomeiwen.com/i9890665/2c9f101e5344257f.png)
这些内容可以放到GPU加速执行(GPU是专门设计来进行图形处理的, 在图形处理上, 比CPU要高效很多). 可以通过使用
transform
来启动这一特性.![](https://img.haomeiwen.com/i9890665/0b09af0e069b6ac0.png)
异步JavaScript
我们知道, JavaScript的执行会阻塞DOM的构建过程, 这是因为JavaScript中可能有DOM操作:
![](https://img.haomeiwen.com/i9890665/c7bf45bdf52265a9.png)
因此浏览器会等待JavaScript引擎的执行, 执行结束之后, 再恢复DOM的构建. 但是并不是所有的JavaScript都会涉及DOM操作. 比如审计信息, WebWorker等, 对于这些脚本, 我们可以显式地指定脚本是不阻塞DOM渲染的.
![](https://img.haomeiwen.com/i9890665/be6203f2d1b03ef7.png)
带有
async
标记的脚本, 浏览器仍然会下载它, 并在合适的时机执行, 但是不会影响DOM树的构建过程.
首次渲染之后
在首次渲染之后, 页面上的元素还可能被不断的重新布局, 重新绘制. 如果处理不当, 这些动作可能会产生性能问题, 产生不好的用户体验.
- 访问元素的某些属性
- 通过JavaScript修改的CSS属性
- 在onScroll中做耗时任务
- 图片的预处理(事先裁剪图片, 而不是依赖浏览器在布局时的缩放)
- 在其他Event Handler中做耗时任务
- 过多的动画
- 过多的数据处理(可以考虑放入webworker内执行)
强制同步布局/回流
元素的一些属性和方法, 当在被访问或者被调用的时候, 会触发浏览器的布局动作(以及后续的Paint动作), 而布局基本上都会波及页面上的所有元素, 当页面元素比较多的时候, 布局和绘制都会花费比较大.
通过TimeLine, 有时候你会看到这样的警告:
![](https://img.haomeiwen.com/i9890665/75c8ee79a59abb80.png)
比如访问一个元素的offsetWidth(布局宽度)属性时, 浏览器需要重新计算(重新布局), 然后才能返回最新的值. 如果这个动作发生在一个很大的循环中, 那么浏览器就不得不进行多次重新布局, 这可能会产生严重的性能问题:
![](https://img.haomeiwen.com/i9890665/b81eb23b77d07269.png)
正确的做法是, 先将这个值读出来, 然后缓存在一个变量上(触发一次重新布局), 以便后续使用:
![](https://img.haomeiwen.com/i9890665/7b006fcc2ae02766.png)
这里有一个完整的列表触发布局
CSS样式修改
布局相关属性修改
修改布局相关属性, 会触发Layout - Paint - Composite Layers, 比如对位置, 尺寸信息的修改:
![](https://img.haomeiwen.com/i9890665/7f5529e3ba54e926.png)
绘制相关属性修改
修改绘制相关属性, 不会触发Layout, 但会触发后续的Paint - Composite Layers, 比如对背景色, 前景色的修改:
![](https://img.haomeiwen.com/i9890665/767b134a382cff7c.png)
其他属性
除了上边的两种之外, 有一些特别的属性可以在不同的层中单独绘制, 然后再合并图层, 对这种属性的访问(如果正确使用了CSS)不会触发Layout - Paint, 而是直接进行Compsite Layers:
- transform
- opacity
transform
展开的话又分为:translate
,scale
,rotate
等, 这些层应该放入单独的渲染层中, 为了对这个元素创建一个独立的渲染层, 你必须提升该元素.
可以通过这样的方式来提升该元素:
image.png
CSS属性will-change为Web开发者提供了一种告知浏览器该元素会有哪些变化的方法, 这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作.
当然, 额外的层次并不是没有代价的. 太多的独立渲染层, 虽然缩减了Paint的时间, 但是增加了Composite Layers的时间, 因此需要仔细权衡. 在作调整之前, 需要Timeline的运行结果来做支持.
还记得性能优化的规则一吗:
If you can't measure it, you can't improve it. - Peter Drucker.
CSS Triggers是一个完整的CSS属性列表, 其中包含了会影响布局或绘制的CSS属性, 以及在不同的浏览器上的不同表现.
总结
了解浏览器的工作方式, 对我们做前端页面渲染性能的分析和优化都非常有帮助. 为了高效而智能的完成渲染, 浏览器也在不断的进行优化, 比如资源的预加载, 更好的利用GPU(启用更多的线程来渲染)等等.
另一方面, 我们在编写前端的HTML, JS, CSS时, 也需要考虑浏览器的现状: 如何减少DOM, CSSOM的构建时间, 如何将耗时任务放在单独的线程中(通过webworker).
网友评论