2.1.浏览器渲染过程(Webkit)
其实大家应该对浏览器的HTML渲染机制比较熟悉了,基本流程同上图所述,大家在入门的时候,你的导师或者前辈可能会告诉你,在渲染方面我们要减少重排和重绘,因为他们会影响浏览器性能。不过你一定不知道其中原理是什么,对吧。今天我们就结合《Webkit技术内幕》(这本书我还是很推荐大家买来看看,好歹作为一名前端工程师,你得知道我们天天接触的浏览器内核是怎样工作的)的相关知识,给大家普及普及那些深层次的概念。
PS:这里提到了Webkit内核,我顺带提一下浏览器内部的渲染引擎、解释器等组件的关系,因为经常有师弟或者一些前端爱好者向我问这方面的知识,分不清他们的关系,我就拿一张图来说明:(这部分内容与本文无关,如果你对此不感兴趣,可以直接跳过)
浏览器的解释器,是包括在渲染引擎内的,我们常说的Chrome(现在使用的是Blink引擎)和Safari使用的Webkit引擎,Firefox使用的Gecko引擎,指的就是渲染引擎。而在渲染引擎内,还包括着我们的HTML解释器(渲染时用于构造DOM树)、CSS解释器(渲染时用于合成CSS规则)还有我们的JS解释器。不过后来,由于JS的使用越来越重要,工作越来越繁杂,所以JS解释器也渐渐独立出来,成为了单独的JS引擎,就像众所周知的V8引擎,我们经常接触的Node.js也是用的它。
2.2.DOM渲染层与GPU硬件加速
如果我告诉你,一个页面是由许多许多层级组成的,他们就像千层面那样,你能想象出这个页面实际的样子吗?这里为了便于大家想象,我附上一张之前Firefox提供的3D View插件的页面Layers层级图:
对,你没看错,页面的真实样子就是这样,是由多个DOM元素渲染层(Layers)组成的,实际上一个页面在构建完Render Tree之后,是经历了这样的流程才最终呈现在我们面前的:
①浏览器会先获取DOM树并依据样式将其分割成多个独立的渲染层
②CPU将每个层绘制进绘图中
③将位图作为纹理上传至GPU(显卡)绘制
④GPU将所有的渲染层缓存(如果下次上传的渲染层没有发生变化,GPU就不需要对其进行重绘)并复合多个渲染层最终形成我们的图像
从上面的步骤我们可以知道,布局是由CPU处理的,而绘制则是由GPU完成的。
其实在chrome中,也为我们提供了相关插件供我们查看页面渲染层的分布情况以及GPU的占用率:(所以说,平时我们得多去尝试尝试chrome的那些莫名其妙的插件,真的会发现好多东西都是神器)
chrome开发者工具菜单→more tools→Layers(开启渲染层功能模块)
chrome开发者工具菜单→more tools→rendering(开启渲染性能监测工具)
执行上面的操作后,你会在浏览器里看到这样的效果:
太多东西了,分模块讲吧:
(一)最先是页面右上方的小黑窗:其实提示已经说的很清楚了,它显示的就是我们的GPU占用率,能够让我们清楚地知道页面是否发生了大量的重绘。
(二)Layers版块:这就是用于显示我们刚提到的DOM渲染层的工具了,左侧的列表里将会列出页面里存在哪些渲染层,还有这些渲染层的详细信息。
(三)Rendering版块:这个版块和我们的控制台在同一个地方,大家可别找不到它。前三个勾选项是我们最常使用的,让我来给大家解释一下他们的功能(充当一次免费翻译)
①Paint flashing:勾选之后会对页面中发生重绘的元素高亮显示
②Layer borders:和我们的Layer版块功能类似,它会用高亮边界突出我们页面中的各个渲染层
③FPS meter:就是开启我们在(一)中提到的小黑窗,用于观察我们的GPU占用率
可能大家会问我,提到DOM渲染层这么深的概念有什么用啊,好像跟性能优化没一点关系啊?大家应该还记得我刚说到GPU会对我们的渲染层作缓存对吧,那么大家试想一下,如果我们把那些一直发生大量重排重绘的元素提取出来,单独触发一个渲染层,那样这个元素不就不会“连累”其他元素一块重绘了对吧。
那么问题来了,什么情况下会触发渲染层呢?大家只要记住:
Video元素、WebGL、Canvas、CSS3 3D、CSS滤镜、z-index大于某个相邻节点的元素都会触发新的Layer,其实我们最常用的方法,就是给某个元素加上下面的样式:
transform: translateZ(0);
backface-visibility: hidden;
这样就可以触发渲染层啦 。
我们把容易触发重排重绘的元素单独触发渲染层,让它与那些“静态”元素隔离,让GPU分担更多的渲染工作,我们通常把这样的措施成为硬件加速,或者是GPU加速。大家之前肯定听过这个说法,现在完全清楚它的原理了吧。
2.3.重排与重绘
现在到我们的重头戏了,重排和重绘。先抛出概念:
①重排(reflow):渲染层内的元素布局发生修改,都会导致页面重新排列,比如窗口的尺寸发生变化、删除或添加DOM元素,修改了影响元素盒子大小的CSS属性(诸如:width、height、padding)。
②重绘(repaint):绘制,即渲染上色,所有对元素的视觉表现属性的修改,都会引发重绘。
我们习惯使用chrome devtools中的performance版块来测量页面重排重绘所占据的时间:
①蓝色部分:HTML解析和网络通信占用的时间
②黄色部分:JavaScript语句执行所占用时间
③紫色部分:重排占用时间
④绿色部分:重绘占用时间
不论是重排还是重绘,都会阻塞浏览器。要提高网页性能,就要降低重排和重绘的频率和成本,近可能少地触发重新渲染。正如我们在2.3中提到的,重排是由CPU处理的,而重绘是由GPU处理的,CPU的处理效率远不及GPU,并且重排一定会引发重绘,而重绘不一定会引发重排。所以在性能优化工作中,我们更应当着重减少重排的发生。
这里给大家推荐一个网站,里面详细列出了哪些CSS属性在不同的渲染引擎中是否会触发重排或重绘:
csstriggers.com/ (图片来自官网)
2.4.优化策略
谈了那么多理论,最实际不过的,就是解决方案,大家一定都等着急了吧,做好准备,一大波干货来袭:
(一)CSS属性读写分离:浏览器每次对元素样式进行读操作时,都必须进行一次重新渲染(重排 + 重绘),所以我们在使用JS对元素样式进行读写操作时,最好将两者分离开,先读后写,避免出现两者交叉使用的情况。最最最客观的解决方案,就是不用JS去操作元素样式,这也是我最推荐的。
(二)通过切换class或者使用元素的style.csstext属性去批量操作元素样式。
(三)DOM元素离线更新:当对DOM进行相关操作时,例、appendChild等都可以使用Document Fragment对象进行离线操作,带元素“组装”完成后再一次插入页面,或者使用display:none 对元素隐藏,在元素“消失”后进行相关操作。
(四)将没用的元素设为不可见:visibility: hidden,这样可以减小重绘的压力,必要的时候再将元素显示。
(五)压缩DOM的深度,一个渲染层内不要有过深的子元素,少用DOM完成页面样式,多使用伪元素或者box-shadow取代。
(六)图片在渲染前指定大小:因为img元素是内联元素,所以在加载图片后会改变宽高,严重的情况会导致整个页面重排,所以最好在渲染前就指定其大小,或者让其脱离文档流。
(七)对页面中可能发生大量重排重绘的元素单独触发渲染层,使用GPU分担CPU压力。(这项策略需要慎用,得着重考量以牺牲GPU占用率为代价能否换来可期的性能优化,毕竟页面中存在太多的渲染层对于GPU而言也是一种不必要的压力,通常情况下,我们会对动画元素采取硬件加速。)