渲染优化(页面布局与重绘的优化)

页面布局与重绘的优化

页面布局也叫作重排和回流,指的是浏览器对页面元素的几何属性进行计算并将最终结果绘制出来的过程。凡是元素的宽高尺寸、在页面中的位置及隐藏或显示等信息发生改变时,都会触发页面的重新布局。

通常页面布局的作用范围会涉及整个文档,所以这个环节会带来大量的性能开销,我们在开发过程中,应当从代码层面出发,尽量避免页面布局或最小化其处理次数。如果仅修改了DOM元素的样式,而未影响其几何属性时,则浏览器会跳过页面布局的计算环节,直接进入重绘阶段。

虽然重绘的性能开销不及页面布局高,但为了更高的性能体验,也应当降低重绘发生的频率和复杂度。本节接下来便针对这两个环节的性能优化给出一些实用性的建议。

一、触发页面布局与重绘的操作

​ 要想避免或减少页面布局与重绘的发生,首先就是需要知道有哪些操作能够触发浏览器的页面布局与重绘的操作,然后在开发过程中尽量去避免。

​ 这些操作大致可以分为三类:首先就是对DOM元素几何属性的修改,这些属性包括width、height、 padding、 margin、 left、 top 等,某元素的这些属性发生变化时,便会波及与它相关的所有节点元素进行几何属性的重新计算,这会带来巨大的计算量;其次是更改DOM树的结构,浏览器进行页面布局时的计算顺序,可类比树的前序遍历,即从上向下、从左向右。

​ 这里对DOM树节点的增、删、移动等操作,只会影响当前节点后的所有节点元素,而不会再次影响前面已经遍历过的元素;最后一类是获取某些特定的属性值操作,比如页面可见区域宽高offsetWidth offsetHeight, 页面视窗中元素与视窗边界的距离offsetTop,offsetLeft,类似的属性值还有scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientWidth、clientHeight及调用window.getComputedStyle方法。

​ 这些属性和方法有一个共性,就是需要通过即时计算得到,所以浏览器就需要重新进行页面布局计算。

二、避免对样式的频繁改动

​ 在通常情况下,页面的一帧内容被渲染到屏幕上会按照如下顺序依次进行,首先执行JavaScript代码,然后依次是样式计算、页面布局、绘制与合成。如果在JavaScript运行阶段涉及上述三类操作,浏览器就会强制提前页面布局的执行,为了尽量降低页面布局计算带来的性能损耗,我们应当避免使用JavaScript对样式进行频繁的修改,如果一定要修改样式,则可通过以下几种方式来降低触发重排或回流的频次。

1.使用类名对样式逐条修改

​ 在JavaScript代码中逐行执行对元素样式的修改,是种糟糕的编码方式,对未形成编码规范的前端初学者来说经常会出现这类的问题。错误代码示范如下:

//获取DOM元素逐行修改样式
const div = document .getElementById('mydiv');
div.style.height = '100px';
div.style.width = '100px';
div.style.border = '2px solid blue'

​ 上述代码对样式逐行修改,每行都会触发一次对渲染树的更改, 于是会导致页面布局重新计算而带来巨大的性能开销。合理的做法是,将多行的样式修改合并到一个类名中,仅在JavaScript脚本中添加或更改类名即可。CSS 类名可预先定义:

.my-div{
    height: 100px;
    border: 2px solid blue;
    width: 100px; 
}

然后统一在JavaScript中通过给指定元素添加类的方式一次完成,这样便可避免触发多次对页面布局的重新计算:

const div = document.getElementById('mydiv');
mydiv.classList.add('my-div');

2.缓存对敏感属性值的计算

​ 有些场景我们想要通过多次计算来获得某个元素在页面中的布局位置,比如:

const list = document.getElementById('list');
for (let i =0; i<10; i++) {
list.style.top = '${list.offsetTop + 10}px';
list.style.left = '${list.offsetLeft + 10}px';
}

​ 这不但在赋值环节会触发页面布局的重新计算,而且取值涉及即时敏感属性的获取,如offsetTop和offsetLeft,也会触发页面布局的重新计算。这样的性能是非常糟糕的,作为优化我们可以将敏感属性通过变量的形式缓存起来,等计算完成后再统一进行赋值触发布局重排。

const list = document.getElementById('list');
//将敏感属性缓存起来
let offsetTop = list.offsetTop, offsetLeft = list.offsetLeft;
for(let i=0;i<10;i++){
offsetTop += 10;
offsetLeft += 10;
}
//计算完成后统一赋值触发重排
list.sty1e.left = offsetLeft;
list.sty1e.top = offsetTop;

3.使用requestAnimationFrame方法控制渲染帧

前面讲JavaScript动画时,提到了requestAnimationFrame方法可以控制回调在两个植染帧之间仅触发一次, 如果在其回调函数中一开始就取值 到即时敏感属性,其实获取的是上一帧旧布局的值,并不会触发页面布局的重新计算。

//在帧开始时触发回调
requestAnimationFrame (queryDivHeight);
function queryDivHeight() {
const div = document.getElementById('div')
//获取并在命令行中打印出指定div元素的高
console.log (div.offsetHeight)
}

如果在请求此元素高度之前更改其样式,浏览器就无法直接使用上一帧的旧有属性值,而需要先应用更改的样式,再运行页面布局计算后,才能返回所需的正确高度值。这样多余的开销显然是没有必要的。因此考虑到性能因素,在requestAnimationFrame方法的回调函数中,应始终优先样式的读取,然后再执行相应的写操作:

//requestAnimationFrame方法的回调函数
function queryDivHeight () {
const div = document .getElementById('div')
//获取并在命令行中打印出指定div元素的高
console.log (div.offsetHeight)
//样式的写操作应放在读取操作后进行
div.classList.add('my-div')
}

三、通过工具对绘制进行评估

除了通过经验去绕过一些明显的性能缺陷,使用工具对网站页面性能进行评估和实时分析也是发现问题的有效手段。这里介绍一 些基于Chrome开发者工具的分析方法,来辅助我们发现渲染阶段可能存在的性能问题。

1.监控渲染信息

打开Chrome的开发者工具,可以在“设置”→“更多工具”中,发现许多很实用的性能辅助小工具,比如监控渲染的Rendering工具,如图所示。

​ 打开Rendering 的工具面板后,会发现许多功能开关与选择器,下面举例介绍其中若干常用功能项。首先是Paint flashing, 当我们开启该功能后,操作页面发生重新渲染,Chrome会让重绘区域进行一次绿色闪动。

​ 这样就可以通过观察闪动区域来判断是否存在多余的绘制开销,比如若仅单击Select组件弹出下拉列表框,却发现整个屏幕区域都发生了闪动,或与此操作组件的无关区域发生了闪动,这都意味着有多余的绘制开销存在,需要进一步研究和优化。

​ Layer borders功能开启后,会在页面上显示出绘制的图层边界。

​ FPS meter功能开启后,会在当前页面的左上角显示实时的帧率情况,GPU 功能是否开启及GPU内存占用情况,如图所示。

2.查看图层详情

​ 当我们通过Rendering工具发现存在有多余的图层渲染时,由于闪动是难于捕捉的,所以还需要工具辅助显示出各个图层的详细信息,这便需要用到Layers图层工具,如图所示。

​ 如图所示工具界面大体分为三部分,①号矩形框区域为当前页面的图层列表:②号矩形框区域为页面带有图层边框的视图:③号矩形框区域为选中图层的详细信息,包括页面尺寸、内存占用、绘制次数等。

​ 通过这些信息能够帮助我们快速定位到所要查看的图层信息。当我们使用Rendering工具监控页面交互过程中有不恰当的图层存在时,便可使用Layers 工具进行问题复现:首先打开目标页面,然后从左 侧图层列表中依次查找出问题图层,接着分析引起该图层发生重绘的原因。

四、降低绘制复杂度

​ 如前所述,绘制是在页面布局确定后,将元素的可视内容绘制到屏幕上的过程。虽然不同的CSS绘制样式看不出性能上明显的不同,但并非所有属性都有同样的性能开销。例如,绘制带有阴影效果的元素内容,就会比仅绘制单色边框所耗费的时间要长,因为涉及模糊就意味着更高的复杂度。CSS属性如下:

// 绘制时间相对较短的边枢颜色
border-color: red;
//绘制时间更长的阴影内容
box-shadow: 0, 8px, rgba(255,0,0,0.5);

​ 当我们使用之前介绍过的渲染性能分析工具,发现了有明显性能瓶颈需要优化时,需要确认是否存在高复杂度的绘制内容,可以使用其他实现方式来替换以降低绘制的复杂度。比如位图的阴影效果,可以考虑使用Photoshop 等图像处理工具直接为图片本身添加阴影效果,而非全交给CSS样式去处理。

​ 除此之外,还要注意对绘制区域的控制,对不需要重新绘制的区域应尽量避免重绘。例如,页面的顶部有一个固定区域的header标头,若它与页面其他位置的某个区域位于同一图层,当后者发生重绘时,就有可能触发包括固定标头区域在内的整个页面的重绘。对于固定不变不期望发生重绘的区域,建议可将其提升为独立的绘图层,避免被其他区域的重绘连带着触发重绘。

五、合成处理

​ 合成处理是将已绘制的不同图层放在一起, 最终在屏幕上渲染出来的过程。在这个环节中,有两个因素可能会影响页面性能:一个是所需合成的图层数量,另一个是实现动画的相关属性。

1.新增图层

在降低绘制复杂度小节中讲到,可通过将固定区域和动画区域拆分到不同图层上进行绘制,来达到绘制区域最小化的目的。接下来我们就来探讨如何创建新的图层,最佳方式便是使用CSS属性will-change来创建:

.new-layer{
 will-change: transform; 
}

该方法在Chrome、Firefox 及Opera上均有效,而对于Safari等不支持will-change属性的浏览器,则可以使用3D变换来强制创建:

.new-layer{
 transform: translate(0); 
}

​ 虽然创建新的图层能够在定程度 上减少绘制区域,但也应 当注意不能创建太多的图层,因为每个图层都需要浏览器为其分配内存及管理开销。如果已经将个一元素提升到所创建的新图层上,也最好使用Chrome开发者工具中的Layers对图层详情进行评估,确定是否真的带来了性能提升,切忌在未经分析评估前就盲目地进行图层创建。

2.仅与合成相关的动画属性

在了解了谊染过程各部分的功能和作用后,我们知道如果个动画的实现不经过页面布局和重绘环节,仅在合成处理阶段就能完成,则将会节省大量的性能开销。目前能够符合这一要求的动画属性只有两个:透明度opacity 和图层变换transform. 它们所能实现的动画效果如表所示,其中用n来表示数字。

动画效果 实现方式
位移 transform:translate(npx,npx);
缩放 transform:scale(n);
旋转 transform:rotate(ndeg)
倾斜 transform:skew(X|Y)(ndeg);
矩阵变换 transform:matrix(3d)(/矩阵变换/)
透明度 opacity:0…1

​ 在使用opacity和transform实现相应的动画效果时,需要注意动画元素应当位于独立的绘图层上,以避免影响其他绘制区域。这就需要将动画元素提升至一个新的绘图层。

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2022-2023 alan_mf
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信