ヒーローのGSAP、Vanilla JSに置き換えて軽量化した話

Xで見かけた投稿がきっかけでした。「animation-timelineとコンテナスタイルクエリを使えば、IntersectionObserverを使わなくてもスクロール演出が組める」という内容で、面白そうだったのでトップページのヒーロー(バッジやロゴがふわっと出てくる演出)に使えないか調べてみることにしました。

裏取りしたら、採用にはまだ早かった

調べてみると、たしかに便利そうな仕組みでした。でもMDNのブラウザ対応表を確認したところ、Chrome・Edge・Safariは対応済みなものの、Firefoxはまだ設定フラグを有効にしないと動かない段階でした。4大ブラウザ揃い踏み、はもう少し先の話。攻めるにはちょっと早いなと判断して、この仕組み自体は「今後に期待」枠にいったん置くことにしました。

💡 Tips: animation-timelineとコンテナスタイルクエリって何?

どちらもCSSだけでスクロール連動の演出を組める仕組みです。animation-timelineはスクロール量そのものをアニメーションの時間軸として扱え、コンテナスタイルクエリは親要素のスタイル状態を見て子要素の見た目を変えられます。今までJavaScriptで書いていた処理の一部が、CSSだけで完結する未来がすぐそこまで来ている、という感じの機能です。

ついでに見返したヒーロー演出が、明らかにオーバースペックだった

この調べ物のついでに、自分のヒーロー演出の実装を見返してみました。ロゴとバッジがふわっとフェードインして、スクロールすると少しパララックスする、それだけの演出です。ところが中身を見ると、GSAPとScrollTriggerをCDN経由で丸ごと読み込んでいました。この規模の動きにフルセットのアニメーションライブラリ、明らかに不釣り合いです。

フェードインは画面に入ったタイミングで1回だけ、パララックスはスクロール量に応じて数値を書き換えるだけ。IntersectionObserverscrollイベント+requestAnimationFrameで十分まかなえる規模の処理です。というわけで、animation-timelineの採用は見送ったものの、GSAPを外してVanilla JSに置き換える作業はやる価値があると判断し、Claude Codeに実装してもらうことにしました。

1つ目の壁: transformの取り合い、また出た

実装をお願いして最初に出てきたのが、見覚えのあるバグでした。フェードイン時の拡大(scale)と、スクロール時のパララックス(translateY)、それぞれ別のCSS/JSから同じtransformプロパティに書き込もうとして、片方がもう片方を上書きしてしまう現象です。

既視感がありました。実は前にバッジを円軌道で回したときにも踏んだのと同じ構造の罠でした。あのときは要素を分けて役割ごとにtransformの持ち主を1つに絞ることで解決したんですが、今回は同じ要素の中で両方の動きを共存させたかったので、別の対処が必要でした。

Claude Codeが選んだのは、CSSカスタムプロパティに一本化する方法です。--hero-tag-scale--hero-tag-parallax-yという2つの変数を用意して、実際のtransformtranslateY(var(--hero-tag-parallax-y)) scale(var(--hero-tag-scale))と、1つの式の中に両方組み込む形にしました。フェードイン側は--hero-tag-scaleだけを書き換え、パララックス側は--hero-tag-parallax-yだけを書き換える。transformプロパティ自体を直接取り合うのではなく、その中の変数だけをそれぞれ担当する、という考え方です。同じ罠を2回目でようやく根本から潰せた感じがして、ちょっと嬉しかったです。

2つ目の壁: タブを裏に回すと、パララックスが二度と動かなくなる

もう1つは、今回はじめて踏んだバグでした。スクロールのたびにrequestAnimationFrameで描画をスケジュールする、よくある「間引き」の実装。ブラウザのタブを裏に回した状態でスクロールすると、以降パララックスが完全に止まってしまう不具合が出ました。

また同じ根っこでした。原因を聞いたら、これも前に踏んだのと同じ話でした。ブラウザは裏に回ったタブのrequestAnimationFrameをほぼ止めてしまうので、「今スケジュール中です」を示すフラグ(ticking)がtrueのまま固まってしまい、タブを表に戻しても二度と更新されなくなっていたんです。以前バッジの検証中にタブが裏に回ってアニメーションが止まって見えた話と、症状は違えど原因は同じdocument.hiddenまわりの挙動でした。

対処はvisibilitychangeイベントを監視して、タブが表に戻ったタイミングでtickingを強制的にfalseに戻し、最新のスクロール位置で描き直す、というものでした。地味な一手間です。でもこれを入れないと、「スクロールしても動かない」という一番気づきにくい壊れ方をしたままになるところでした。

同じ罠を2回踏んで、ようやく気づいたこと

今回、transformの取り合いも、タブ非表示によるrAF停止も、どちらも初めて出会うバグではありませんでした。1回目はその場しのぎで解決していたんですが、2回目に同じ構造で出てきたことで、「これはこのプロジェクト全体で気をつけるべきパターンなんだ」とようやく実感できました。

結果的に、CDN読み込みが1本減った分だけページは軽くなりましたし、animation-timelineという新しい選択肢を評価したおかげで、今の実装の無駄にも気づけました。思わぬ副産物です。新しい技術を試そうとして寄り道した先で、既存の実装の見直しにつながる。今回はそういう回でした。

この記事を書いた人

Web制作歴10年近く。WordPressのテーマカスタマイズやコーディングを中心に、副業・本業ともにweb制作に携わっています。このブログでは、実際に手を動かして分かったことを書いています。

Cat's Handをフォローする

この記事が参考になったら、応援クリックをお願いします

タイトルとURLをコピーしました