perf: 优化智能体与工作流幕布渲染性能
- 分阶段加载智能体配置并按需缓存 MCP 工具 - 合并画布状态更新与节点尺寸监听,启用大图可视区域渲染和静态连线 - 隔离 Tinyflow Store 实例并补充数据同步与回归测试
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
76
easyflow-ui-admin/packages/tinyflow-vue/src/dataSync.test.ts
Normal file
76
easyflow-ui-admin/packages/tinyflow-vue/src/dataSync.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
86
easyflow-ui-admin/packages/tinyflow-vue/src/dataSync.ts
Normal file
86
easyflow-ui-admin/packages/tinyflow-vue/src/dataSync.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user