视差

Paul Lewis

简介

视差网站最近风靡一时,赶快一睹为快:

如果您不熟悉这些区域,它们是网站,页面视觉结构会随着您滚动而发生变化。正常情况下,页面内的元素会按照页面上的滚动位置按比例旋转或移动。

视差演示页面
我们的演示页面具有视差效果

您是否喜欢视差网站是一回事,但您可以自信地说,这些网站就是性能的黑洞。其原因在于,浏览器往往会针对以下情况进行优化:当您滚动屏幕时,新内容会显示在屏幕顶部或底部(具体取决于您的滚动方向),一般来说,当滚动过程中的视觉变化非常小时,浏览器的效果最佳。对于视差网站,这种情况很少见,因为页面的所有大视觉元素经常发生变化,导致浏览器对整个页面进行重绘。

将视差网站这样泛化为合理的做法是合理的:

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

我们之前已经介绍过滚动性能,以及您可以通过哪些方式提高应用响应能力,因此本文以此为基础,值得一读(如果您还没有读过的话)。

因此,问题是:如果您正在构建视差滚动网站,您是否被迫进行高昂的重渲染,或者是否有其他方法可以最大限度地提升广告效果?我们来看看有哪些可选方案。

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

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

如果您在帧模式下启动开发者工具 Timeline 并滚动,您会注意到全屏绘制操作占用大量资源,如果您大量滚动,则可能会在单个帧中看到多个滚动事件,每个事件都会触发布局工作。

没有去抖动的滚动事件的 Chrome 开发者工具。
开发者工具在单个帧中显示大型绘制和多个事件触发的布局。

需要注意的一点是,要达到 60fps(与 60Hz 的典型显示器刷新率一致),我们只需 16 毫秒以上即可完成所有工作。在第一个版本中,我们会在每次收到滚动事件时执行视觉更新,但是正如我们在之前有关使用 requestAnimationFrame 实现更精简、更精细的动画滚动性能的文章中讨论的,这种更新与浏览器的更新时间表不一致,因此我们要么会错过帧,要么在每个帧中处理太多工作。这很容易给您的网站造成卡顿、不自然的感觉,进而导致用户失望和小猫不开心。

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

如果您重复滚动测试,可能会注意到细微的改进,但提升不多。其原因在于,我们通过滚动触发的布局操作并不那么耗费资源,但在其他用例中,开销却很高。现在,至少我们在每一帧中只执行一个布局操作。

显示去抖动的滚动事件的 Chrome 开发者工具。
开发者工具在单个帧中显示大型绘制和多个事件触发的布局。

现在,我们可以为每帧处理 1 个或 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 有点过度,或者 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,应该能够根据所需支持非常轻松地切换和更改渲染程序。

总结

我们已经评估了几种处理视差网站的方法,从绝对定位元素到使用固定位置的画布。当然,具体的实施方式取决于您想要实现的目标以及所使用的具体设计,但知道自己有哪些选择是很有帮助的。

一如既往,无论您采用哪种方法,先不要猜测,但要进行测试