0%

性能优化 - 回流与重绘的调试与优化

回流与重绘已经是个老生常谈的问题啦, 但谈起性能优化上它又占有一席之地。今天来谈一谈什么是回流与重绘, 我们该如何去测试并针对性的去优化。

概述

HTML中, 每一个标签都有自己的盒子模型. 浏览器在解析HTML的过程中会通过一个叫frame的对象对盒子进行操作. 它主要有三个动作:

  1. 构建 frame, 以建立DOM树.
  2. 回流(reflow), 布局引擎为frame计算图形, 以确定对象位置, 浏览器根据各种样式来计算结果放在它该出现的位置.
  3. 重绘(repaint), 当计算好盒子模型的位置, 大小以及其他属性后, 浏览器就根据各自的特性进行绘制一遍, 显现出来给用户看.

代价

**回流(reflow)**就是布局引擎为frame 计算图形的过程。但是这里需要我们注意的是, 回流并不仅仅只是在渲染页面的时候会触发, 实际上当我们动态修改某个 css 属性或者操作 DOM 时, 都有可能会触发回流和重绘。

也就是说, 我们操作 DOM 实际上是有代价的。因为 DOM 的改变会导致浏览器重新计算的它的位置和渲染的样式。

假设有这么一个场景:用户打开了一个很长的页面,就比如 ecma-262 规格文档,同时右键点击了翻译,这时下拉滚动条页面, 在一些配置比较差的电脑可能会导致网页卡死。

我们知道浏览器的翻译功能是将当前页面的文字翻译至另一种语言,这其中需要替换 DOM 元素,同时用户打开的这个文档内容没有按章分隔,因此用户每滚动一次就需要重新替换内容、计算元素位置,频繁地触发回流的后果将导致网页占有性能徒然增大,配置较差的电脑顶不住这么大的压力,从而会引发页面卡死。这就是回流的代价。

触发

回流是必不可免的,甚至很多时候我们不需要过分的关注回流的发生。但过于频繁的触发的话还是可能成为渲染性能瓶颈,因此我们需要知道回流是如何触发的。

YaHoo!性能小组总结了一些导致回流发生的一些因素:

  1. 调整窗口大小
  2. 改变字体
  3. 增加或者移除样式表
  4. 内容变化,比如用户在 input 框中输入文字, CSS3 动画等
  5. 激活 CSS 伪类,比如 :hover
  6. 操作class属性
  7. 脚本操作DOM
  8. 计算offsetWidthoffsetHeight属性
  9. 设置 style 属性的值

而重绘则是视觉效果变化引起的重新绘制。比如 color 或者 background 发生了变化,那就该给触发重绘的元素化化妆,化成它想要的样子。

回流与重绘两者之间的联系在于: 触发回流一定会触发重绘, 而触发重绘却不一定会触发回流

我们可以把页面理解为一个黑板,黑板上有一朵画好的小花。现在我们要把这朵从左边(left)移到了右边(right),那我们是不是要先确定好右边的具体位置,画好形状(回流),再画上它原有的颜色(重绘)。

但如果我们仅仅是想换给花朵换一个颜色,那么只需擦掉花朵上的颜色,再重新涂上自己期望的颜色(重绘)就可以了。

调试

我们光是用联想当然是不行的呀,我们需要一个工具来辅助查看页面渲染的情况。chrome devtools 就可以做到这件事。接下来我们先找个页面测试一下, 看看该如何去调试回流与重绘。

首先打开天猫官网, 然后 chrome devtools 上打开 Rendering 面板,打开方式有两种:

  1. 点击 More tools 下的 Rendering 面板
  2. Command+ Shift+ P(MacControl+ Shift+ P(Windows,Linux,Chrome OS) 打开命令菜单,输入rendering 并回车。

接着我们可以在面板上看到有以下几个选项:

  1. Paint flashing: 高亮(绿色)显示重绘的页面区域
  2. Layer boders: 显示图层边框(橙色/实时)和图块(青色。我们知道页面是由多个”图层”组合的, 最终显示给用户看的就是多图层叠加在一起的效果,cssz-index机制就可以很好的体现着一点。
  3. FPS meter: 显示绘制每秒帧数,帧速率分布和GPU内存。或许玩游戏的朋友对这些参数会比较熟悉,该选项更多的是用来分析页面交互和动画性能.
  4. Scrolling performance issues: 突出显示可以减慢滚动的元素(蓝绿色),包括touch&whell事件处理程序和其他主线程滚动情况。主要用来分析滚动性能问题。

勾上 Paint flashingLayer bodersScrolling performance issues 等选项后刷新天猫首页,开始分析页面的渲染情况。

首先能看到天猫的 Logo 是一个 GIF 的动态图,浏览器需要绘制 gif 的每一帧展示给用户看,因此 logo 区域上不断闪烁的绿色高亮表示浏览器正在勤恳的绘制的图形。轮播图组件也同理, 内容的不断变化触发着回流与重绘。

紧接着我们往下滚, 发现左下角的工具栏会随着滚动而发生回流。很显然, 这是使用了 fixed 定位。fixed 是相对浏览器窗口进行定位的,我们每滚动一点,元素会随着滚动的变化而重新计算位置,进而导致触发回流。值得一提的是,fixed 定位只会对自身元素进行渲染, 而不会影响身边的DOM。

接着我们继续往下分析。然后发现商品模块似乎不符合预期呀? 怎么停止滚动了,还是会不断的触发重绘? 打开审查元素一看,发现模块中有一个 gif 的背景图片。将属性关闭,发现几个模块的重绘都消失了,果然是这东西作祟呀。

紧接着来检查一下这 gif 是有什么特殊的作用,打开 background 属性的 url 发现是一个加载的 loading。父元素加一个 loading 动图, 在子元素还没加载出来时显示 loading,当加载完毕后由于层级的关系自然就覆盖了上去, 用户自然就看不到了。

但这样的效果需要额外的重绘代价,当该商品模块多到一定地步后,叠加起来的渲染可能还是会造成一定的浪费。但如果你说这是 bug 吗,其实也不算是。只能说是一种比较偷懒的方案,笔者并不推荐使用这样的方案。

如何优化

好啦,到这里为止我们知道了回流重绘的触发与调试,那么就可以在编写代码时合理的去避开回流的影响来减少页面的开销。

就比如 display:none 这个属性,该属性的作用就如同它名字一样直观,就是用来控制显示的状态嘛… 因此很多人喜欢拿它来做隐藏某个元素。而却忽略了(或者说不知道)它本身所带的回流性能开销(因为会影响节点的位置从而触发回流和重绘).

有一些前辈帮我们总结了避免回流的经验以供我们借鉴:

  1. 如果想设定元素的样式,通过改变元素的 class 类名 (尽可能在 DOM 树的最里层)
  2. 避免设置多项内联样式
  3. 应用元素的动画,使用 position 属性的 fixed 值或 absolute 值(如前文示例所提)
  4. 权衡平滑和速度
  5. 避免使用 table 布局
  6. 避免使用 CSS 的 JavaScript 表达式 (仅 IE 浏览器)

csstriggers 可以查看 css 属性在不同引擎下的渲染影响。

除此之外, 使用 JavaScript 动态插入多个节点时, 可以使用DocumentFragment. 创建后一次插入. 就能避免多次的渲染性能.

总结

最后总结一下所学的概念:

  1. 回流(reflow), 就是布局引擎为 frame 计算图形, 确定节点位置的一个动作。其中触发回流的原因主要是 DOM 节点大小或位置的改变才会触发回流。
  2. 重绘则是表面的视觉效果的改变从而引发重绘。
  3. 其中触发回流就必定会触发重绘, 而触发重绘不一定会触发回流。
  4. 可以在编写代码时根据触发回流重绘的特点有意的控制代码的编写。
  5. 可以通过 chrome devtools 的 rendering 面板进行渲染性能分析
「请笔者喝杯奶茶鼓励一下」

欢迎关注我的其它发布渠道