避免不必要的绘制

Paul Lewis

简介

绘制网站或应用元素的开销可能会非常大,并且可能会对运行时性能产生负面影响。在本文中,我们将快速了解一下哪些内容会在浏览器中触发绘制,以及如何避免不必要的绘制。

绘画:超速导览

浏览器必须执行的一项主要任务是将 DOM 和 CSS 转换为屏幕上的像素,这个过程需要一个相当复杂的过程。它首先读取标记,然后据此创建 DOM 树。它会使用 CSS 执行类似操作,然后据此创建 CSSOM。然后 DOM 和 CSSOM 合并,最终,我们达成了一个可以开始绘制某些像素的结构。

绘画过程本身很有趣。在 Chrome 中,由一些名为 Skia 的软件对 DOM 和 CSS 树进行光栅化。如果您使用过 canvas 元素,那么 Skia 的 API 对您来说会非常熟悉;其中有许多 moveTolineTo 样式的函数,以及许多更高级的函数。从本质上讲,所有需要绘制的元素都被提炼为可执行的 Skia 调用集合,并且输出的是一堆位图。这些位图会上传到 GPU,然后 GPU 会帮忙将它们合成为屏幕上呈现的最终图片。

Dom 到像素

需要注意的是,您应用于元素的样式会直接影响 Skia 的工作负载。如果您使用算法繁杂的样式,Skia 就需要做更多的工作。Colt McAnlis 写了一篇关于 CSS 如何影响网页渲染权重的文章,所以您应该阅读这篇报道,了解详情。

话虽如此,绘制工作的执行需要时间,如果不减少,将会超过帧预算约 16 毫秒。用户会注意到我们遗漏了帧,并将其视为卡顿,这最终会损害应用的用户体验。我们真的不希望发生这种情况,所以让我们来看看哪些类型的原因导致必须进行绘制工作,以及我们可以采取哪些措施。

滚动

每当您在浏览器中向上或向下滚动时,都需要重新绘制内容,才能使内容显示在屏幕上。好的,这只是一小块区域,但即使这样,需要绘制的元素也可以应用复杂的样式。因此,绘制区域一小段区域,并不一定很快就会完成。

要查看正在重新绘制的区域,可以使用 Chrome 开发者工具中的“Show Paint Rectangles”(显示绘制矩形)功能(只需点击右下角的小齿轮即可)。然后,在开发者工具打开的情况下,您只需与您的页面交互,就会看到闪烁的矩形显示 Chrome 绘制您页面某一部分的位置和时间。

在 Chrome 开发者工具中显示绘制矩形
在 Chrome 开发者工具中显示绘制矩形

滚动性能对您网站的成功至关重要;如果您的网站或应用无法正常滚动,或者用户不喜欢,用户会注意到这一点。因此,我们的目标之一是在滚动时让绘制操作保持浅色,这样用户就不会出现卡顿。

我之前写过一篇关于滚动性能的文章,如果您想详细了解滚动性能的具体细节,可以读一读。

互动

交互是绘制工作的另一个原因:悬停、点击、触摸、拖动。每当用户执行上述任一互动(例如悬停)时,Chrome 都必须重新绘制受影响的元素。而且,与滚动一样,如果需要大量复杂的绘制,帧速率将下降。

每个人都希望获得美观、流畅的互动动画,所以同样,我们需要了解动画中发生变化的样式是否花费了太多时间。

组合

使用昂贵颜料的演示
使用昂贵绘制的演示

如果在滚动鼠标的同时移动鼠标,会出现什么情况?我有可能在滚动浏览某个元素时,无意中与元素“交互”,导致绘制开销过大。反过来,这又可能会使我花费约 16.7 毫秒的帧预算(也就是我们需要保持 16.7 毫秒以内达到每秒 60 帧所需的时间)。为了准确说明我的意思,我制作了一个演示版。希望在您滚动和移动鼠标时,您会看到悬停效果起作用,不过,我们先来了解一下 Chrome 开发者工具的作用:

Chrome 的开发者工具显示开销高的帧
Chrome 的开发者工具显示昂贵的帧

在上图中,当我将鼠标悬停在其中一个块上时,您可以看到开发者工具正在注册绘制工作。我在演示中使用了一些超重的风格来强调这一点,因此我一直在加倍提升帧数预算,而且偶尔也会用尽我的帧预算。我最后想做的就是不必要地执行这种绘制工作,特别是在滚动期间,当有其他工作要完成时,尤其如此!

那么,我们如何防止这种情况发生呢?恰巧,修复操作实施起来非常简单。此处的技巧是附加一个 scroll 处理程序,用于停用悬停效果,并设置计时器,以再次启用这些效果。这意味着我们可以保证,在您滚动页面时,无需执行任何开销大的互动绘制。当您停止操作足够长的时间后,我们认为可以放心地将其重新开启。

代码如下:

// Used to track the enabling of hover effects
var enableTimer = 0;

/*
 * Listen for a scroll and use that to remove
 * the possibility of hover effects
 */
window.addEventListener('scroll', function() {
  clearTimeout(enableTimer);
  removeHoverClass();

  // enable after 1 second, choose your own value here!
  enableTimer = setTimeout(addHoverClass, 1000);
}, false);

/**
 * Removes the hover class from the body. Hover styles
 * are reliant on this class being present
 */
function removeHoverClass() {
  document.body.classList.remove('hover');
}

/**
 * Adds the hover class to the body. Hover styles
 * are reliant on this class being present
 */
function addHoverClass() {
  document.body.classList.add('hover');
}

如您所见,我们在 body 上使用一个类来跟踪是否允许“允许”悬停效果,而底层样式依赖于此类的存在:

/* Expect the hover class to be on the body
 before doing any hover effects */
.hover .block:hover {
 …
}

这就是全部内容!

总结

渲染性能对于享受应用的用户至关重要,您应始终力求将绘制工作负载控制在 16 毫秒以内。为了做到这一点,您应该在整个开发过程中使用开发者工具进行集成,以发现并解决出现的瓶颈问题。

无意间交互(尤其是在大量绘制元素上)可能代价很高,并且会降低渲染性能。如您所见,我们可以使用一小段代码来解决此问题。

查看您的网站和应用,稍加防喷涂即可能达到相同的效果吗?