視差'

Paul Lewis

簡介

視差網站最近相當流行,請看看以下幾個例子:

如果您不熟悉這類網站,請注意捲動時網頁的視覺結構會有所變動。通常,頁面中的元素會根據頁面捲動位置,以相同比例縮放、旋轉或移動。

示範視差錯覺頁面
我們的示範頁面已完成,並加上視差效果

無論您是否喜歡視差動畫網站,您可以肯定的是,這類網站的效能會很差。這是因為瀏覽器通常會針對捲動時新內容出現在螢幕頂端或底端的情況進行最佳化 (取決於捲動方向),而且一般來說,瀏覽器在捲動期間視覺變化極少時運作效果最佳。但在視差網站中,這種情況很少發生,因為網頁上經常會出現大型視覺元素變更,導致瀏覽器需要重新繪製整個網頁。

以下是一般視差網站的概述:

  • 背景元素會在您上下捲動時變更位置、旋轉和縮放。
  • 頁面內容 (例如文字或較小的圖片),以典型的由上而下方式捲動。

我們先前介紹了捲動效能,以及如何改善應用程式的回應速度。本文將在此基礎上進一步說明,因此如果您尚未閱讀先前的文章,不妨參考。

因此問題是,如果您要建構視差捲動網站,是否會被迫採用耗費資源的重新繪製作業,或是有其他方法可用來盡可能提高效能?我們來看看有哪些選項。

方法 1:使用 DOM 元素和絕對位置

這似乎是大多數人採用的預設做法。頁面中含有多個元素,每次觸發捲動事件時,系統都會進行多項視覺更新來轉換這些元素。

如果您在影格模式下啟動 DevTools 時間軸,並開始捲動畫面,就會發現有耗用大量資源的全螢幕繪圖作業,而且如果您捲動畫面次數很多,可能會在單一影格中看到多個捲動事件,每個事件都會觸發版面配置作業。

未使用去抖動捲動事件的 Chrome 開發人員工具。
開發人員工具在單一畫面中顯示大型繪圖和多個事件觸發的版面配置。

請務必記住,要達到 60fps (符合一般螢幕的 60Hz 刷新率),我們只有 16 毫秒的時間來完成所有作業。在這個第一個版本中,我們會在每次收到捲動事件時執行視覺更新,但如同我們在先前有關使用 requestAnimationFrame 的簡潔、精簡動畫捲動效能的文章中所討論的,這與瀏覽器的更新時間表不一致,因此我們要麼錯過了影格,要麼在每個影格中執行太多工作。這很容易導致網站出現卡頓和不自然的情況,讓使用者感到失望,也讓貓咪不開心。

讓我們將更新程式碼從捲動事件移至 requestAnimationFrame 回呼,並在捲動事件的回呼中擷取捲動值。

如果重複進行捲動測試,可能會發現畫面捲動速度稍微提升,但幅度不大。原因是我們透過捲動觸發的版面配置作業並沒有那麼耗費資源,但在其他用途中可能會耗費資源。現在,我們至少只會在每個影格中執行一個版面配置作業。

使用去抖動捲動事件的 Chrome 開發人員工具。
開發人員工具在單一畫面中顯示大型繪圖和多個事件觸發的版面配置。

我們現在可以處理每個影格中一或一百個捲動事件,但重要的是,我們只會在 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,應該可以根據所需支援功能,輕鬆切換和變更轉譯器。

結論

我們評估了幾種處理視差網站的方法,從絕對定位元素到使用固定位置的畫布。當然,您採用的實作方式取決於您要達成的目標和所採用的具體設計,但瞭解自己有其他選擇總是好事。

無論採用哪種方法,請不要猜測,而是進行測試