← ./articles-ja

React useMemoは重いlayout計算と軽いhighlight処理に分ける

useMemo は、dependency listがcacheしたい処理と合っている時だけ効きます。

よくあるperformance bugは、重いdata transformationと一時的なUI stateを1つのmemoに混ぜることです。cacheはされていますが、node selectionやrow selectionやhighlight変更のたびに重い計算が再実行されます。

memoを2段階に分けます。重いlayoutを先に、軽いdecorationを後にします。

症状: clickのたびにlayoutが走る

素朴な実装です。

const graphNodes = useMemo(() => {
  return computeGraphLayout(analyzeResult, selectedNodeId);
}, [analyzeResult, selectedNodeId]);

computeGraphLayout がdagre、tree layout、aggregation、大きなsortを含むなら、selectedNodeId の変更で重い計算全体が無効化されます。

その結果、nodeを選択するたびにUIが引っかかります。

Stage 1: stable dataからlayoutを計算する

まず、geometryを本当のdata dependencyだけから計算します。

const layoutNodes = useMemo(() => {
  return computeGraphLayout(analyzeResult);
}, [analyzeResult]);

ここに入れる処理です。

  • graph layout
  • hierarchy construction
  • grouping
  • aggregation
  • large list sorting

selection、hover、focus stylingは入れません。

Stage 2: 一時的なUI stateを軽く適用する

次に、既にlayout済みのnodeへselection stateを乗せます。

const renderedNodes = useMemo(() => {
  return layoutNodes.map((node) => ({
    ...node,
    selected: node.id === selectedNodeId,
    style: {
      ...node.style,
      stroke: node.id === selectedNodeId ? "#2563eb" : node.style?.stroke,
    },
  }));
}, [layoutNodes, selectedNodeId]);

これでselection click時は、既存nodeのmapだけになります。重いlayoutは analyzeResult が変わるまで再利用されます。

2つのstageの責務を分ける

Stage 1が答える質問です。

nodeは何で、どこに置くか?

Stage 2が答える質問です。

既に配置されたnodeを今どう見せるか?

geometryを変えるdependencyはStage 1です。style、focus、selection、badgeだけを変えるdependencyはStage 2です。

使わないほうがよい場合

どこにでも入れるpatternではありません。

次の場合は避けます。

  • 計算が軽い
  • jankを測定できない、体感できない
  • transient stateが本当にlayoutを変える
  • simple componentが読みにくくなる

重い処理があり、一時的なUI stateが頻繁に変わる時に使います。

検証チェック

  • node選択でlayout functionが呼ばれない
  • source data変更ではlayout functionが呼ばれる
  • hoverとselectionの見た目が正しい
  • React Profilerでselection commitが短くなる
  • dependency arrayがstage責務と一致する
  • data変更とselection変更の両方をtestする

参考