视差

Paul Lewis

简介

视差网站最近很流行,不妨看看这些:

如果您不熟悉这类网站,请注意,在这些网站上,页面的视觉结构会随着滚动而发生变化。通常,页面中的元素会根据页面上的滚动位置按比例缩放、旋转或移动。

一个演示版视差页面
带有视差效果的演示页面

您是否喜欢采用视差效果的网站是一回事,但可以非常肯定地说,它们是性能的“黑洞”。原因在于,浏览器通常针对在滚动时在屏幕顶部或底部显示新内容的情况进行优化(具体取决于滚动方向),一般来说,当滚动期间视觉上发生的变化很少时,浏览器的效果最好。对于全局视差网站,这种情况很少发生,因为很多时候,整个页面上的大型视觉元素都会发生变化,导致浏览器重新绘制整个页面。

我们可以将全局视差网站概括为:

  • 背景元素会随着您上下滚动而更改其位置、旋转和缩放。
  • 页面内容,例如文本或较小的图片,以典型的从上到下方式滚动。

我们之前介绍了滚动性能以及可以用来提高应用响应速度的方法,本文在此基础上进行了深入探讨,因此如果您尚未阅读之前的文章,不妨先阅读一下。

问题在于,如果您要构建全局视差滚动网站,是否必须使用昂贵的重绘,还是可以采用其他方法来最大限度地提高性能?我们来看看有哪些选项。

方法 1:使用 DOM 元素和绝对位置

这似乎是大多数人采用的默认方法。页面中包含许多元素,每当触发滚动事件时,系统都会执行一系列视觉更新来转换这些元素。

如果您在帧模式下启动 DevTools 时间轴并滚动浏览,会发现存在耗时的全屏绘制操作;如果您滚动很多次,则可能会在单个帧内看到多个滚动事件,每个事件都会触发布局工作。

未启用去抖滚动事件的 Chrome DevTools。
DevTools 在单个帧中显示大量绘制操作和多个事件触发的布局。

请务必注意,为了达到 60fps(与典型的 60Hz 显示器刷新率相匹配),我们只有 16 毫秒的时间来完成所有工作。在此第一个版本中,我们会在每次收到滚动事件时执行视觉更新,但正如我们在使用 requestAnimationFrame 实现更轻量、更高效的动画滚动性能一文中所讨论的那样,这与浏览器的更新时间表不一致,因此我们会错过帧,或者在每个帧中执行太多工作。这很容易导致网站看起来不自然、不流畅,进而让用户感到失望,让猫咪不开心。

我们将更新代码从滚动事件移至 requestAnimationFrame 回调,并仅在滚动事件的回调中捕获滚动值。

如果您重复滚动测试,可能会发现略有改进,但幅度不大。原因在于,我们通过滚动触发的布局操作并不会花费太多资源,但在其他用例中,它确实可能会花费很多资源。现在,我们至少只会在每一帧中执行 1 项布局操作。

带有去抖滚动事件的 Chrome DevTools。
DevTools 在单个帧中显示大量绘制操作和多个事件触发的布局。

现在,我们每帧可以处理一个或 100 个滚动事件,但重要的是,我们只存储最新值,以便在每次运行 requestAnimationFrame 回调并执行视觉更新时使用。重点是,您从每次收到滚动事件时尝试强制进行视觉更新,转变为请求浏览器为您提供进行这些更新的适当窗口。您真是太客气了。

无论是否使用 requestAnimationFrame,这种方法的主要问题在于,我们实际上只有一个用于整个页面的层,并且通过移动这些视觉元素,我们需要进行大量(且昂贵)的重绘。通常,绘制是阻塞操作(不过这种情况正在发生变化),这意味着浏览器无法执行任何其他工作,并且帧的预算(16 毫秒)通常会被严重超出,导致画面仍然不流畅。

方法 2:使用 DOM 元素和 3D 转换

除了使用绝对位置之外,我们还可以采用另一种方法,即对元素应用 3D 转换。在这种情况下,我们发现,应用了 3D 转换的元素会为每个元素分配一个新层,在 WebKit 浏览器中,这通常还会导致切换到硬件合成器。相比之下,在选项 1 中,我们为网页创建了一个大型层,当任何内容发生变化时,都需要对其进行重绘,并且所有绘制和合成都由 CPU 处理。

也就是说,使用选项时,情况有所不同:我们可能会为应用 3D 转换的任何元素创建一个图层。如果我们接下来要做的只是对元素进行更多转换,则无需重新绘制图层,GPU 可以处理元素的移动和最终页面的合成。

很多时候,人们只需使用 -webkit-transform: translateZ(0); 黑客攻击方法,就能神奇地提升性能。虽然目前这种方法可行,但存在一些问题:

  1. 它不支持跨浏览器。
  2. 它会为每个经过转换的元素创建一个新层,从而迫使浏览器执行此操作。过多的图层可能会带来其他性能瓶颈,因此请慎用!
  3. 已为某些 WebKit 端口停用(最下方的第四个项目)。

如果您选择 3D 翻译,请谨慎,因为这只是暂时性解决方案!理想情况下,我们会看到 2D 转换与 3D 转换具有类似的渲染特性。浏览器的进步速度非常快,因此我们希望在那之前就能看到这种情况。

最后,您应尽可能避免绘制,只需在页面中移动现有元素即可。例如,在视差网站中,使用固定高度的 div 并更改其背景位置来实现此效果是一种典型方法。遗憾的是,这意味着元素需要在每次传递时重新绘制,这可能会影响性能。相反,您应尽可能创建元素(如有必要,请使用 overflow: hidden 将其封装在 div 中),然后直接对其进行转换。

选项 3:使用固定位置的画布或 WebGL

我们要考虑的最后一个选项是在页面后面使用固定位置的画布,我们将在其中绘制经过转换的图片。乍一看,这可能不是性能最高的解决方案,但这种方法实际上有几个优势:

  • 由于只有一个元素(画布),因此我们不再需要进行太多合成器工作。
  • 我们有效地处理了单个硬件加速位图。
  • Canvas2D API 非常适合我们要执行的转换,这意味着开发和维护更易于管理。

使用画布元素会为我们提供一个新图层,但它只是一个图层,而在选项 2 中,我们实际上为应用了 3D 转换的每个元素都提供了一个新图层,因此我们需要完成更多工作来将所有这些图层合并在一起。鉴于转换的跨浏览器实现方式不同,这也是目前最兼容的解决方案。


/**
 * Updates and draws in the underlying visual elements to the canvas.
 */
function updateElements () {

  var relativeY = lastScrollY / h;

  // Fill the canvas up
  context.fillStyle = "#1e2124";
  context.fillRect(0, 0, canvas.width, canvas.height);

  // Draw the background
  context.drawImage(bg, 0, pos(0, -3600, relativeY, 0));

  // Draw each of the blobs in turn
  context.drawImage(blob1, 484, pos(254, -4400, relativeY, 0));
  context.drawImage(blob2, 84, pos(954, -5400, relativeY, 0));
  context.drawImage(blob3, 584, pos(1054, -3900, relativeY, 0));
  context.drawImage(blob4, 44, pos(1400, -6900, relativeY, 0));
  context.drawImage(blob5, -40, pos(1730, -5900, relativeY, 0));
  context.drawImage(blob6, 325, pos(2860, -7900, relativeY, 0));
  context.drawImage(blob7, 725, pos(2550, -4900, relativeY, 0));
  context.drawImage(blob8, 570, pos(2300, -3700, relativeY, 0));
  context.drawImage(blob9, 640, pos(3700, -9000, relativeY, 0));

  // Allow another rAF call to be scheduled
  ticking = false;
}

/**
 * Calculates a relative disposition given the page's scroll
 * range normalized from 0 to 1
 * @param {number} base The starting value.
 * @param {number} range The amount of pixels it can move.
 * @param {number} relY The normalized scroll value.
 * @param {number} offset A base normalized value from which to start the scroll behavior.
 * @returns {number} The updated position value.
 */
function pos(base, range, relY, offset) {
  return base + limit(0, 1, relY - offset) * range;
}

/**
 * Clamps a number to a range.
 * @param {number} min The minimum value.
 * @param {number} max The maximum value.
 * @param {number} value The value to limit.
 * @returns {number} The clamped value.
 */
function limit(min, max, value) {
  return Math.max(min, Math.min(max, value));
}

在处理大型图片(或可轻松写入画布中的其他元素)时,这种方法确实很有用,当然,处理大量文本会更具挑战性,但根据您的网站,这种方法可能是最合适的解决方案。如果您必须处理画布中的文本,则必须使用 fillText API 方法,但代价是会降低可访问性(您只是将文本光栅化为位图!),而且您现在还必须处理换行和一大堆其他问题。如果可以避免,最好不要这样做,您可能更适合使用上述转换方法。

鉴于我们要尽可能实现这一点,因此没有理由假定应该在画布元素内完成视差效果处理。如果浏览器支持,我们可以使用 WebGL。关键在于,WebGL 是所有 API 中到显卡的最直接路线,因此最有可能实现 60fps,尤其是在网站的效果较为复杂的情况下。

您可能会立即认为 WebGL 过于复杂,或者支持范围不广,但如果您使用 Three.js 之类的库,则可以随时改用画布元素,并且您的代码会以一致且易于理解的方式进行抽象化处理。我们只需使用 Modernizr 检查是否有适当的 API 支持即可:

// check for WebGL support, otherwise switch to canvas
if (Modernizr.webgl) {
  renderer = new THREE.WebGLRenderer();
} else if (Modernizr.canvas) {
  renderer = new THREE.CanvasRenderer();
}

关于此方法的最后一点是,如果您不想在网页中添加额外的元素,则可以随时在 Firefox 和基于 WebKit 的浏览器中将画布用作背景元素。显然,这种情况并不普遍,因此您应一如既往地谨慎对待。

一切由您选择

开发者默认使用绝对定位元素而非任何其他选项的主要原因可能只是支持范围广泛。这在某种程度上只是一种幻想,因为所定位的旧版浏览器可能会提供非常糟糕的呈现体验。即使在当今的现代浏览器中,使用绝对定位的元素也并不一定能带来良好的性能!

借助转换(当然是 3D 转换),您可以直接使用 DOM 元素并实现稳定的帧速率。要想取得理想效果,关键是尽可能避免绘制,而只尝试移动元素。请注意,WebKit 浏览器创建层的方式不一定与其他浏览器引擎相关联,因此请务必先进行测试,然后再确定是否采用该解决方案。

如果您只针对顶级浏览器进行开发,并且能够使用画布渲染网站,那么这可能是您的最佳选择。当然,如果您要使用 Three.js,则应该能够根据您所需的支持,非常轻松地在渲染程序之间切换和更改。

总结

我们评估了几种处理视差网站的方法,从绝对定位元素到使用固定位置画布。当然,您采用的实现方式将取决于您要实现的目标以及您所采用的具体设计,但了解您有选择总是件好事。

一如既往,无论您尝试哪种方法,请不要猜测,而是进行测试