perf: 减少工作流设计器重复计算与序列化

- 缓存节点汇聚模式拓扑分析结果

- 草稿保存复用内容序列化结果
This commit is contained in:
2026-09-04 14:56:20 +08:00
parent c7cac61ce8
commit 386cebf342
5 changed files with 175 additions and 14 deletions

View File

@@ -18,7 +18,7 @@
import {useTinyflowStore} from '#store/stores.svelte';
import NodeJoinModeSetting from './NodeJoinModeSetting.svelte';
import {
analyzeJoinMode,
getCachedJoinModeAnalysis,
getJoinModeBadge,
type JoinMode,
} from '../utils/joinMode';
@@ -64,11 +64,7 @@
const store = useTinyflowStore();
const updateNodeInternals = useUpdateNodeInternals();
const joinModeAnalysis = $derived.by(() => analyzeJoinMode(
store.getNodes(),
store.getEdges(),
id,
));
const joinModeAnalysis = $derived.by(() => getCachedJoinModeAnalysis(store, id));
const joinModeBadge = $derived(getJoinModeBadge(data.joinMode));
const items = $derived.by(() => {

View File

@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
import {
analyzeJoinMode,
getCachedJoinModeAnalysis,
getJoinModeBadge,
parseJoinMode,
} from './joinMode';
@@ -154,4 +155,65 @@ describe('join mode graph analysis', () => {
expect(analysis.invalidMode).toBe(true);
expect(analysis.mode).toBeNull();
});
it('reuses analysis when ordinary node content changes', () => {
let nodes = [
node('start', 'startNode'),
node('join', 'codeNode', {joinMode: 'any', title: 'before'}),
];
const edges = [edge('start-join', 'start', 'join')];
const store = {
getNodes: () => nodes,
getEdges: () => edges,
};
const first = getCachedJoinModeAnalysis(store, 'join');
nodes = [
node('start', 'startNode'),
node('join', 'codeNode', {joinMode: 'any', title: 'after'}),
];
expect(getCachedJoinModeAnalysis(store, 'join')).toBe(first);
});
it('invalidates cached analysis when topology rules change', () => {
let nodes = [
node('start', 'startNode'),
node('join', 'codeNode', {joinMode: 'any'}),
];
const edges = [edge('start-join', 'start', 'join')];
const store = {
getNodes: () => nodes,
getEdges: () => edges,
};
const first = getCachedJoinModeAnalysis(store, 'join');
nodes = [
node('start', 'startNode'),
node('join', 'codeNode', {joinMode: 'all'}),
];
const second = getCachedJoinModeAnalysis(store, 'join');
expect(second).not.toBe(first);
expect(second.mode).toBe('all');
});
it.each([null, ''])(
'invalidates the default cache when joinMode becomes %j',
(joinMode) => {
let nodes = [node('join')];
const store = {
getNodes: () => nodes,
getEdges: () => [] as Edge[],
};
const first = getCachedJoinModeAnalysis(store, 'join');
nodes = [node('join', 'codeNode', {joinMode})];
const second = getCachedJoinModeAnalysis(store, 'join');
expect(second).not.toBe(first);
expect(second.mode).toBeNull();
expect(second.invalidMode).toBe(true);
},
);
});

View File

@@ -13,6 +13,20 @@ export type JoinModeAnalysis = {
allDisabledReason: string;
};
type JoinModeStore = {
getEdges: () => Edge[];
getNodes: () => Node[];
};
type JoinModeCacheEntry = {
analyses: Map<string, JoinModeAnalysis>;
edges: Edge[];
nodes: Node[];
topologySignature: string;
};
const joinModeAnalysisCache = new WeakMap<object, JoinModeCacheEntry>();
const text = (value: unknown) => (value == null ? '' : String(value).trim());
export function parseJoinMode(value: unknown): JoinMode | null {
@@ -109,6 +123,63 @@ export function analyzeJoinMode(
};
}
/**
* Reuses join-mode analysis while node edits leave the graph topology intact.
* XYFlow replaces the nodes/edges arrays on graph changes, so the signature is
* calculated once per store update instead of once per rendered node.
*/
export function getCachedJoinModeAnalysis(
store: JoinModeStore,
nodeId: string,
): JoinModeAnalysis {
const nodes = store.getNodes();
const edges = store.getEdges();
let entry = joinModeAnalysisCache.get(store as object);
if (entry?.nodes !== nodes || entry.edges !== edges) {
const topologySignature = createTopologySignature(nodes, edges);
entry = entry?.topologySignature === topologySignature
? {...entry, nodes, edges}
: {nodes, edges, topologySignature, analyses: new Map()};
joinModeAnalysisCache.set(store as object, entry);
}
const cached = entry.analyses.get(nodeId);
if (cached) return cached;
const analysis = analyzeJoinMode(nodes, edges, nodeId);
entry.analyses.set(nodeId, analysis);
return analysis;
}
function createTopologySignature(nodes: Node[], edges: Edge[]) {
return JSON.stringify([
nodes.map((node) => [
node.id,
node.type,
node.parentId,
joinModeSignature(node),
text(node.data?.condition),
]),
edges.map((edge) => [
edge.source,
edge.target,
text(edge.data?.condition),
]),
]);
}
function joinModeSignature(node: Node) {
const hasJoinMode = Boolean(node.data)
&& Object.prototype.hasOwnProperty.call(node.data, 'joinMode');
const value = node.data?.joinMode;
return [
hasJoinMode,
value === undefined ? 'undefined' : value === null ? 'null' : typeof value,
text(value).toLowerCase(),
];
}
function findGuaranteedNodes(nodes: Node[], edges: Edge[]) {
const guaranteed = new Set(
nodes