From 8d003a1d212c92b4a712d9d761557f1f34f767fc Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Wed, 18 Mar 2026 19:38:53 +1100 Subject: [PATCH] perf: tidy svg frame bg render pass --- .../excalidraw/renderer/staticSvgScene.ts | 130 ++++++++++-------- 1 file changed, 76 insertions(+), 54 deletions(-) diff --git a/packages/excalidraw/renderer/staticSvgScene.ts b/packages/excalidraw/renderer/staticSvgScene.ts index 2a344e29de..4858896df7 100644 --- a/packages/excalidraw/renderer/staticSvgScene.ts +++ b/packages/excalidraw/renderer/staticSvgScene.ts @@ -720,26 +720,15 @@ export const renderSceneToSvg = ( return; } - const renderedFrameBackgrounds = new Set(); - const maybeRenderFrameBackground = ( - element: NonDeletedExcalidrawElement | ExcalidrawFrameLikeElement, - ) => { - if ( - !renderConfig.frameRendering.enabled || - (!renderConfig.frameRendering.outline && !renderConfig.exportingFrame) - ) { - return; - } - - const frame = - renderConfig.exportingFrame || - (isFrameLikeElement(element) - ? element - : getContainingFrame(element, elementsMap)); + const nonIframeElements = elements.filter((el) => !isIframeLikeElement(el)); + const frameBackgroundByElementId = new Map< + ExcalidrawElement["id"], + ExcalidrawFrameLikeElement + >(); + const renderFrameBackgroundNode = (frame: ExcalidrawFrameLikeElement) => { if ( !frame || - renderedFrameBackgrounds.has(frame.id) || !frame.backgroundColor || isTransparent(frame.backgroundColor) ) { @@ -777,58 +766,91 @@ export const renderSceneToSvg = ( rect.setAttribute("stroke", "none"); svgRoot.appendChild(rect); - renderedFrameBackgrounds.add(frame.id); }; - if (renderConfig.exportingFrame) { - maybeRenderFrameBackground(renderConfig.exportingFrame); + if ( + renderConfig.frameRendering.enabled && + (renderConfig.frameRendering.outline || renderConfig.exportingFrame) + ) { + const renderedFrameBackgrounds = new Set(); + if (renderConfig.exportingFrame) { + renderFrameBackgroundNode(renderConfig.exportingFrame); + renderedFrameBackgrounds.add(renderConfig.exportingFrame.id); + } + + const maybeQueueFrameBackground = ( + element: NonDeletedExcalidrawElement | ExcalidrawFrameLikeElement, + ) => { + const frame = isFrameLikeElement(element) + ? element + : getContainingFrame(element, elementsMap); + + if ( + !frame || + renderedFrameBackgrounds.has(frame.id) || + !frame.backgroundColor || + isTransparent(frame.backgroundColor) + ) { + return; + } + + frameBackgroundByElementId.set(element.id, frame); + renderedFrameBackgrounds.add(frame.id); + }; + + nonIframeElements.forEach((element) => { + if (!element.isDeleted) { + maybeQueueFrameBackground(element); + } + }); } // render elements - elements - .filter((el) => !isIframeLikeElement(el)) - .forEach((element) => { - if (!element.isDeleted) { - maybeRenderFrameBackground(element); - if ( - isTextElement(element) && - element.containerId && - elementsMap.has(element.containerId) - ) { - // will be rendered with the container - return; - } + nonIframeElements.forEach((element) => { + if (!element.isDeleted) { + const frameBackground = frameBackgroundByElementId.get(element.id); + if (frameBackground) { + renderFrameBackgroundNode(frameBackground); + } + if ( + isTextElement(element) && + element.containerId && + elementsMap.has(element.containerId) + ) { + // will be rendered with the container + return; + } - try { + try { + renderElementToSvg( + element, + elementsMap, + rsvg, + svgRoot, + files, + element.x + renderConfig.offsetX, + element.y + renderConfig.offsetY, + renderConfig, + ); + + const boundTextElement = getBoundTextElement(element, elementsMap); + if (boundTextElement) { renderElementToSvg( - element, + boundTextElement, elementsMap, rsvg, svgRoot, files, - element.x + renderConfig.offsetX, - element.y + renderConfig.offsetY, + boundTextElement.x + renderConfig.offsetX, + boundTextElement.y + renderConfig.offsetY, renderConfig, ); - - const boundTextElement = getBoundTextElement(element, elementsMap); - if (boundTextElement) { - renderElementToSvg( - boundTextElement, - elementsMap, - rsvg, - svgRoot, - files, - boundTextElement.x + renderConfig.offsetX, - boundTextElement.y + renderConfig.offsetY, - renderConfig, - ); - } - } catch (error: any) { - console.error(error); } + } catch (error: any) { + console.error(error); } - }); + } + }); // render embeddables on top elements