パフォーマンスに優れたパララックス

パララックス効果は、好みは別として、今後も広く使われていくでしょう。パララックス効果を適切に使用すると、ウェブアプリに奥行きと繊細さを加えることができます。ただし、パララックス効果をパフォーマンスに優れた方法で実装するのは難しい場合があります。この記事では、パフォーマンスに優れ、クロスブラウザで動作するソリューションについて説明します。

パララックス イラスト。

要約

  • スクロール イベントや background-position を使用してパララックス アニメーションを作成しないでください。
  • CSS 3D 変換を使用して、より正確なパララックス効果を作成します。
  • モバイル Safari では、position: sticky を使用してパララックス効果が伝播されるようにします。

ドロップイン ソリューションをご希望の場合は、UI 要素サンプルの GitHub リポジトリにアクセスして、Parallax ヘルパー JS を入手してください。GitHub リポジトリで、パララックス スクロールのライブデモを確認できます。

パララックスの問題

まず、パララックス エフェクトを実現する一般的な 2 つの方法と、特にそれらがこの目的に適していない理由について説明します。

不適切: スクロール イベントを使用する

パララックスの主な要件は、スクロールに連動することです。ページのスクロール位置が変化するたびに、パララックス要素の位置が更新される必要があります。シンプルに聞こえますが、最新のブラウザの重要なメカニズムは、非同期で動作する機能です。この場合、スクロール イベントに適用されます。ほとんどのブラウザでは、スクロール イベントは「ベスト エフォート」として配信され、スクロール アニメーションのすべてのフレームで配信される保証はありません。

この重要な情報は、スクロール イベントに基づいて要素を移動する JavaScript ベースのソリューションを避ける必要がある理由を示しています。JavaScript では、パララックス効果がページのスクロール位置と同期することを保証できません。以前のバージョンのモバイル Safari では、スクロール イベントは実際にスクロールの終了時に配信されていたため、JavaScript ベースのスクロール エフェクトを作成できませんでした。新しいバージョンでは、アニメーション中にスクロール イベントが配信されますが、Chrome と同様に「ベスト エフォート」ベースです。メインスレッドが他の処理でビジー状態の場合、スクロール イベントはすぐに配信されません。つまり、パララックス効果は失われます。

不正: background-position を更新しています

フレームごとにペイントすることも避けるべきです。多くのソリューションでは、パララックス ルックを実現するために background-position を変更しようとします。これにより、スクロール時にブラウザがページの該当部分を再描画することになり、アニメーションが大幅にジャンクする可能性があります。

パララックス モーションの約束を果たすには、加速プロパティとして適用でき(現在は、変換と不透明度に固執することを意味します)、スクロール イベントに依存しないものが望まれます。

3D の CSS

Scott KellumKeith Clark は、CSS 3D を使用してパララックス モーションを実現する分野で重要な仕事をしてきました。彼らが使用している手法は、次のとおりです。

  • overflow-y: scroll(おそらく overflow-x: hidden も)でスクロールするように、親要素を設定します。
  • 同じ要素に perspective 値を適用し、perspective-origintop left または 0 0 に設定します。
  • その要素の子要素に Z 方向の移動を適用し、画面上のサイズに影響を与えずに視差効果を実現するために、子要素をスケールアップします。

このアプローチの CSS は次のようになります。

.container {
  width: 100%;
  height: 100%;
  overflow-x: hidden;
  overflow-y: scroll;
  perspective: 1px;
  perspective-origin: 0 0;
}

.parallax-child {
  transform-origin: 0 0;
  transform: translateZ(-2px) scale(3);
}

これは、次のような HTML スニペットがあると仮定しています。

<div class="container">
    <div class="parallax-child"></div>
</div>

遠近感のスケールの調整

子要素を後方に押し出すと、遠近感の値に比例して小さくなります。スケールアップする必要がある量は、(遠近感 - 距離)÷ 遠近感の式で計算できます。パララックス要素はパララックス効果を適用しつつ、作成したサイズで表示することがほとんどであるため、そのままにせず、この方法で拡大する必要があります。

上記のコードの場合、遠近感は 1pxparallax-child の Z 距離は -2px です。つまり、要素を 3 倍に拡大する必要があります。これは、コードに挿入された値 scale(3) で確認できます。

translateZ 値が適用されていないコンテンツには、ゼロの値を代入できます。つまり、スケールは (perspective - 0)/ perspective となり、値は 1 になります。つまり、スケールアップもスケールダウンも行われていないことを意味します。非常に便利です。

このアプローチの仕組み

後でこの知識を使用するため、なぜこれが機能するのか明確にすることが重要です。スクロールは効果的に変換であるため、高速化できます。ほとんどの場合、GPU を使用してレイヤを移動します。遠近感のない一般的なスクロールでは、スクロール要素とその子要素を比較すると、スクロールは 1:1 で実行されます。要素を 300px 下方向にスクロールすると、その子要素は同じ量(300px)上方向に変換されます。

ただし、スクロール エレメントにパースペクティブ値を適用すると、このプロセスが混乱し、スクロール変換の基盤となる行列が変更されます。選択した perspective 値と translateZ 値によっては、300 px スクロールしても子要素が 150 px しか移動しない場合があります。要素の translateZ 値が 0 の場合、要素は(以前のように)1:1 でスクロールされますが、遠近感の原点から Z 方向に押し出された子は、異なるレートでスクロールされます。結果として、パララックス モーションが発生します。非常に重要な点として、これはブラウザの内部スクロール メカニズムの一部として自動的に処理されます。つまり、scroll イベントをリッスンしたり、background-position を変更したりする必要はありません。

唯一の難点: モバイル Safari

すべてのエフェクトには注意点があります。トランスフォームに関する重要な注意点の 1 つは、子要素への 3D エフェクトの保持です。パースペクティブのある要素とそのパララックス チルドレンの間に階層内の要素がある場合、3D パースペクティブは「フラット化」され、効果が失われます。

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>

上記の HTML では、.parallax-container が新しく、perspective 値が効果的にフラット化され、パララックス効果がなくなります。ほとんどの場合、解決策は非常に簡単です。要素に transform-style: preserve-3d を追加すると、ツリーの上位に適用された 3D 効果(パースペクティブ値など)が要素に伝播されます。

.parallax-container {
  transform-style: preserve-3d;
}

ただし、モバイル Safari の場合は、もう少し複雑です。コンテナ要素に overflow-y: scroll を適用することは技術的には可能ですが、スクロール要素をフリングできなくなります。解決策は -webkit-overflow-scrolling: touch を追加することですが、これにより perspective もフラットになり、視差は発生しません。

プログレッシブ エンハンスメントの観点から、これはそれほど問題ではありません。すべての状況でパララックスを使用できない場合でも、アプリは機能しますが、回避策を見つけるとよいでしょう。

position: sticky が解決策です。

実際には、position: sticky という形でいくつかのヘルプがあります。これは、スクロール中に要素をビューポートの上部または特定の親要素に「固定」できるようにするために存在します。仕様は、ほとんどの場合かなりの量ですが、役に立つ小さな宝石が含まれています。

一見、大した意味がないように思えますが、この文の重要なポイントは、要素のスティッキネスがどのように正確に計算されるかについて言及している点です。「オフセットは、スクロール ボックスを持つ最も近い祖先を参照して計算されます」。つまり、スティッキー要素を移動する距離(別の要素またはビューポートに接続されているように表示するため)は、他の変換が適用されるに計算され、に計算されるわけではありません。つまり、前のスクロールの例と非常によく似て、オフセットが 300 px で計算された場合、その 300 px のオフセット値を、スティッキー要素に適用する前に、パースペクティブ(または他の変換)を使用して操作できるようになりました。

パララックス要素に position: -webkit-sticky を適用すると、-webkit-overflow-scrolling: touch のフラット化効果を効果的に「反転」できます。これにより、パララックス要素はスクロール ボックスを持つ最も近い祖先(この場合は .container)を参照します。次に、前回と同様に、.parallax-containerperspective 値を適用します。これにより、計算されたスクロール オフセットが変更され、パララックス効果が作成されます。

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>
.container {
  overflow-y: scroll;
  -webkit-overflow-scrolling: touch;
}

.parallax-container {
  perspective: 1px;
}

.parallax-child {
  position: -webkit-sticky;
  top: 0px;
  transform: translate(-2px) scale(3);
}

これにより、Mobile Safari でパララックス エフェクトが復元されます。

固定位置の注意事項

ただし、position: sticky はパララックスの仕組みを変更します。スティッキー ポジショニングでは、要素をスクロール コンテナに固定しようとしますが、スティッキー以外のバージョンではそうしません。つまり、スティッキーがあるパララックスの場合、スティッキーがないパララックスとは逆になります。

  • position: sticky の場合、要素が z=0 に近いほど、移動量は小さくなります
  • position: stickyない場合、要素が z=0 に近づくほど移動量が大きくなります。

抽象的と思われる場合は、Robert Flack によるこちらのデモをご覧ください。要素の固定位置の有無に応じて要素の動作がどのように異なるかを示しています。違いを確認するには、Chrome Canary(執筆時点ではバージョン 56)または Safari が必要です。

パララックス パースペクティブのスクリーンショット

position: sticky がパララックス スクロールに与える影響を示した ロバート フラックのデモ

さまざまなバグと回避策

ただし、他の方法と同様に、まだ調整が必要な点があります。

  • スティッキーのサポートが一貫していない。Chrome ではサポートがまだ実装中です。Edge ではサポートされていません。Firefox では、スティッキーをパースペクティブ変換と組み合わせるとペイント バグが発生する問題があります。このような場合は、必要な場合にのみ position: sticky-webkit- 接頭辞付きのバージョン)を追加するように、少しコードを追加することをおすすめします。これはモバイル Safari 専用です。
  • この効果は Edge で「ただ機能する」わけではありません。Edge は OS レベルでスクロールを処理しようとします。これは通常は良いことですが、この場合はスクロール中の視点の変化を検出できなくなります。この問題を解決するには、固定位置要素を追加します。これにより、Edge を OS 以外のスクロール方法に切り替え、視点の変化を考慮に入れることができます。
  • 「ページのコンテンツが巨大になりました!」多くのブラウザは、ページのコンテンツのサイズを決定する際にスケールを考慮しますが、残念ながら Chrome と Safari は遠近感を考慮していません。たとえば、要素に 3 倍のスケールが適用されている場合、perspective が適用された後に要素が 1 倍になったとしても、スクロールバーなどが表示されることがあります。この問題を回避するには、要素を右下からスケーリングします(transform-origin: bottom right を使用)。これにより、サイズが大きい要素がスクロール可能な領域の「負の領域」(通常は左上)に拡大されます。スクロール可能な領域では、負の領域のコンテンツを表示またはスクロールすることはできません。

まとめ

パララックス効果は、よく考えたうえで使用すると楽しい効果になります。ご覧のとおり、パフォーマンスに優れ、スクロールに連動した、クロスブラウザ対応の方法で実装できます。望ましい効果を得るには、少しの計算と少量のボイラープレートが必要になるため、小さなヘルパー ライブラリとサンプルをまとめました。これは UI 要素サンプルの GitHub リポジトリで確認できます。

ぜひお試しいただき、ご感想をお寄せください。