Upgrade to Pro — share decks privately, control downloads, hide ads and more …

React Suspenseを使って遷移体験を向上させる

kobayang
September 25, 2023

React Suspenseを使って遷移体験を向上させる

ページ遷移をJSで制御する場合のページバック時のユーザー体験の問題について解説します。続いてそれを解決するページキャッシュの方針について説明します。最後に、React Suspenseの仕組みを応用してページバック時の問題を解決する方法について説明します。

kobayang

September 25, 2023
Tweet

More Decks by kobayang

Other Decks in Technology

Transcript

  1. Copyrights(c) Henry, Inc. All rights reserved. 自己紹介 • 小林 直樹(こばやん)

    • 採用系ベンチャーで5年ほど • ヘンリーに今年の6月にジョイン • Webエンジニア • X: @kbys_02
  2. Copyrights(c) Henry, Inc. All rights reserved. アウトライン • SPAのページバック時の課題 •

    前ページをキャッシュする • React Suspense と React Freeze • まとめ お詫び: アーキテクチャ設計の話はほぼしません🙏
  3. Copyrights(c) Henry, Inc. All rights reserved. ref: Next.jsで戻る厨を満たすrecoil-sync-next: https://zenn.dev/akfm/articles/recoi-sync-next SPAのページバック時の課題

    前のページに戻る体験について • 通常の遷移の場合ブラウザキャッシュである程度復元する • ページコントロールをJSで行う(SPA的な遷移)場合、 前のページに戻る時に状態がリセットされる • 戻る時の体験を気をつけないといけない
  4. Copyrights(c) Henry, Inc. All rights reserved. SPAのページバック時の課題 何がリセットされるか? • UIの状態

    ◦ スクロールの位置など • State ◦ コンポーネント内のState(useState) • 良くある例 ◦ 一覧→個別→一覧に戻った時に振り出しに戻る(*) ref: (*) 最近のWebサイトで「記事一覧ページ」と「個別記事ページ」を行き来すると「記事一覧ページ」がふりだしに戻る問 題について - 結城浩の連ツイ:https://rentwi.hyuki.net/?1576010373357965312
  5. Copyrights(c) Henry, Inc. All rights reserved. • Next.js 上で動いている •

    各ページへはNext.jsのrouterからSPA的に遷移 前提: Henryの場合
  6. Copyrights(c) Henry, Inc. All rights reserved. 前提: Henryの場合 • 各ページの行き来が頻繁に発生

    ◦ 診療で患者一覧<>患者カルテ ◦ 患者カルテ内をさらに行き来する...など • ページバックたびに状態がリセットされると不便
  7. Copyrights(c) Henry, Inc. All rights reserved. SPAのページバック時の課題 • 解決案 ◦

    URL Paramにより状態を持たせる? ▪ 解決できる範囲が局所的 • 大きな変更なしで課題を解決したい
  8. Copyrights(c) Henry, Inc. All rights reserved. const cache = new

    Map(); const App = ({ key, Component }) => { const renderPages = useMemo(() => { if (!cache.get(key)) cache.set(key, Component); const elements = []; cache.forEach((C, k) => { elements.push(<Page $show={key === k}><C /></Page>); }); return elements; }, [key, Component]); return <>{renderPages}</>; }; 実装イメージ
  9. Copyrights(c) Henry, Inc. All rights reserved. 実装イメージ const cache =

    new Map(); const App = ({ key, Component }) => { const renderPages = useMemo(() => { if (!cache.get(key)) cache.set(key, Component); const elements = []; cache.forEach((C, k) => { elements.push(<Page $show={key === k}><C /></Page>); }); return elements; }, [key, Component]); return <>{renderPages}</>; }; ページ遷移に対してユ ニークなkeyとコンポー ネントを要求 Next.jsの場合につい ては後述
  10. Copyrights(c) Henry, Inc. All rights reserved. コンポーネントをkeyで キャッシュ 実装イメージ const

    cache = new Map(); const App = ({ key, Component }) => { const renderPages = useMemo(() => { if (!cache.get(key)) cache.set(key, Component); const elements = []; cache.forEach((C, k) => { elements.push(<Page $show={key === k}><C /></Page>); }); return elements; }, [key, Component]); return <>{renderPages}</>; };
  11. Copyrights(c) Henry, Inc. All rights reserved. キャッシュされたコン ポーネントを描画 現在のkeyの時だけコ ンポーネント表示

    他は display: none 実装イメージ const cache = new Map(); const App = ({ key, Component }) => { const renderPages = useMemo(() => { if (!cache.get(key)) cache.set(key, Component); const elements = []; cache.forEach((C, k) => { elements.push(<Page $show={key === k}><C /></Page>); }); return elements; }, [key, Component]); return <>{renderPages}</>; };
  12. Copyrights(c) Henry, Inc. All rights reserved. 注: keyとComponentに何を使うか? Next.jsの場合 •

    Component ◦ Next.jsのAppPropsのComponent ◦ ページが切り替わるたびに変わる • key ◦ Next.jsでは HistoryState に key が格納されている ◦ window.history.state.key から取得可能 ▪ router objectからとりたい... ▪ App Routerだどkeyがない...(*) ref: (*) Add router.key to identify history: https://github.com/vercel/next.js/discussions/47242
  13. Copyrights(c) Henry, Inc. All rights reserved. 問題点 display: noneで大体動くが描画処理の重さに課題 •

    display: noneしてもNode上には要素が存在 • 遷移のたびに再描画の更新(reconcile)が走る • 履歴が深くなるたびに処理が重くなる
  14. Copyrights(c) Henry, Inc. All rights reserved. • React Freeze (*)

    • React Suspense を使って描画状態を制御 ◦ freeze の boolean 改善策: React Freeze (Suspense) を使う import { Freeze } from "react-freeze" <Freeze freeze={visible}><Page /></Freeze> ref: (*) software-mansion/react-freeze: https://github.com/software-mansion/react-freeze
  15. Copyrights(c) Henry, Inc. All rights reserved. • 仕組みはシンプル • freeze中にPromiseをthrowしSuspendにする

    • より詳細は以前記事(*)を書いているので興味があれば • Suspendの間はReconcileされない性質を利用 • 不要な描画処理をなくすことができる React Freeze ref: (*) React Suspenseで不要な描画処理をなくす: https://zenn.dev/kobayang/articles/8e06c77cec9359
  16. Copyrights(c) Henry, Inc. All rights reserved. 前ページをキャッシュする React Freeze で

    Suspend 状態にする • ページ切り替え時に前ページをキャッシュする
  17. Copyrights(c) Henry, Inc. All rights reserved. 実装イメージ(with React Freeze) const

    cache = new Map(); const App = ({ key, Component }) => { const renderPages = useMemo(() => { if (!cache.get(key)) cache.set(key, Component); const elements = []; cache.forEach((C, k) => { elements.push( <Freeze freeze={key !== k}><C /></Freeze>); )}); return elements; }, [key, Component]); return <>{renderPages}</>; }; キャッシュされたコン ポーネントを描画 React Freezeを代わ りに使う 現在のkey以外を freeze
  18. Copyrights(c) Henry, Inc. All rights reserved. プロダクトに導入する • Freeze中のコンポーネントの副作用の確認 •

    キャッシュを無限に増やさないようにコントロール • メモリ使用量の確認
  19. Copyrights(c) Henry, Inc. All rights reserved. プロダクトに導入する • Freeze中のコンポーネントの副作用の確認 ◦

    表示しているページの裏でキャッシュされているページも描画 ◦ Suspend の間も useEffect は発火 ▪ tips: useLayoutEffect は発火しない ◦ 副作用の見直し ▪ useEffectの発火条件やロジックに問題ないかを確認 ▪ どうしても発火してほしくない副作用を useLayoutEffect にする
  20. Copyrights(c) Henry, Inc. All rights reserved. プロダクトに導入する • キャッシュを無限に増やさないようにコントロール ◦

    reconcile が起きないとはいえ、何もしないと無限に積まれる ◦ 雑にやるなら最大数を決めておくなど • ヘンリー特有の事情 ◦ ナビゲーションの stack を管理している(*) ▪ Next.js の routeChangeComplete をハンドリングして管理 ◦ 相互に行き来できるページ遷移を push → replace に変更 ▪ replace によるSPA的な遷移の場合は key は変わらないため ▪ 無限に stack を増やさせないように ref: (*) ルーター自前実装の話: https://www.slideshare.net/KazushiKawamura/ss-252983036
  21. Copyrights(c) Henry, Inc. All rights reserved. • メモリ使用量の確認 ◦ Developer

    Tools の Memory から確認 ◦ 50個ほどStackに積んで許容の範囲内であることを確認した プロダクトに導入する
  22. Copyrights(c) Henry, Inc. All rights reserved. まとめ • SPAの場合特にページバック時の遷移体験が悪い •

    ページをキャッシュすることで状態のリセットを防ぐ • その際にReact Freezeを使うと再描画をなくせる • 副作用の考慮や無限にキャッシュするのを防ぐ仕組みが必要