視差'

Paul Lewis

簡介

看來視差網站是近期有各種憤怒的內容,請看看這些網站:

如果你不熟悉這些機制,就必須瞭解捲動頁面時,頁面視覺結構的變動。通常是頁面縮放頁面中的元素,會根據捲動位置按比例旋轉或移動。

視差示範頁面
我們的示範頁面完整呈現視差效果

無論你是否喜歡提供視差網站,只要確保網站成效良好,都很安心。原因在於,當您捲動畫面時,如果畫面的頂端或底部會顯示新內容 (視捲動方向而定),瀏覽器通常會針對新內容進行最佳化調整。一般而言,瀏覽器最好在捲動畫面時只稍微調整外觀,才能發揮最佳效能。以視差網站來說,由於整個網頁上有許多大量視覺元素都會變更,導致瀏覽器會重新繪製整個網頁,因此這種情況很少見。

將視差網站的一般化為合理的,如下所示:

  • 上下捲動時可變更其位置、旋轉和縮放的背景元素。
  • 網頁內容 (例如文字或小型圖片) 會以一般由上而下的方式捲動。

我們先前提過捲動效能,並介紹如何改善應用程式回應速度。本文是以這些基礎為基礎,如果您還沒閱讀的話,建議您閱讀這項說明。

那麼,打造視差捲動網站時,可能會發現成本高昂,還是有其他可盡量提高效能的方法?接著來看看這些選項。

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

這是大多數人採用的預設做法。該頁面會有許多元素,而每當捲動事件觸發一系列視覺更新時,即會轉換元素。

如果在影格模式中啟用開發人員工具時間軸,並在畫面四處捲動,就會發現全螢幕繪製作業耗用大量資源,而捲動大量事件時,在單一影格中可能會看到多個捲動事件,且每個事件都會觸發版面配置的工作。

不含去彈跳回事件的 Chrome 開發人員工具。
開發人員工具在單一影格中顯示大型繪製和多個事件觸發的版面配置。

切記,如果要達到 60fps (與一般的螢幕刷新率是 60 Hz 相等),我們只需超過 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。重點在於,WebGL 能以最直接的方式將所有 API 導向顯示卡,因此最適合支援 60fps,在網站效果十分複雜時更是如此。

建議您立即做出反應,也許是因為 WebGL 的技巧過失,或是對於支援並不普及,但如果您使用 Three.js 這類項目,就可以隨時改回使用 Canvas 元素,並以一致、友善的方式提取程式碼。我們只需使用 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,應該就能根據所需的支援,輕鬆在轉譯器間切換及變更。

結論

我們已經評估過幾種處理視差網站的方法,從絕對定位元素到使用固定的位置畫布。至於要做什麼,端看您想達成的目標和採用的具體設計而定,不過建議您多瞭解一些方法。

別忘了,無論您嘗試哪一種方式,都請不猜測,但試一試