perf: 优化智能体与工作流幕布渲染性能

- 分阶段加载智能体配置并按需缓存 MCP 工具

- 合并画布状态更新与节点尺寸监听,启用大图可视区域渲染和静态连线

- 隔离 Tinyflow Store 实例并补充数据同步与回归测试
This commit is contained in:
2026-07-27 18:27:01 +08:00
parent dc7e46260b
commit aedefe6b5e
39 changed files with 1349 additions and 408 deletions

View File

@@ -3,7 +3,9 @@
</template>
<script setup lang="ts">
import {Tinyflow as TinyflowNative, TinyflowOptions} from '@tinyflow-ai/ui';
import type { TinyflowOptions } from '@tinyflow-ai/ui';
import { Tinyflow as TinyflowNative } from '@tinyflow-ai/ui';
import '@tinyflow-ai/ui/dist/index.css';
import {
getCurrentInstance,
@@ -15,8 +17,7 @@ import {
watch,
} from 'vue';
type TinyflowDataOption = Exclude<TinyflowOptions['data'], string | undefined>;
type StructuredCloneFn = <T>(value: T) => T;
import { createTinyflowDataSync } from './dataSync';
const props = defineProps<
{
@@ -30,7 +31,7 @@ const attrs = useAttrs();
const instance = getCurrentInstance();
let tinyflow: TinyflowNative | null = null;
let mountedDataReady = false;
let lastAppliedDataSignature = '';
const dataSync = createTinyflowDataSync();
const optionalBooleanOptionKeys = new Set([
'readonly',
'hideBottomDock',
@@ -45,6 +46,7 @@ const optionalBooleanOptionKeys = new Set([
'nodesDraggable',
'nodesConnectable',
'elementsSelectable',
'onlyRenderVisibleElements',
'dropEnabled',
]);
@@ -81,58 +83,6 @@ function normalizeProps(source: Record<string, unknown>) {
);
}
// 安全深拷贝工具函数
function safeDeepClone<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') return obj;
try {
const clone = (globalThis as { structuredClone?: StructuredCloneFn })
.structuredClone;
if (clone) {
return clone(obj);
}
} catch {
// Fall through to JSON cloning for browsers without native structuredClone.
}
try {
return JSON.parse(JSON.stringify(obj));
} catch {
try {
console.warn(
'Failed to clone object, returning original (may cause issues)',
obj,
);
return obj;
} catch {
return obj;
}
}
}
function createDataSignature(data: unknown) {
if (data == null || typeof data === 'string') {
return String(data ?? '');
}
try {
return JSON.stringify(data);
} catch {
return '';
}
}
function cloneDataIfChanged(data: TinyflowOptions['data']) {
if (data == null || typeof data === 'string') {
return null;
}
const signature = createDataSignature(data);
if (signature && signature === lastAppliedDataSignature) {
return null;
}
lastAppliedDataSignature = signature;
return safeDeepClone(data as TinyflowDataOption);
}
onMounted(() => {
if (divRef.value) {
// 净化 props.data避免响应式对象或函数污染
@@ -141,8 +91,21 @@ onMounted(() => {
...normalizeProps(props),
} as any;
if ('data' in cleanedProps && cleanedProps.data != null) {
cleanedProps.data = cloneDataIfChanged(cleanedProps.data);
cleanedProps.data = dataSync.cloneExternalDataIfChanged(cleanedProps.data);
}
(['onDataChange', 'onDataCommit'] as const).forEach((callbackKey) => {
const callback = cleanedProps[callbackKey];
if (typeof callback !== 'function') {
return;
}
cleanedProps[callbackKey] = (
data: TinyflowOptions['data'],
...args: unknown[]
) => {
dataSync.markNativeData(data);
callback(data, ...args);
};
});
tinyflow = new TinyflowNative({
...cleanedProps,
@@ -158,7 +121,7 @@ onUnmounted(() => {
tinyflow = null;
}
mountedDataReady = false;
lastAppliedDataSignature = '';
dataSync.reset();
});
watch(
@@ -179,7 +142,7 @@ watch(
data != null &&
typeof data !== 'string'
) {
const clonedData = cloneDataIfChanged(data);
const clonedData = dataSync.cloneExternalDataIfChanged(data);
if (clonedData) {
tinyflow.updateData(clonedData, { preserveViewport: true });
}
@@ -229,7 +192,7 @@ const updateData = (
options?: { preserveViewport?: boolean },
) => {
if (tinyflow && data != null && typeof data !== 'string') {
const clonedData = cloneDataIfChanged(data);
const clonedData = dataSync.cloneExternalDataIfChanged(data);
if (!clonedData) {
return true;
}

View File

@@ -0,0 +1,76 @@
import { describe, expect, it } from 'vitest';
import { createTinyflowDataSync } from './dataSync';
describe('createTinyflowDataSync', () => {
it('克隆真正的外部数据并跳过相同内容', () => {
const sync = createTinyflowDataSync();
const data = {
edges: [],
nodes: [{ data: { title: '开始' }, id: 'start', position: { x: 0, y: 0 } }],
};
const cloned = sync.cloneExternalDataIfChanged(data);
expect(cloned).toEqual(data);
expect(cloned).not.toBe(data);
expect(sync.cloneExternalDataIfChanged(data)).toBeNull();
});
it('跳过原生画布回传后由父组件形成的属性回声', () => {
const sync = createTinyflowDataSync();
const emitted = {
edges: [],
nodes: [{ data: { title: '开始' }, id: 'start', position: { x: 12, y: 8 } }],
viewport: { x: 20, y: 30, zoom: 1 },
};
sync.markNativeData(emitted);
expect(
sync.cloneExternalDataIfChanged(structuredClone(emitted)),
).toBeNull();
});
it('原生回传后仍应用内容不同的外部更新', () => {
const sync = createTinyflowDataSync();
const emitted = {
edges: [],
nodes: [{ data: { title: '开始' }, id: 'start', position: { x: 12, y: 8 } }],
};
sync.markNativeData(emitted);
const external = {
...emitted,
nodes: [
{
...emitted.nodes[0],
data: { title: '已更新' },
},
],
};
expect(sync.cloneExternalDataIfChanged(external)).toEqual(external);
});
it('画布变化后允许重新应用曾经加载过的外部版本', () => {
const sync = createTinyflowDataSync();
const initial = {
edges: [],
nodes: [{ data: { title: '初始' }, id: 'start', position: { x: 0, y: 0 } }],
};
expect(sync.cloneExternalDataIfChanged(initial)).toEqual(initial);
sync.markNativeData({
...initial,
nodes: [
{
...initial.nodes[0],
position: { x: 120, y: 80 },
},
],
});
expect(sync.cloneExternalDataIfChanged(initial)).toEqual(initial);
});
});

View File

@@ -0,0 +1,86 @@
import type { TinyflowOptions } from '@tinyflow-ai/ui';
type TinyflowDataOption = Exclude<
TinyflowOptions['data'],
string | undefined
>;
type StructuredCloneFn = <T>(value: T) => T;
function createDataSignature(data: unknown) {
if (data == null || typeof data === 'string') {
return String(data ?? '');
}
try {
return JSON.stringify(data);
} catch {
return '';
}
}
function safeDeepClone<T>(value: T): T {
if (value === null || typeof value !== 'object') {
return value;
}
try {
const clone = (globalThis as { structuredClone?: StructuredCloneFn })
.structuredClone;
if (clone) {
return clone(value);
}
} catch {
// Fall through to JSON cloning for browsers without native structuredClone.
}
try {
return JSON.parse(JSON.stringify(value));
} catch {
console.warn(
'Failed to clone object, returning original (may cause issues)',
value,
);
return value;
}
}
/**
* 维护 Vue 属性数据与 Tinyflow 原生数据之间的同步边界。
*
* 原生画布回传的数据会被父组件再次作为属性传入。延迟到属性更新时再比较
* 两侧签名,可以阻止同一份画布状态被重新深拷贝并整图应用,同时避免在
* 每一帧提前序列化完整画布。
*/
export function createTinyflowDataSync() {
let lastAppliedDataSignature = '';
let lastNativeData: TinyflowOptions['data'] | null = null;
return {
cloneExternalDataIfChanged(data: TinyflowOptions['data']) {
if (data == null || typeof data === 'string') {
return null;
}
const signature = createDataSignature(data);
if (lastNativeData != null) {
const nativeSignature = createDataSignature(lastNativeData);
lastNativeData = null;
if (signature && signature === nativeSignature) {
lastAppliedDataSignature = signature;
return null;
}
lastAppliedDataSignature = '';
}
if (signature && signature === lastAppliedDataSignature) {
return null;
}
lastAppliedDataSignature = signature;
return safeDeepClone(data as TinyflowDataOption);
},
markNativeData(data: TinyflowOptions['data']) {
lastNativeData = data;
},
reset() {
lastAppliedDataSignature = '';
lastNativeData = null;
},
};
}