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する