避免不必要的绘制

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 开发者工具中的“显示绘制矩形”功能(只需点击右下角的小齿轮即可)。然后,打开 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 进行集成,以便及时发现和解决瓶颈问题。

意外交互,特别是在大量绘制的元素上,成本很高,并且会降低渲染性能。如您所见,我们可以使用一小段代码来解决此问题。

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