パララクシン

はじめに

最近、パララックス サイトが流行しています。以下をご覧ください。

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

デモの視差ページ
パララックス エフェクトを備えたデモページ

パララックス サイトが好みかどうかは別として、パフォーマンスの面ではブラックホールであることは間違いありません。これは、ブラウザはスクロール時に新しいコンテンツが画面の上部または下部に表示される(スクロールの方向に応じて)場合を想定して最適化されているためです。一般的に、スクロール中に視覚的な変化がほとんどない場合に、ブラウザは最適に動作します。パララックス サイトでは、ページ全体の大きなビジュアル要素が変更されることが多く、ブラウザがページ全体を再描画するため、これはほとんどありません。

パララックス サイトを次のように一般化できます。

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

以前の記事では、スクロールのパフォーマンスと、アプリの応答性を改善する方法について説明しました。この記事はその基礎を踏まえたものであるため、まだご覧になっていない方は、ぜひご覧ください。

パララックス スクロール サイトを構築する場合、高価な再描画に頼らざるを得ないのか、パフォーマンスを最大化するために別の方法をとることができるのか、という問題が浮かびます。選択肢を見てみましょう。

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

これは、ほとんどのユーザーがデフォルトで採用しているアプローチです。ページには多数の要素があり、スクロール イベントが発生するたびに、それらの要素を変換するために多数の視覚的な更新が行われます。

DevTools タイムラインをフレームモードで起動してスクロールすると、高負荷の全画面ペイント オペレーションがあることがわかります。また、スクロールを繰り返すと、1 つのフレーム内に複数のスクロール イベントが表示され、それぞれがレイアウト処理をトリガーすることがあります。

デバウンスされたスクロール イベントのない Chrome DevTools。
1 つのフレームに大きなペイントと複数のイベントトリガー レイアウトを表示する DevTools。

重要なのは、60fps(一般的なモニターのリフレッシュ レートである 60 Hz に一致)を達成するには、すべての処理を 16 ミリ秒強で行う必要があることです。この最初のバージョンでは、スクロール イベントが発生するたびに視覚的な更新を行っています。しかし、requestAnimationFrame による軽量で効果的なアニメーションスクロール パフォーマンスに関する以前の記事で説明したように、これはブラウザの更新スケジュールと一致しないため、フレームを逃すか、各フレーム内で過剰な処理を行うことになります。そのため、サイトがぎくしゃくして不自然な印象を与え、ユーザーの不満を招き、猫を不幸にしてしまう可能性があります。

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

スクロール テストを繰り返すと、大幅ではないものの、若干改善される可能性があります。これは、スクロールによってトリガーされるレイアウト オペレーションはそれほどコストがかからないものの、他のユースケースでは非常にコストがかかる場合があるためです。これで、少なくとも各フレームで1 つのレイアウト オペレーションのみを実行するようになりました。

デバウンスされたスクロール イベントを含む Chrome DevTools。
1 つのフレームに大きなペイントと複数のイベントトリガー レイアウトを表示する DevTools。

これで、フレームごとに 1 つまたは 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 変換を使用する場合は、問題の解決策として一時的なものですので、ご注意ください。理想的には、3D と同様に 2D 変換でも同様のレンダリング特性が得られるはずです。ブラウザは驚異的な速度で進化しています。それまでに実現することを願っています。

最後に、可能な限りペイントを回避し、既存の要素をページ内で移動するようにしてください。たとえば、パララックス サイトでは、固定の高さの div を使用して背景の位置を変更し、効果を実現するのが一般的です。ただし、この場合、要素はパスごとに再描画されるため、パフォーマンスに影響する可能性があります。代わりに、可能であれば要素を作成して(必要に応じて overflow: hidden で div 内にラップして)移動することをおすすめします。

オプション 3: 固定位置のキャンバスまたは WebGL を使用する

最後に検討するオプションは、変換された画像を描画するページの一番後ろに固定位置のキャンバスを使用する方法です。一見すると、これはパフォーマンスの高いソリューションのようには見えませんが、このアプローチには次のようなメリットがあります。

  • 要素がキャンバス 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 を使用する場合は、必要なサポートに応じてレンダラを簡単に切り替えることができます。

まとめ

パララックス サイトに対応するためのいくつかのアプローチ(絶対配置要素から固定位置キャンバスの使用まで)を評価しました。実装方法は、達成しようとしている目標と使用している具体的な設計によって異なりますが、選択肢があることを知っておくことは常に有益です。

どのアプローチを試すにしても、推測せずにテストしてください。