Next.js はどうやってスクロール位置を復元するのかを読んだ
読んだので軽く個人的まとめメモ。
scrollRestoration
というフラグが experimental の機能で存在する
デフォルトでもブラウザ側でスクロール位置を復元してくれることもありますが、Safariでは復元されなかったり、ChromeでもgetServerSideProps利用時にはこのフラグを有効にしないとスクロール位置が復元されないなど不安定な状態です。
スクロール復元挙動
- whatwg に記載がある
- ブラウザの history entry には
scroll position data
という state がある- スクロール位置の情報を持っているっぽい
scroll restoration mode
というモードがある- History.scrollRestoration - Web API | MDN
- ↑これに初期値である auto か manual を代入すると
scroll restoration mode
にできる
scroll restoration mode
がauto
なら、scroll position data
をもとに user agent がスクロール位置を復元することができる。
Next.js の挙動
- デフォルトだと
experimental.scrollRestoration = false
になっている- この場合の
history.scrollRestoration
はauto
- auto なら復元できるんじゃなかったっけ?と思ったけど SPA では auto でもブラウザの復元処理がうまくいかないことがあるらしい
- この場合の
getServerSideProps
- getServerSideProps を含むページの遷移時やブラウザバック時には Next.js は getServerSideProps をサーバー側で実行する
- props などの戻り値を取得するような fetch を実行するためブラウザバック時のレンダリングは非同期になる
- ここがあまりピンときてないんだけど fetch の実行が終わるまでレンダリングが完了しないということだろうか
- これ(非同期のレンダリング?)だとスクロール位置がブラウザによって復元されない
getStaicProps
- これは getServerSideProps と同様にブラウザバック時に fetch が走る
- fetch によって取得するのは静的な JSON であり、多くの場合は fetch 先から 304(Not Modified)が返る
- Chrome ではスクロール位置が復元されたが Safari では復元されなかった
- Data fetching を含まない場合
- 動的な props 取得がないのでブラウザバック時のレンダリングで fetch が走らず、同期的にレンダリングできる
- 結果は getStaicProps と同様に Chrome では復元されたが Safari ではされない
スクロール位置喪失と Navigation API
- SPA だと状態の復元やスクロール位置の復元が軽視されがちで、これらを軽視するとブラウザバックやブラウザフォワードの際の UX が悪化する
- こういった状況の改善のために Navigation API というブラウザ API が作られ、 SPA の体験改善が検討されている
- Navigation API はまだ Chrome でしか実装されていないため Chrome 以外での対応のためにも別途実装が必要
- これが Next.js の
experimental.scrollRestoration
- これが Next.js の
experimental.scrollRestoration
の実装
- 1 現在の履歴 (history) に対して key を発行
- Next.js の
next/link
やuseRouter
などで使われている Router クラスを使う - Router クラスは Next.js のライフサイクルに対する責務を持っている
- この Router 内で新規の履歴に対してユニークな key を作成する
- もともとは単純な index だったが簡単に破綻するため、記事の筆者が修正のプルリクエストを送りマージされたらしい、すごい
- Next.js の
- 2 遷移前の key とスクロール位置を Session Storage に保存
- 遷移前の key を用いて Session Storage に
__next_scroll_${key}
という名前で保存する
- 遷移前の key を用いて Session Storage に
- 3 ブラウザバック / ブラウザフォワード時に key をもとにスクロール位置を取得
__next_scroll_${key}
で getItem して、値があればスクロール位置の情報として取得する
- 4 Next.js の render 処理でスクロール位置を復元
- レンダリング後の useLayoutEffect で
window.scrollTo
に取得したスクロール位置の情報を渡す
- レンダリング後の useLayoutEffect で
残課題
- ブラウザバック / ブラウザフォワード時については問題ないが、リロード時には Router の key がリセットされてしまうので復元できない
- これについては記事の著者がまたまた Next.js にプルリクエストを送っているらしい、すごい
Navigation API の利用
- 将来的に Navigation API が普及した場合
Navigation APIではnavigateイベント時に遷移を定義できるようになります。遷移を定義できるというのは、ブラウザ側に「遷移の開始〜終了」をPromiseで渡すAPIを用意することで、ブラウザ側がMPAでしかできなかったローダーの表示やブラウザによるスクロール位置の復元を行えるようになることを意味します。
必要であれば、e.restoreScroll()というメソッドを呼び出せばtransitionWhileに渡すPromiseの任意のタイミングでスクロール位置を復元できるので、履歴のkey管理やSession Storageへのスクロール位置情報の格納は不要になります。