避免不必要的绘制

Paul Lewis

简介

为网站或应用绘制元素的开销可能非常高,并且可能会对运行时性能产生负面连锁影响。本文将简要介绍哪些操作可能会触发浏览器中的绘制操作,以及如何防止不必要的绘制操作。

绘画:超快速导览

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

绘制过程本身就很有趣。在 Chrome 中,DOM 和 CSS 的组合树由名为 Skia 的软件进行光栅化处理。如果您曾使用过 canvas 元素,那么 Skia 的 API 会非常熟悉;其中包含许多 moveTolineTo 风格的函数,以及一些更高级的函数。从本质上讲,所有需要绘制的元素都会提炼为一系列可执行的 Skia 调用,其输出是一堆位图。这些位图会上传到 GPU,GPU 会将它们合并在一起,以便在屏幕上显示最终图片。

将 DOM 转换为像素

需要注意的是,Skia 的工作负载会直接受到您应用于元素的样式的直接影响。如果您使用算法上较重的样式,Skia 将需要执行更多工作。Colt McAnlis 撰写了一篇介绍 CSS 如何影响网页呈现大小的文章,您应该阅读该文章,以获得更多深入见解。

尽管如此,绘制工作需要时间来执行,如果不缩短绘制时间,我们将超出大约 16 毫秒的帧预算。用户会注意到我们错过了帧,并将其视为卡顿,这最终会影响应用的用户体验。我们真的不希望这样,因此我们来看看是什么原因导致需要绘制操作,以及我们可以采取哪些措施来解决此问题。

滚动

每当您在浏览器中向上或向下滚动时,浏览器都需要先重新绘制内容,然后内容才会显示在屏幕上。一切顺利的话,这将只是一个小区域,但即使如此,需要绘制的元素也可能会应用复杂的样式。因此,即使您要涂刷的面积很小,也并不意味着涂刷过程会很快。

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

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

滚动性能对网站的成功至关重要;如果您的网站或应用滚动不顺畅,用户会非常注意这一点,并且不喜欢这种情况。因此,我们有充分的理由在滚动期间尽量减少绘制工作,以免用户看到卡顿。

我之前曾撰写了一篇有关滚动性能的文章,如果您想详细了解滚动性能的具体信息,请参阅该文章。

互动次数

互动是导致绘制工作的另一个原因:悬停、点击、轻触、拖动。每当用户执行其中一种互动(例如悬停)时,Chrome 都必须重新绘制受影响的元素。与滚动类似,如果需要进行大量复杂的绘制,您会看到帧速率下降。

每个人都希望看到流畅的互动动画,因此我们需要再次看看动画中更改的样式是否花费了太多时间。

不幸的组合

使用昂贵的油漆的演示
使用昂贵涂料的演示

如果我在滚动时同时移动鼠标,会怎么样?在滚动浏览元素时,我完全有可能无意中与元素“互动”,从而触发昂贵的绘制操作。这反过来可能会使我的帧预算超出约 16.7 毫秒(我们需要将时间控制在该时间以内,才能达到每秒 60 帧)。我制作了一个演示,以便向您展示具体情况。希望您在滚动和移动鼠标时,会看到悬停效果生效,但我们还是来看看 Chrome 的开发者工具是如何解读的:

显示耗时帧的 Chrome 开发者工具
Chrome 开发者工具显示耗时帧

在上图中,您可以看到当我将鼠标悬停在其中一个代码块上时,DevTools 会注册绘制工作。为了说明这一点,我在演示中使用了一些超重样式,因此我的帧预算会达到上限,有时甚至会超出上限。我最不想做的就是不必要地执行此绘制工作,尤其是在滚动期间还有其他工作要做时!

那么,我们如何阻止这种情况的发生呢?事实证明,解决方法非常简单。这里的技巧是附加一个 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 毫秒以下。为此,您应在整个开发流程中使用 DevTools 进行集成,以便在出现瓶颈时及时发现并加以解决。

无意中的互动(尤其是在绘制密集的元素上)可能会产生巨大开销,并会降低渲染性能。如您所见,我们可以使用一小段代码来解决此问题。

看看您的网站和应用,它们是否需要一些保护措施?