【脱GSAP】スマホでパララックスがガタつく・ワープする現象はposition: stickyで解決する

【脱GSAP】スマホでパララックスがガタつく・ワープする現象はposition: stickyで解決する

「PCでは完璧なのに、iPhoneで見るとガタガタ震える…」

「スクロールした瞬間に背景がワープする…」

「リロードすると直るけれど、初回アクセス時だけ崩れる…」

Web制作の現場で、GSAP (ScrollTrigger) を使ったパララックス実装中に、このような現象にハマった経験はありませんか?

今回は、特定のセクションにおけるパララックス実装で発生した「スマホ特有の不具合」に対し、最終的に 「GSAP(pin)の使用をやめて、CSSの position: sticky に戻る」 ことで解決した事例をご紹介します。

当初の要件と実装

当初は、アニメーションライブラリである GSAP とそのプラグイン ScrollTrigger を採用していました。

実装したかった要件はシンプルです。

  • 「採用情報セクションに到達したら背景を固定し、画像だけが下から上に流れていくパララックス演出」

PCでの動作は pin: true で画面を固定し、scrub で滑らかに画像を動かすことで完璧に動作していました。

しかし、スマホの実機テストで問題が発覚します。

スマホ実機で発生した不具合

iPhone(iOS Safari)で確認すると、以下のような挙動が頻発しました。

  • ガタつき(Jitter): スクロールするたびに画面が細かく震える。
  • 位置ズレ: 固定されるはずの要素が一瞬遅れてついてくる、あるいは意図しない位置に飛ぶ。
  • 二重表示: 固定された要素とスクロール中の要素が重なって見える。
  • 初回表示の崩れ: リロードすると直るが、ページ遷移直後だけ計算がズレる。

原因は「ブラウザの仕様」と「JS制御」の相性

調査の結果、以下の要因が複雑に絡み合っていることが分かりました。

  1. アドレスバーの伸縮: スクロールでアドレスバーが動くたびに 100vh の高さが変わり、GSAPが再計算(Refresh)を繰り返してしまう。
  2. 慣性スクロールとの競合: iOS特有の慣性スクロールと、JSによる強制的な位置固定(pin)の処理が喧嘩をしている。
  3. 読み込み遅延: loading="lazy" やWebフォントの適用タイミングで高さが変わり、計算が狂う。

ScrollTrigger.normalizeScroll(true)ignoreMobileResize の設定、画像の読み込み待機など、あらゆるJSの調整を試みましたが、コードが複雑になるばかりで根本的な解決には至りませんでした。

JSで固定するのをやめる

そこで、原点に立ち返って考え直しました。

「そもそも、画面を固定するだけなら CSS の position: sticky で良いのでは?」

GSAPの pin は、position: fixedtransform を駆使して「擬似的に固定」しています。そのため、計算がわずかでもズレるとガタつきが発生します。

一方、position: sticky はブラウザのネイティブ機能であり、OSレベルで最適化されているため、描画がガタつくことはありません。

「固定はCSS(sticky)に任せて、パララックスの移動計算だけを軽いJSで行う」

これが、最も安定する解決策でした。

解決コード(脱GSAP版)

CSS:固定は sticky に任せる

まず、CSSでセクションを固定します。これだけで「ガタつき・位置ズレ」は根本的に解消されます。

CSS

// 固定したいセクションの親要素
#js-recruit-section {
  position: relative;
  // 高さを持たせることでスクロール領域を作る
  height: 200vh; 
}

// 固定する中身
.sticky-content {
  position: -webkit-sticky;
  position: sticky;
  top: 0;
  left: 0;
  height: 100vh;
  overflow: hidden;
  // これだけで、どんなにスクロールしても滑らかに固定される
}

JS:移動計算だけをシンプルに書く

GSAPのような高機能なライブラリは使用せず、requestAnimationFrame を使った必要最小限のコードを記述します。

JavaScript

document.addEventListener("DOMContentLoaded", () => {
  const section = document.getElementById("js-recruit-section");
  const parallaxImages = document.querySelectorAll(".js-parallax-img");
  const parallaxTitle = document.querySelector(".js-parallax-title");
   
  let ticking = false;

  function updateParallax() {
    // セクションの位置情報を取得
    const rect = section.getBoundingClientRect();
    const sectionTop = rect.top;
    const sectionHeight = rect.height;
    const windowHeight = window.innerHeight;

    // セクションが画面内にある時だけ計算する(軽量化)
    if (sectionTop < windowHeight && sectionTop > -sectionHeight) {
      let scrollProgress = 0;
       
      // セクションの上端が画面上部に到達してから計算開始
      if (sectionTop <= 0) {
        scrollProgress = Math.abs(sectionTop);
      }

      // 画像を動かす
      parallaxImages.forEach((img) => {
        const speed = parseFloat(img.getAttribute("data-speed")); // HTML側で速度調整可能に
        const startOffset = 400; // 初期位置
        // スクロール量に応じて transform を更新
        const yPos = startOffset - scrollProgress * speed * 10;
        img.style.transform = `translateY(${yPos}px)`;
      });

      // タイトルも動かす(視差効果)
      if (parallaxTitle) {
        const tSpeed = parseFloat(parallaxTitle.getAttribute("data-speed"));
        const tStartOffset = 0;
        const tPos = tStartOffset - scrollProgress * tSpeed * 5;
        parallaxTitle.style.transform = `translateY(${tPos}px)`;
      }
    }
    ticking = false;
  }

  // スクロールイベントの最適化
  window.addEventListener("scroll", () => {
    if (!ticking) {
      window.requestAnimationFrame(updateParallax);
      ticking = true;
    }
  }, { passive: true }); // passive: true でスクロール性能を阻害しない

  // 初回実行
  updateParallax();
});

技術的なポイント:

  • requestAnimationFrame: スクロールイベントを間引いて、描画のタイミングに合わせて計算することでCPU負荷を抑えています。
  • passive: true: スクロールイベントリスナーにこれを付与することで、ブラウザに「スクロールをブロックしない」と伝え、パフォーマンスを向上させています。
  • getBoundingClientRect: 要素の現在位置を正確に取得し、画面内にあるときだけ計算を実行しています。

結果:驚くほど滑らかに

この変更によるメリットは明確でした。

  1. ガタつきゼロ: position: sticky はブラウザのネイティブ挙動なので、非常に滑らかです。
  2. 表示崩れなし: 複雑な再計算がないため、ワープや二重表示が起きません。
  3. 超軽量: 巨大なライブラリを読み込まず、計算量も最小限。古い端末でも快適です。
  4. メンテナンス性向上: バニラJSによるシンプルな記述なので、後から誰が見ても修正が容易です。

まとめ

GSAPは素晴らしいツールですが、こと「スマホでの画面固定」に関しては、ブラウザ標準の position: sticky の安定性が勝ります。

もし今、ScrollTriggerの「スマホ実機でのガタつき」に悩み、設定の調整地獄に陥っているなら、一度 「脱GSAP(固定処理のみCSSに任せる)」 を検討してみてください。

シンプル・イズ・ベスト。これが、長い検証の末にたどり着いた最適解でした。

最近の投稿