パララクシン

はじめに

最近注目を集めているパララックス サイトに注目してください。

見慣れないとしたら、スクロールするとページの視覚的構造が変わるサイトです。通常、ページスケール内の要素は、ページのスクロール位置に比例して回転または移動します。

デモのパララックス ページ
視差効果を備えたデモページ

サイトのパララックス表示が好ましいかどうかも重要ですが、自信を持って言えるのは、サイトのパフォーマンスのブラックホールであるということです。ブラウザは、スクロールすると(スクロール方向に応じて)画面の上部または下部に新しいコンテンツが表示される場合に最適化される傾向があり、一般的に、スクロール中の視覚的変化がほとんどない場合にブラウザが最適に動作します。パララックス サイトでは、ページ全体の大きな視覚要素が何度も変更され、ブラウザがページ全体の再描画を行うため、めったに発生しません。

パララックス サイトは次のように一般化するのが妥当です。

  • 上下にスクロールすると、位置、回転、拡大縮小が変化する背景要素です。
  • 通常は上から下にスクロールするテキストや小さい画像などのページ コンテンツ。

以前、スクロールのパフォーマンスと、アプリの応答性を向上させる方法について説明しました。この記事は、その基盤の上に構築されているので、まだ読んでいない場合は読む価値があるかもしれません。

問題は、パララックス スクロール サイトを構築しているかどうか、コストの高い再ペイントに縛られているか、あるいはパフォーマンスを最大化するために取ることができる別のアプローチがあるかということです。選択肢を見ていきましょう。

オプション 1: DOM 要素と絶対位置を使用する

ほとんどの人が、この方法をデフォルトとして利用しているようです。ページ内には多数の要素があり、スクロール イベントが発生するたびに、要素を変換するために一連の視覚的な更新が行われます。

DevTools のタイムラインをフレームモードで開始し、スクロールすると、負荷の高い全画面ペイント操作があることがわかります。また、何度もスクロールすると、1 つのフレーム内に複数のスクロール イベントが表示され、それぞれがレイアウト処理をトリガーします。

デバウンス スクロール イベントのない Chrome DevTools。
DevTools で、大きなペイントと複数のイベント トリガー レイアウトを単一フレームに表示

注意すべき重要な点は、60 fps(一般的なモニターのリフレッシュ レートの 60 Hz と同じ)に達するまでには、16 ミリ秒強ですべての処理を完了できることです。この最初のバージョンでは、スクロール イベントを取得するたびにビジュアル アップデートを行っていますが、以前の記事でrequestAnimationFrame による無駄のない、意地悪なアニメーションスクロール パフォーマンスで説明したように、これはブラウザの更新スケジュールとは一致しないため、フレームを見逃したり、各フレーム内で処理を多くしすぎたりする可能性があります。そうすると、サイトに不自然で不自然な印象を与え、ユーザーが不満を抱き、子猫が不満を抱くことになりかねません。

更新コードをスクロール イベントから requestAnimationFrame コールバックに移動し、スクロール イベントのコールバックでスクロール値を取得しましょう。

スクロール テストを繰り返すと、大きくは改善しないものの、わずかに改善が見られる可能性があります。その理由は、スクロールによってトリガーするレイアウト操作はそれほど高コストではないものの、他のユースケースでは高コストになる可能性があるからです。少なくとも、各フレームで 1 つのレイアウト操作のみを実行します。

デバウンス スクロール イベントを備えた Chrome DevTools。
DevTools で、大きなペイントと複数のイベント トリガー レイアウトを単一フレームに表示

フレームごとに 100 または 100 のスクロール イベントを処理できるようになりましたが、重要なのは最新の値のみを保存し、requestAnimationFrame コールバックが実行され、ビジュアル アップデートを実行するたびに使用する方法です。ポイントは、スクロール イベントを受け取るたびにビジュアル更新を強制的に適用しようとしていたところから、ブラウザに対して適切なウィンドウを表示するようリクエストできるようになったことです。甘くない?

requestAnimationFrame を使用するかどうかにかかわらず、このアプローチの主な問題は、基本的にページ全体に 1 つのレイヤがあることです。これらの視覚要素を動かすと、大規模な(そして費用のかかる)再ペイントが必要になります。通常、ペイントはブロッキング処理です(ただし、これは変化しています)。つまり、ブラウザは他の処理を行えず、フレームのバジェットである 16 ミリ秒を超えてしまうこともよくあり、ジャンクの状態が残ります。

オプション 2: DOM 要素と 3D 変換を使用する

絶対位置を使用する代わりに、要素に 3D 変換を適用するという方法もあります。この状況では、3D 変換が適用された要素に要素ごとに新しいレイヤが割り当てられます。WebKit ブラウザでは、多くの場合、ハードウェア コンポジタへの切り替えも行われます。一方、オプション 1 では、ページ用に 1 つの大きなレイヤがあり、何かが変更されたときに再ペイントする必要がありました。また、ペイントと合成はすべて CPU によって処理されていました。

つまり、このオプションでは状況が異なり、3D 変換を適用する要素に対してレイヤが 1 つ存在する可能性があります。この時点で要素に対する変換を増やすだけであれば、レイヤを再描画する必要はなく、GPU が要素の移動と最終的なページの合成を処理できます。

多くの場合、-webkit-transform: translateZ(0); をハッキングするだけで、パフォーマンスが飛躍的に向上しますが、現在はうまく機能していますが、次のような問題があります。

  1. クロスブラウザとの互換性がありません。
  2. 変換された要素ごとに新しいレイヤが作成され、ブラウザの操作が強制されます。多数のレイヤはパフォーマンスのボトルネックを引き起こす可能性があるため、慎重に使用してください。
  3. 一部の WebKit ポートでは無効になっています(下から 4 項目目)。

3D 変換ルートを進む場合は注意が必要です。これは問題の一時的な解決策です。2D 変換でも 3D と同様のレンダリング特性が得られるのが理想的です。ブラウザは驚異的なスピードで進歩しているため、その前にこれが実現できると期待しています。

最後に、可能な限りペイントを避け、ページ内で既存の要素を移動するようにしましょう。たとえば、パララックス サイトでは、高さを固定した div を使用し、背景の位置を変更して効果を加えることが一般的なアプローチです。そのため、パスごとに要素を再ペイントする必要があり、パフォーマンスの面でコストがかかる可能性があります。可能であれば、要素を作成し(必要に応じて overflow: hidden で div でラップ)、単純に翻訳します。

方法 3: 固定位置のキャンバスまたは WebGL を使用する

最後に、ページの背面に固定位置のキャンバスを使用して、変換後の画像を描画します。一見すると最もパフォーマンスの高いソリューションに見えないかもしれませんが、実際にはこのアプローチにはいくつかの利点があります。

  • キャンバスが 1 つの要素だけになるため、コンポジタの作業はあまり必要なくなりました。
  • ここでは、1 つのハードウェア アクセラレーション ビットマップを効果的に処理しています。
  • Canvas2D API は、実行しようとしている種類の変換に最適であり、開発とメンテナンスが管理しやすくなります。

キャンバス要素を使用すると新しいレイヤが得られますが、1 つのレイヤにすぎないのに対し、オプション 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 の中で最も直接的なルートであるため、特にサイトの効果が複雑な場合は、60 fps を実現する最も有力な候補になるということです。

すぐに反応するのは、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 を使用した場合も、必要なサポートに応じて、レンダラを非常に簡単に入れ替えることができます。

おわりに

Google は、絶対位置の要素から固定位置のキャンバスの使用まで、視差サイトを扱ういくつかのアプローチを評価してきました。当然のことながら、どのような実装を行うかは、目指すものと取り組んでいる具体的な設計によって異なりますが、常に選択肢があることを知っておくことも大切です。

どのようなアプローチを取る場合でも、推測ではなくテストしてください。