// A devlog by mood.

@swup/scroll-plugin更新でスクロール復元が崩れた話

3.x系から4.0系に更新後にブラウザバック時のスクロール位置復元が崩れたので見直したメモ。

@swup/scroll-pluginをアップデートしたら、ブラウザバック時のスクロール位置復元が壊れました。

記事一覧から詳細に移動して、戻る。
本来なら一覧の元いた位置に戻ってほしいのに、ページ上部に戻るようになってしまった。

SPAっぽいページ遷移を入れているサイトではよくあるやつですが、今回は@swup/scroll-plugin4.0系へ更新したタイミングで表面化しました。

使っている構成

このテンプレートでは、ページ遷移とスクロールまわりに以下を使っています。

  • swup
  • @swup/scroll-plugin
  • @swup/js-plugin
  • lenis
  • gsap

Swupでページ遷移を差し替えつつ、Lenisでスムーススクロールを管理しています。
さらにGSAPで遷移アニメーションを入れているので、普通のブラウザ遷移よりスクロール復元のタイミングが少しシビアです。

今回の更新では、@swup/scroll-plugin3.3系から4.0系へ上げました。
そこで、以前の「swup.scrollToを直接上書きする」やり方を見直すことになりました。

特にブラウザバックでは、

  • 遷移前のスクロール位置を保存する
  • 戻り先のHTMLを差し替える
  • 戻り先の高さが決まる
  • 保存していた位置へ戻す
  • 遷移アニメーションを終える

という順番が崩れると、元の位置に戻れません。

壊れ方

記事一覧の下のほうまでスクロールして、記事詳細へ移動する。
そこからブラウザバックすると、本来は一覧の同じ位置に戻ってほしい。

でも@swup/scroll-plugin更新後は、復元位置が復元しなくなった。

原因としては、Swup Scroll Plugin側のスクロール復元とLenis側のスクロール状態がうまく噛み合っていなかったこと。
Swup Scroll Pluginは戻り先のスクロール位置を知っていますが、実際のスクロールはLenisが握っている。
ここがずれると、Swupは「復元したつもり」でも、Lenis上では別の位置にいる状態になります。

以前はswup.scrollToを上書きして、そこでLenisのscrollToを呼んでいました。

swup.scrollTo = (offset, animate = true) => {
  const target = Number.isFinite(offset) ? offset : 0;

  if (!lenis) {
    window.scrollTo(0, target);
    return;
  }

  lenis.resize();
  lenis.scrollTo(target, { immediate: !animate, force: true });
};

一旦はこれで動いていたんですが、プラグイン内部の流れに後から差し込む形なので、@swup/scroll-pluginの更新に弱い。
スクロール開始・終了のフックや、横方向の位置、スクロールコンテナの扱いも自前で合わせる必要がありました。

対処

最終的には、@swup/scroll-plugin v4の流れに合わせて、swup.scrollToを上書きするのをやめました。

代わりに、@swup/scroll-pluginscrollFunctionを使って、SwupがスクロールしたいタイミングだけLenisへ橋渡しする形に変更しました。

const scrollWithLenis =
  (lenis: Lenis | null) =>
  (scrollContainer, top, left, animate, start, end) => {
    start();

    if (!lenis) {
      window.scrollTo(left, top);
      end();
      return;
    }

    lenis.resize();

    if (!animate) {
      lenis.scrollTo(top, { immediate: true, force: true });
      end();
      return;
    }

    lenis.scrollTo(top, {
      force: true,
      onComplete: end,
    });
  };

設定側ではこんな感じです。

new SwupScrollPlugin({
  animateScroll: {
    betweenPages: false,
    samePageWithHash: true,
    samePage: true,
  },
  offset: 112,
  scrollFunction: scrollWithLenis(lenis),
});

これで、スクロール位置の保存や復元の判断はSwup Scroll Pluginに任せつつ、実際にスクロールを動かす部分だけLenisに渡せます。

startendもプラグインから渡されるので、スクロール処理の開始・完了をSwup側に返せるのもよいところです。
自前でscroll:startscroll:endを呼ぶより、役割がかなり分かりやすくなりました。

まとめ

今回の修正でよかったのは、独自の上書きを減らせたことです。

swup.scrollToを直接差し替えると、最初は手早く直せます。
でも、Swup Scroll Pluginが本来持っている「いつリセットするか」「いつ復元するか」「スクロールコンテナをどう扱うか」まで自分で面倒を見ることになります。

今回はscrollFunctionに寄せたことで、

  • スクロール位置の保存・復元はプラグインに任せる
  • 実際のスクロール実行だけLenisに任せる
  • 通常遷移では先頭へ戻す
  • ブラウザバックでは元の位置へ戻す

という分担に戻せました。

ライブラリのアップデートで壊れたときは、つい動いていた頃の処理を足して直したくなります。
でも、ライブラリ側に用意されている拡張点を使ったほうが、次の更新に強くなりやすいですね。

以上、おわり。