将一个网站或是网页应用程序的组件画出来可以是相当耗费资源的,甚至有可能在程序执行时对性能产生负面的连锁效应。这篇文章很快的说明一下什幺会让浏览器产生画图(painting)的行为,并且让你知道如何预防不必要的画图行为。
画图行为
浏览器主要的工作之一,就是将你的 DOM 和 CSS 转换成萤幕上的像素(pixel),而达成这项工作需要经过相当复杂的处理。 一开始是这样的,浏览器读取标记语言(markup),然后创建 DOM 树。 接下来创建 CSSOM,做的事情和创建 DOM 树差不多。 接着将 DOM 和 CSSOM 组合在一起,最后,我们就得到一个可以来画像素的结构。
如果想要更深入的知道浏览器是怎幺运作的,这篇文章 in-depth article here on HTML5 Rocks 值得一看!
画图行为的处理本身是很有趣的。 在 Chrome 中,组合好的 DOM 和 CSS 树,会使用一个软体叫做 Skia 来点阵化。 如果你曾经玩过 canvas 组件,Skia 的 API 会让你感到非常熟悉,这个 API 有很多moveTo-
和lineTo-
形式和一堆更加进阶的函式。实际上,所有需要被画出来的组件会被提取到 Skia 可以执行的的集合当中,然后产出一堆点阵(bitmaps)。这些点阵会被上传到 GPU,GPU 再合成这些点阵成为萤幕上显示的图。
要注意的是,Skia 的工作负载跟你怎幺使用你的组件有直接相关。如果你使用了繁重的算法,Skia 会需要做更多的工作。 Colt McAnlis 写了篇文章 article on how CSS affects page render weight,可以看看。
就像先前说的,画图行为需要时间,如果我们不想办法减少处理时间,而让它执行超过我们的帧预算,约是16毫秒,用户就会发现画图会掉帧,这会让这个 APP 的用户经验非常的差,而且觉得这个东西很没用。我们绝对不想要这样,所以我们要来看看什幺事情所造成的画图行为是不必要的,然后看看我们能够怎幺办。
每次你将卷轴卷上卷下的时候,浏览器就需要重画它即将要显示在萤幕上的内容。如果重画的区域很小当然很好,但即使在这样的情况下,组件也有可能是以很复杂的方式被提交给浏览器的。所以需要画图的区域很小不代表它可以被画得很快。
你可以用 Chrome 的 DevTools 里面的"Show Paint Rectangles"功能(按一下右下角的齿轮图案)来看看那些区域被重画了。当 DevTools 开启的状态下,简单的跟网页做些互动,你就会看见闪亮的矩形出现在页面被重画的地方。
卷轴的性能跟你网站的成功与否很有关系,用户的的确确会发现你的网站或是网页应用程序卷页卷得不好,而且他们很不喜欢这样。所以我们有兴趣的是让卷动卷轴时,让画图行为保持轻量,让用户不会看到奇怪的地方。
互动(Interactions)
互动是另一个会导致画图行为的原因,比如: hover 行为、点按行为、拖拉行为。当用户执行以上其中一个行为,比如说 hover 行为好了,Chrome 就必须去重画收到影响的组件。而且就像卷轴事件,如果要执行大量又复杂的画图行为,也会导致掉帧。
每个人都想要很棒的、平滑的、互动的动画效果,所以再一次的我们需要看看这些动画而产生的改变会不会花掉太多的时间。
错误的范例
如果我同时卷动卷轴又滑动鼠标会发生什幺事?对我来说非常有可能不经意的跟某个我卷过它的组件"互动"了,然后触发了一次昂贵的画图行为。反过来说,这样会导致我用掉帧预算约16.7毫秒(需要每秒要画60帧用掉的时间)。这个错误的范例,来让你知道错误的状态。希望你在卷动卷轴跟滑动鼠标的时候会看到 hover 的影响,但先让我们来看看这在 Chrome DevTools 中所表现的样子:
你可以看上面这张图,当我 hover 过某一个区块时,DevTools 暂存了画图的工作。 我故意让它执行了一些超级繁重的处理,好强调我想表达的,所以有些时候超过了帧预算。最后我想要的是让画图行为不必要的发生,特别是执行卷动卷轴的时候!
那幺我们到底要怎幺停止这样昂贵的画图行为呢?其实很简单实作,技巧在于连接一个 scroll 的处理函式,会关闭hover的影响,然后设定一个计时器,时间到了再把 hover 打开。意思是我们保证当你在卷动卷轴时不用去执行昂贵的互动事件的重画。当你停止动作一段足够的时间后,我们再将互动事件启动。
实作这个技巧是会影响到你的应用程序的用户经验的,所以要灵活运用。只有你和你的团队有权力决定要停止多久再重新启动 hover 事件的影响。
程序代码
/ 用来追踪 hover 行为的启动情形
var enableTimer = 0;
/*
* 监听卷轴并移除可能的hover
*/
window.addEventListener('scroll', function() {
clearTimeout(enableTimer);
removeHoverClass();
// 在一秒后重新启动 hover,这里你自己决定你要的等待多久!
enableTimer = setTimeout(addHoverClass, 1000);
}, false);
/**
* 从 body 移除 hover 的 class
*/
function removeHoverClass() {
document.body.classList.remove('hover');
}
/**
* 把 hover 行为需要的 class 加回去
*/
function addHoverClass() {
document.body.classList.add('hover');
}
可以看到我们放了一个class在body上,去追踪hover现在是否是被允许的。相关的style如下:
/* 在执行任何hover事件之前要把这个class加上去 */
.hover .block:hover {
......
}
结论
浏览器的 Rendering 性能对于用户使用你的应用程序是很关键的,你应该要一直注意并确保你的画图行为的工作量小于 16ms。 而且你应该在开发阶段使用 DevTools 来帮助你确认并修复你的程序的瓶颈。
一些不经意的互动行为,特别是跟那些会产生昂贵画图行为的组件互动时,可能会非常的耗费 Redering 性能。 但也如你所见,我们可以用一小段程序码来修复这样的情形。
多看看你的网站和应用程序吧,看看是不是能为它的画图行为再多做一点保护。