$30 off During Our Annual Pro Sale. View Details »

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.
    React Suspenseを使って
    遷移体験を向上させる
    Frontend Night 〜アーキテクチャ設計編〜
    2023-09-27 @kobayang

    View Slide

  2. Copyrights(c) Henry, Inc. All rights reserved.
    自己紹介
    ● 小林 直樹(こばやん)
    ● 採用系ベンチャーで5年ほど
    ● ヘンリーに今年の6月にジョイン
    ● Webエンジニア
    ● X: @kbys_02

    View Slide

  3. Copyrights(c) Henry, Inc. All rights reserved.
    アウトライン
    ● SPAのページバック時の課題
    ● 前ページをキャッシュする
    ● React Suspense と React Freeze
    ● まとめ
    お詫び: アーキテクチャ設計の話はほぼしません🙏

    View Slide

  4. Copyrights(c) Henry, Inc. All rights reserved.
    ref: Next.jsで戻る厨を満たすrecoil-sync-next: https://zenn.dev/akfm/articles/recoi-sync-next
    SPAのページバック時の課題
    前のページに戻る体験について
    ● 通常の遷移の場合ブラウザキャッシュである程度復元する
    ● ページコントロールをJSで行う(SPA的な遷移)場合、
    前のページに戻る時に状態がリセットされる
    ● 戻る時の体験を気をつけないといけない

    View Slide

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

    View Slide

  6. Copyrights(c) Henry, Inc. All rights reserved.
    ● Next.js 上で動いている
    ● 各ページへはNext.jsのrouterからSPA的に遷移
    前提: Henryの場合

    View Slide

  7. Copyrights(c) Henry, Inc. All rights reserved.
    前提: Henryの場合
    ● 各ページの行き来が頻繁に発生
    ○ 診療で患者一覧<>患者カルテ
    ○ 患者カルテ内をさらに行き来する...など
    ● ページバックたびに状態がリセットされると不便

    View Slide

  8. Copyrights(c) Henry, Inc. All rights reserved.
    SPAのページバック時の課題
    ● 解決案
    ○ URL Paramにより状態を持たせる?
    ■ 解決できる範囲が局所的
    ● 大きな変更なしで課題を解決したい

    View Slide

  9. Copyrights(c) Henry, Inc. All rights reserved.
    SPAのページバック時の課題
    ● ページ切り替え時に前ページをキャッシュする

    View Slide

  10. Copyrights(c) Henry, Inc. All rights reserved.
    前ページをキャッシュする
    ● ページ切り替え時に前ページをキャッシュする

    View Slide

  11. Copyrights(c) Henry, Inc. All rights reserved.
    前ページをキャッシュする
    display: none で非表示にしつつ残す
    ● ページ切り替え時に前ページをキャッシュする

    View Slide

  12. 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();
    });
    return elements;
    }, [key, Component]);
    return <>{renderPages}>;
    };
    実装イメージ

    View Slide

  13. 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();
    });
    return elements;
    }, [key, Component]);
    return <>{renderPages}>;
    };
    ページ遷移に対してユ
    ニークなkeyとコンポー
    ネントを要求
    Next.jsの場合につい
    ては後述

    View Slide

  14. 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();
    });
    return elements;
    }, [key, Component]);
    return <>{renderPages}>;
    };

    View Slide

  15. 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();
    });
    return elements;
    }, [key, Component]);
    return <>{renderPages}>;
    };

    View Slide

  16. 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

    View Slide

  17. Copyrights(c) Henry, Inc. All rights reserved.
    注: 実際は他にもキャッシュが必要
    ● キャッシュするのはComponentだけではない
    ○ Page Props
    ○ Router Object
    ○ 細かすぎる話になるのでここでは割愛

    View Slide

  18. Copyrights(c) Henry, Inc. All rights reserved.
    問題点
    display: noneで大体動くが描画処理の重さに課題
    ● display: noneしてもNode上には要素が存在
    ● 遷移のたびに再描画の更新(reconcile)が走る
    ● 履歴が深くなるたびに処理が重くなる

    View Slide

  19. Copyrights(c) Henry, Inc. All rights reserved.
    ● React Freeze (*)
    ● React Suspense を使って描画状態を制御
    ○ freeze の boolean
    改善策: React Freeze (Suspense) を使う
    import { Freeze } from "react-freeze"

    ref: (*) software-mansion/react-freeze: https://github.com/software-mansion/react-freeze

    View Slide

  20. Copyrights(c) Henry, Inc. All rights reserved.
    ● 仕組みはシンプル
    ● freeze中にPromiseをthrowしSuspendにする
    ● より詳細は以前記事(*)を書いているので興味があれば
    ● Suspendの間はReconcileされない性質を利用
    ● 不要な描画処理をなくすことができる
    React Freeze
    ref: (*) React Suspenseで不要な描画処理をなくす: https://zenn.dev/kobayang/articles/8e06c77cec9359

    View Slide

  21. Copyrights(c) Henry, Inc. All rights reserved.
    前ページをキャッシュする
    React Freeze で Suspend 状態にする
    ● ページ切り替え時に前ページをキャッシュする

    View Slide

  22. 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(
    );
    )});
    return elements;
    }, [key, Component]);
    return <>{renderPages}>;
    };
    キャッシュされたコン
    ポーネントを描画
    React Freezeを代わ
    りに使う
    現在のkey以外を
    freeze

    View Slide

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

    View Slide

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

    View Slide

  25. 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

    View Slide

  26. Copyrights(c) Henry, Inc. All rights reserved.
    ● メモリ使用量の確認
    ○ Developer Tools の Memory から確認
    ○ 50個ほどStackに積んで許容の範囲内であることを確認した
    プロダクトに導入する

    View Slide

  27. Copyrights(c) Henry, Inc. All rights reserved.
    まとめ
    ● SPAの場合特にページバック時の遷移体験が悪い
    ● ページをキャッシュすることで状態のリセットを防ぐ
    ● その際にReact Freezeを使うと再描画をなくせる
    ● 副作用の考慮や無限にキャッシュするのを防ぐ仕組みが必要

    View Slide