概述
今年赶上疫情,来研究一下项目性能做做提升,那么前端到底该做哪些工作来提升我们的代码性能,从性能出发review过往代码的不足? 本文从浏览器渲染页面开始聊一聊浏览器的重绘和重排,认真阅读本文,理解了这2点根本上的原因,相信对前端性能优化方案分析和理解能有更多了解。
浏览器渲染页面
1.浏览器会把下载到的html资源和css资源分别解析成dom树和样式结构体,每个html节点都是dom树中的一个节点,根节点为document;样式结构体的解析过程则是将样式转化为浏览器能识别的css样式(去除其他兼容性样式);
2.dom tree和样式结构体结合形成render tree,这一过程主要是将dom节点和css样式对应起来,给每个dom节点匹配样式,并且解析需要渲染的节点添加到render tree,例如display为none的节点不会添加到render tree中而visibilty为hidden的隐藏节点则会添加到render tree;
3.最后就是浏览器根据render tree来进行页面的绘制了;
重排和重绘
重排是指render tree中的一部分元素或正题因为其尺寸,布局,是否存在于dom tree等原因而必须重新编排dom tree的布局,这个过程我们称为重排,举例来说,根据上图重排过程就是当使用js操作页面时,会先根据dom tree和样式表,计算得出某些改变节点尺寸定位的样式会影响自身和整个页面其他节点的布局(也有可能是某些类似resize事件改变所有节点的样式),因此需要重新计算整个页面节点或部分节点所占的空间大小和布局情况,这个重新计算排版的过程就是重排,每个页面至少要经历一次重排,也就是页面首次加载的时候;
在重排的过程中势必会将render tree中受到影响的部分失效,并且重新构造这部分渲染树并最终绘制到屏幕中,这个绘制过程将影响render tree中部分元素的颜色图像边框阴影等外观风格样式,不会影响到页面的布局,这个绘制过程就称为重绘;
综上所述,我们了解到为什么说重排必将引起重绘而重绘不一定会引起重排,因为对元素进行不影响页面布局的属性的操作(重绘)是不会引发dom tree的布局的,也就不会引起重排;
重绘和重排会不断触发,这是不可避免的,因为重排的过程会根据样式表和dom tree进行重新排列页面元素的计算,因此越是复杂的层级深的样式表和dom tree触发重排和重绘所要消耗的资源成本越大,而前端优化的主要优化点就是尽量的减少浏览器的重排和重绘以及围绕他们的优化操作;
ok,接下来进入正题,我们来就上面我们了解到的重排和重绘的原因来聊聊优化的方案;
前端优化方案
1.页面布局流-减少每次重排和重绘的成本
之前提到的一个结论就是越复杂层级越深的样式表和dom tree会增加重排和重绘耗费的资源,原因就是在重排和重绘的计算过程会根据dom tree逐层遍历解析定位css和dom 节点的关系计算最终样式,生成和修改render tree的过程都不可避免的要经过这个计算过程,因此 精简页面的dom结构和css样式代码层级,合理页面布局是第一个优化方案;
2.代码编写流-减少重排和重绘
在说页面重排时我们提到改变页面布局的操作最终触发重排,这里先列举一下:
- 添加或者删除可见的DOM元素;
- 元素位置改变;
- 元素尺寸改变——边距、填充、边框、宽度和高度
- 内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变;
- 页面渲染初始化;
- 浏览器窗口尺寸改变——resize事件发生时
具体哪些属性会造成浏览器的重绘和重排可以在这个网站上查询到CSS Triggers;
上述这些操作因为会改变元素自身或者页面其他元素的大小定位导致触发重排,每次上述操作都会导致浏览器的重排重绘,浏览器当然也注意到了这点,因此浏览器会设计一个队列,每次重排和重绘操作都会进入一个队列,只有当队列中的操作到了一定的数量或者一定的时间间隔就会触发flush这个队列,进行一次批量的处理从而达到多次重排重绘变成一次重排重绘,因此就有了这样的优化方案:
- 多次针对上述样式的操作通过合并多次样式修改使其在同一个队列里完成,例如动态修改className合并多次修改dom节点样式的操作,采用display为none不会存在render tree中,修改完其他样式再反转显示可以让重排只发生2次;
- 并且操作尽量减少对其他元素的布局影响来优化添加删除元素时尽量减少对其他元素布局的影响(举个栗子,往dom tree最前面添加一个元素可能导致整个render tree重排而在body最后面插入元素则可能不会影响前面的元素;)
这里有一个优化点要展开,虽然浏览器会使用队列来优化合并多次重排操作,但当我们做了访问元素的准确样式和位置的操作时(例如offsetTop, offsetLeft, offsetWidth, offsetHeight,scrollTop/Left/Width/Height,clientTop/Left/Width/Height,width,height等)浏览器为了给我们最精确的值会提前flush队列,(例如修改某些样式的中间有访问元素这些style的操作,这时如果等队列满足执行条件执行完成再获取就可能取到错误的值),因此我们上述合并多次样式修改利用浏览器的优化机制减少重排重绘的方法就可能会因为访问了这类属性失效,在编写动画效果时会经常遇到这样的属性访问和操作,因此也就引出了下面几点优化方法:
- 不要经常访问因此浏览器flush队列的属性,必要时可以采用缓存的方式(计算属性时将这类属性值缓存出来,计算完成后一次性修改样式);
- 让元素脱离动画流,减少render tree的规模
- 尽量减少js动画,使用性能更加友好的
requestAnimationFrame替代setInterval,使用css3 的transform属性达到开启动画线程(GPU)的方式优化动画操作,GPU可以高效不断的绘制相同位图,将同一位图进行位移翻转和缩放,使用transform相比较直接操作会减少元素的重绘和重排次数,同一位图多次操作都会在gpu上高效完成,本篇不专门讨论动画性能的优化就不展开了;
3.算法架构流-减少dom操作
虽然可以通过合理布局来精简我们的dom结构,但是随着需求的日益增长,所面临的网页功能也就越来越复杂,所对应的dom 树也非常庞大复杂,每次对dom树的修改操作都会引起浏览器的重排和重绘,因此如何减少dom操作也就成了非常重要的性能优化方案,这里引出了react所实现的虚拟dom,通过虚拟dom,可以将新旧dom的变化计算交给js,一次性更新dom的变化从而减少浏览器的重绘和重排;
这里提一下,react从没有说过虚拟dom会比优化直接操作dom的方式更高效,只是在普适的情况下考虑工程开发的效率,在构建任何一个实际应用的时候,我们不可能对每个dom操作都做手动优化。
但是diff->patch,这两个过程,重点在于diff后patch的操作变得有多简单,差距在于diff算法所耗费的资源有多大,算法有多省时;
emm,如果有下次博客更新的话,应该会总结一下react的性能优化方案,分析总结一下原理。
初次分享,如有问题,还有指正。