mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-13 16:14:07 -08:00
perf(editor): Improve canvas rendering performance (#8022)
## Summary - Refactor usage of `setSuspendDrawing`, removing it from loops and only calling it after batch operations are done - Batch adding of nodes to improve copy/paste and workflow load performance - Cache i18n calls - Debounce connections dragging handler if there are more than 20 nodes ## Related tickets and issues > Include links to **Linear ticket** or Github issue or Community forum post. Important in order to close *automatically* and provide context to reviewers. - https://community.n8n.io/t/slow-ui-in-big-scenarios/33830/8 --------- Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
parent
e3c363d72c
commit
b780436a6b
|
@ -637,8 +637,7 @@ export default defineComponent({
|
||||||
// so we only update it when necessary (when node is mounted and when it's opened and closed (isActive))
|
// so we only update it when necessary (when node is mounted and when it's opened and closed (isActive))
|
||||||
try {
|
try {
|
||||||
const nodeSubtitle =
|
const nodeSubtitle =
|
||||||
this.nodeHelpers.getNodeSubtitle(this.data, this.nodeType, this.getCurrentWorkflow()) ||
|
this.nodeHelpers.getNodeSubtitle(this.data, this.nodeType, this.workflow) || '';
|
||||||
'';
|
|
||||||
|
|
||||||
this.nodeSubtitle = nodeSubtitle.includes(CUSTOM_API_CALL_KEY) ? '' : nodeSubtitle;
|
this.nodeSubtitle = nodeSubtitle.includes(CUSTOM_API_CALL_KEY) ? '' : nodeSubtitle;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -23,6 +23,8 @@ export const i18nInstance = createI18n({
|
||||||
});
|
});
|
||||||
|
|
||||||
export class I18nClass {
|
export class I18nClass {
|
||||||
|
private baseTextCache = new Map<string, string>();
|
||||||
|
|
||||||
private get i18n() {
|
private get i18n() {
|
||||||
return i18nInstance.global;
|
return i18nInstance.global;
|
||||||
}
|
}
|
||||||
|
@ -50,11 +52,25 @@ export class I18nClass {
|
||||||
key: BaseTextKey,
|
key: BaseTextKey,
|
||||||
options?: { adjustToNumber?: number; interpolate?: { [key: string]: string } },
|
options?: { adjustToNumber?: number; interpolate?: { [key: string]: string } },
|
||||||
): string {
|
): string {
|
||||||
if (options?.adjustToNumber !== undefined) {
|
// Create a unique cache key
|
||||||
return this.i18n.tc(key, options.adjustToNumber, options?.interpolate).toString();
|
const cacheKey = `${key}-${JSON.stringify(options)}`;
|
||||||
|
|
||||||
|
// Check if the result is already cached
|
||||||
|
if (this.baseTextCache.has(cacheKey)) {
|
||||||
|
return this.baseTextCache.get(cacheKey) ?? key;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.i18n.t(key, options?.interpolate).toString();
|
let result: string;
|
||||||
|
if (options?.adjustToNumber !== undefined) {
|
||||||
|
result = this.i18n.tc(key, options.adjustToNumber, options?.interpolate ?? {}).toString();
|
||||||
|
} else {
|
||||||
|
result = this.i18n.t(key, options?.interpolate ?? {}).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the result in the cache
|
||||||
|
this.baseTextCache.set(cacheKey, result);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -70,7 +70,6 @@ export const register = () => {
|
||||||
container.appendChild(unconnectedGroup);
|
container.appendChild(unconnectedGroup);
|
||||||
container.appendChild(defaultGroup);
|
container.appendChild(defaultGroup);
|
||||||
|
|
||||||
endpointInstance.setupOverlays();
|
|
||||||
endpointInstance.setVisible(false);
|
endpointInstance.setVisible(false);
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
|
|
|
@ -33,11 +33,6 @@ export class N8nAddInputEndpoint extends EndpointRepresentation<ComputedN8nAddIn
|
||||||
|
|
||||||
type = N8nAddInputEndpoint.type;
|
type = N8nAddInputEndpoint.type;
|
||||||
|
|
||||||
setupOverlays() {
|
|
||||||
this.endpoint.instance.setSuspendDrawing(true);
|
|
||||||
this.endpoint.instance.setSuspendDrawing(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
this.instance.bind(EVENT_ENDPOINT_CLICK, this.fireClickEvent);
|
this.instance.bind(EVENT_ENDPOINT_CLICK, this.fireClickEvent);
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,6 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
||||||
|
|
||||||
setupOverlays() {
|
setupOverlays() {
|
||||||
this.clearOverlays();
|
this.clearOverlays();
|
||||||
this.endpoint.instance.setSuspendDrawing(true);
|
|
||||||
this.stalkOverlay = this.endpoint.addOverlay({
|
this.stalkOverlay = this.endpoint.addOverlay({
|
||||||
type: 'Custom',
|
type: 'Custom',
|
||||||
options: {
|
options: {
|
||||||
|
@ -78,7 +77,6 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.endpoint.instance.setSuspendDrawing(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
|
@ -151,18 +149,14 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsVisible(visible: boolean) {
|
setIsVisible(visible: boolean) {
|
||||||
this.instance.setSuspendDrawing(true);
|
|
||||||
Object.keys(this.endpoint.getOverlays()).forEach((overlay) => {
|
Object.keys(this.endpoint.getOverlays()).forEach((overlay) => {
|
||||||
this.endpoint.getOverlays()[overlay].setVisible(visible);
|
this.endpoint.getOverlays()[overlay].setVisible(visible);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setVisible(visible);
|
this.setVisible(visible);
|
||||||
|
|
||||||
// Re-trigger the success state if label is set
|
// Re-trigger the success state if label is set
|
||||||
if (visible && this.label) {
|
if (visible && this.label) {
|
||||||
this.setSuccessOutput(this.label);
|
this.setSuccessOutput(this.label);
|
||||||
}
|
}
|
||||||
this.instance.setSuspendDrawing(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setSuccessOutput(label: string) {
|
setSuccessOutput(label: string) {
|
||||||
|
|
|
@ -894,8 +894,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.workflowExecutionPairedItemMappings = getPairedItemsMapping(this.workflowExecutionData);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
resetAllNodesIssues(): boolean {
|
resetAllNodesIssues(): boolean {
|
||||||
|
@ -1130,7 +1128,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
this.workflowExecutionData.data!.resultData.runData[pushData.nodeName].push(pushData.data);
|
this.workflowExecutionData.data!.resultData.runData[pushData.nodeName].push(pushData.data);
|
||||||
this.workflowExecutionPairedItemMappings = getPairedItemsMapping(this.workflowExecutionData);
|
|
||||||
},
|
},
|
||||||
clearNodeExecutionData(nodeName: string): void {
|
clearNodeExecutionData(nodeName: string): void {
|
||||||
if (!this.workflowExecutionData?.data) {
|
if (!this.workflowExecutionData?.data) {
|
||||||
|
|
|
@ -75,6 +75,7 @@
|
||||||
@removeNode="(name) => removeNode(name, true)"
|
@removeNode="(name) => removeNode(name, true)"
|
||||||
:key="`${stickyData.id}_sticky`"
|
:key="`${stickyData.id}_sticky`"
|
||||||
:name="stickyData.name"
|
:name="stickyData.name"
|
||||||
|
:workflow="currentWorkflowObject"
|
||||||
:isReadOnly="isReadOnlyRoute || readOnlyEnv"
|
:isReadOnly="isReadOnlyRoute || readOnlyEnv"
|
||||||
:instance="instance"
|
:instance="instance"
|
||||||
:isActive="!!activeNode && activeNode.name === stickyData.name"
|
:isActive="!!activeNode && activeNode.name === stickyData.name"
|
||||||
|
@ -732,6 +733,7 @@ export default defineComponent({
|
||||||
showTriggerMissingTooltip: false,
|
showTriggerMissingTooltip: false,
|
||||||
workflowData: null as INewWorkflowData | null,
|
workflowData: null as INewWorkflowData | null,
|
||||||
activeConnection: null as null | Connection,
|
activeConnection: null as null | Connection,
|
||||||
|
isInsertingNodes: false,
|
||||||
isProductionExecutionPreview: false,
|
isProductionExecutionPreview: false,
|
||||||
enterTimer: undefined as undefined | ReturnType<typeof setTimeout>,
|
enterTimer: undefined as undefined | ReturnType<typeof setTimeout>,
|
||||||
exitTimer: undefined as undefined | ReturnType<typeof setTimeout>,
|
exitTimer: undefined as undefined | ReturnType<typeof setTimeout>,
|
||||||
|
@ -2735,15 +2737,6 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
setTimeout(() => {
|
|
||||||
NodeViewUtils.addConnectionTestData(
|
|
||||||
info.source,
|
|
||||||
info.target,
|
|
||||||
info.connection?.connector?.hasOwnProperty('canvas')
|
|
||||||
? (info.connection.connector.canvas as HTMLElement)
|
|
||||||
: undefined,
|
|
||||||
);
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
const endpointArrow = NodeViewUtils.getOverlay(
|
const endpointArrow = NodeViewUtils.getOverlay(
|
||||||
info.connection,
|
info.connection,
|
||||||
|
@ -2763,14 +2756,44 @@ export default defineComponent({
|
||||||
if (!this.suspendRecordingDetachedConnections) {
|
if (!this.suspendRecordingDetachedConnections) {
|
||||||
this.historyStore.pushCommandToUndo(new AddConnectionCommand(connectionData));
|
this.historyStore.pushCommandToUndo(new AddConnectionCommand(connectionData));
|
||||||
}
|
}
|
||||||
|
// When we add multiple nodes, this event could be fired hundreds of times for large workflows.
|
||||||
|
// And because the updateNodesInputIssues() method is quite expensive, we only call it if not in insert mode
|
||||||
|
if (!this.isInsertingNodes) {
|
||||||
this.nodeHelpers.updateNodesInputIssues();
|
this.nodeHelpers.updateNodesInputIssues();
|
||||||
this.resetEndpointsErrors();
|
this.resetEndpointsErrors();
|
||||||
|
setTimeout(() => {
|
||||||
|
NodeViewUtils.addConnectionTestData(
|
||||||
|
info.source,
|
||||||
|
info.target,
|
||||||
|
info.connection?.connector?.hasOwnProperty('canvas')
|
||||||
|
? (info.connection.connector.canvas as HTMLElement)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
addConectionsTestData() {
|
||||||
|
this.instance.connections.forEach((connection) => {
|
||||||
|
NodeViewUtils.addConnectionTestData(
|
||||||
|
connection.source,
|
||||||
|
connection.target,
|
||||||
|
connection?.connector?.hasOwnProperty('canvas')
|
||||||
|
? (connection?.connector.canvas as HTMLElement)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
onDragMove() {
|
onDragMove() {
|
||||||
|
const totalNodes = this.nodes.length;
|
||||||
|
void this.callDebounced('updateConnectionsOverlays', {
|
||||||
|
debounceTime: totalNodes > 20 ? 200 : 0,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateConnectionsOverlays() {
|
||||||
this.instance?.connections.forEach((connection) => {
|
this.instance?.connections.forEach((connection) => {
|
||||||
NodeViewUtils.showOrHideItemsLabel(connection);
|
NodeViewUtils.showOrHideItemsLabel(connection);
|
||||||
NodeViewUtils.showOrHideMidpointArrow(connection);
|
NodeViewUtils.showOrHideMidpointArrow(connection);
|
||||||
|
@ -3899,7 +3922,7 @@ export default defineComponent({
|
||||||
if (!nodes?.length) {
|
if (!nodes?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.isInsertingNodes = true;
|
||||||
// Before proceeding we must check if all nodes contain the `properties` attribute.
|
// Before proceeding we must check if all nodes contain the `properties` attribute.
|
||||||
// Nodes are loaded without this information so we must make sure that all nodes
|
// Nodes are loaded without this information so we must make sure that all nodes
|
||||||
// being added have this information.
|
// being added have this information.
|
||||||
|
@ -3957,60 +3980,64 @@ export default defineComponent({
|
||||||
|
|
||||||
// check and match credentials, apply new format if old is used
|
// check and match credentials, apply new format if old is used
|
||||||
this.matchCredentials(node);
|
this.matchCredentials(node);
|
||||||
|
|
||||||
this.workflowsStore.addNode(node);
|
this.workflowsStore.addNode(node);
|
||||||
if (trackHistory) {
|
if (trackHistory) {
|
||||||
this.historyStore.pushCommandToUndo(new AddNodeCommand(node));
|
this.historyStore.pushCommandToUndo(new AddNodeCommand(node));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for the node to be rendered
|
// Wait for the nodes to be rendered
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
|
|
||||||
// Suspend drawing
|
|
||||||
this.instance?.setSuspendDrawing(true);
|
this.instance?.setSuspendDrawing(true);
|
||||||
|
|
||||||
// Load the connections
|
if (connections) {
|
||||||
if (connections !== undefined) {
|
await this.addConnections(connections);
|
||||||
let connectionData;
|
|
||||||
for (const sourceNode of Object.keys(connections)) {
|
|
||||||
for (const type of Object.keys(connections[sourceNode])) {
|
|
||||||
for (
|
|
||||||
let sourceIndex = 0;
|
|
||||||
sourceIndex < connections[sourceNode][type].length;
|
|
||||||
sourceIndex++
|
|
||||||
) {
|
|
||||||
const outwardConnections = connections[sourceNode][type][sourceIndex];
|
|
||||||
if (!outwardConnections) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
outwardConnections.forEach((targetData) => {
|
|
||||||
connectionData = [
|
|
||||||
{
|
|
||||||
node: sourceNode,
|
|
||||||
type,
|
|
||||||
index: sourceIndex,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
node: targetData.node,
|
|
||||||
type: targetData.type,
|
|
||||||
index: targetData.index,
|
|
||||||
},
|
|
||||||
] as [IConnection, IConnection];
|
|
||||||
|
|
||||||
this.__addConnection(connectionData);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the node issues at the end as the node-connections are required
|
// Add the node issues at the end as the node-connections are required
|
||||||
void this.nodeHelpers.refreshNodeIssues();
|
this.nodeHelpers.refreshNodeIssues();
|
||||||
|
this.nodeHelpers.updateNodesInputIssues();
|
||||||
|
this.resetEndpointsErrors();
|
||||||
|
this.isInsertingNodes = false;
|
||||||
|
|
||||||
// Now it can draw again
|
// Now it can draw again
|
||||||
this.instance?.setSuspendDrawing(false, true);
|
this.instance?.setSuspendDrawing(false, true);
|
||||||
},
|
},
|
||||||
|
async addConnections(connections: IConnections) {
|
||||||
|
const batchedConnectionData: Array<[IConnection, IConnection]> = [];
|
||||||
|
|
||||||
|
for (const sourceNode in connections) {
|
||||||
|
for (const type in connections[sourceNode]) {
|
||||||
|
connections[sourceNode][type].forEach((outwardConnections, sourceIndex) => {
|
||||||
|
if (outwardConnections) {
|
||||||
|
outwardConnections.forEach((targetData) => {
|
||||||
|
batchedConnectionData.push([
|
||||||
|
{ node: sourceNode, type, index: sourceIndex },
|
||||||
|
{ node: targetData.node, type: targetData.type, index: targetData.index },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the connections in batches
|
||||||
|
await this.processConnectionBatch(batchedConnectionData);
|
||||||
|
setTimeout(this.addConectionsTestData, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
async processConnectionBatch(batchedConnectionData: Array<[IConnection, IConnection]>) {
|
||||||
|
const batchSize = 100;
|
||||||
|
|
||||||
|
for (let i = 0; i < batchedConnectionData.length; i += batchSize) {
|
||||||
|
const batch = batchedConnectionData.slice(i, i + batchSize);
|
||||||
|
|
||||||
|
batch.forEach((connectionData) => {
|
||||||
|
this.__addConnection(connectionData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async addNodesToWorkflow(data: IWorkflowDataUpdate): Promise<IWorkflowDataUpdate> {
|
async addNodesToWorkflow(data: IWorkflowDataUpdate): Promise<IWorkflowDataUpdate> {
|
||||||
// Because nodes with the same name maybe already exist, it could
|
// Because nodes with the same name maybe already exist, it could
|
||||||
// be needed that they have to be renamed. Also could it be possible
|
// be needed that they have to be renamed. Also could it be possible
|
||||||
|
|
Loading…
Reference in a new issue