前端性能优化学习 06 渲染优化 03 页面布局与重绘优化、合成处理、小结

页面布局与重绘优化

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

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

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

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

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

这些操作大致可以分为三类:

(1)首先就是对 DOM 元素几何属性的修改,这些属性包括 widthheightpaddingmarginlefttop 等,当元素的这些属性发生变化时,便会波及与它相关的所有节点元素进行几何属性的重新计算,这会带来巨大的计算量;

(2)其次是更改 DOM 树的结构,浏览器进行页面布局时的计算顺序,可类比树的前序遍历,即从上向下、从左向右。这里对 DOM 树节点的增、删、移动等操作,只会影响当前节点后的所有节点元素,而不会再次影响前面已经遍历过的元素;

(3)最后一类是获取某些特定的属性值操作,比如页面可见区域宽高 offsetWidthoffsetHeight,页面视窗中元素与视窗边界的距离 offsetTopoffsetLeft,类似的属性值还有 scrollTopscrollLeftscrollWidthscrollHeightclientTopclientWidthclientHeight 及调用 window.getComputedStyle 方法。这些属性和方法有一个共性,就是需要通过即时计算得到,所以浏览器就需要重新进行页面布局计算。

避免对样式的频繁改动

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

使用类名对样式逐条修改

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

const div = document.getElementById('mydiv')

div.style.height = '100px'
div.style.width = '100px'
div.style.border = '1px solid blue'

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

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

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

const div = document.getElementById('mydiv')

div.classList.add('mydiv')

缓存对敏感属性值的计算

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

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`
}

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

const list = document.getElementById('list')
let {
    
     offsetTop, offsetLeft } = list

for (let i = 0; i < 10; i++) {
    
    
  offsetTop += 10
  offsetLeft += 10
}

// 计算完成后统一赋值触发重排
list.style.left = offsetLeft
list.style.top = offsetTop

使用 requestAnimationFrame 方法控制渲染帧

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

requestAnimationFrame(queryDivHeight)

function queryDivHeight () {
    
    
  const div = document.getElementById('div')
  console.log(div.offsetHeight)
}

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

requestAnimationFrame(queryDivHeight)

function queryDivHeight () {
    
    
  const div = document.getElementById('div')
  console.log(div.offsetHeight)
  // 样式的写操作应该放在读操作后进行
  div.classList.add('my-div')
}

降低绘制复杂度

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

// 绘制时间相对较短的边框颜色
border-color: red;

// 绘制时间较长的阴影内容
box-shadow: 0, 8px, rgba(255, 0, 0, 0.5);

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

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

通过工具对绘制进行评估

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

监控渲染信息

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

在这里插入图片描述

打开 Rendering 的工具面板后,会发现许多功能开关与选择器。

例如 Paint flashing,当我们开启该功能后,操作页面发生重新渲染,Chrome 会让重绘区域进行一次绿色闪动。

示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>布局与重绘</title>
  <style>
    .box {
      
      
      width: 100px;
      height: 100px;
      background-color: skyblue;
      margin-bottom: 10px;
    }
  </style>
</head>
<body>
<button onclick="change()">改变</button>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>

<script>
  function change() {
      
      
    const divs = document.querySelectorAll('.box')
    divs[0].style.width = '200px'
    // divs[0].style.height = '200px'
  }
</script>
</body>
</html>

在这里插入图片描述

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

查看绘制图层

开发人员工具的 Layers 面板(Edge 将功能挪到了 3D View 面板),可以查看浏览器生成的绘制图层,据此可以查看 HTML 内容的图层分布,可以将不需要重绘的内容单独提取到一层,降低绘制复杂度,避免其他内容的改变导致这块内容重绘。

示例:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .header {
      
      
        position: fixed;
        top: 0;
        left: 0;
        background-color: rgba(100, 1, 200, 0.4);
        height: 50px;
        width: 100px;
      }
      .box {
      
      
        width: 200px;
        height: 100px;
        padding: 50px;
        background-color: pink;
      }
    </style>
  </head>
  <body>
    <div class="header">Header</div>
    <div class="box">box</div>
    <button onclick="change()">改变</button>
    <script>
      function change() {
      
      
        const box = document.querySelector('.box')
        box.style.backgroundColor = 'skyblue'
      }
    </script>
  </body>
</html>

在这里插入图片描述

合成处理

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

新增图层

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

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

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

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

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

仅与合成相关的动画属性

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

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

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

小结

本章介绍了与渲染过程相关的一些性能优化内容,首先按照浏览器对页面的渲染过程,将其划分为五个阶段:JavaScript执行、样式计算、页面布局、绘制和合成,然后依次针对每个阶段的处理特点给出了若干优化思路。

需要重点说明的是,这里所列举的优化建议,对整个渲染过程的优化来说是有限的,随着前端技术的迭代、业务复杂度的加深,我们所要面对的性能问题是很难罗列穷尽的。

在面对更复杂的性能问题场景时,我们应当学会熟练使用浏览器的开发者工具,去分析出可能存在的性能瓶颈并定位到问题元素的位置,然后采取这里所介绍的思考方式,制定出合理的优化方案进行性能改进。应当做到所有性能优化都要量化可控,避免盲目地为了优化而优化,否则很容易画蛇添足。

猜你喜欢

转载自blog.csdn.net/u012961419/article/details/124817000
今日推荐