From 04ab9968427f9363f6a773172a2eba00ac044b74 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 10:04:23 +0000 Subject: [PATCH 1/2] perf(configurator): rAF-coalesce preview iframe writes during rapid override changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SL-020: PreviewPanel.svelte's two $effects that write to the preview iframe(s) and call bumpPreviewVersion() re-run on every overrides tick — e.g. every input event while dragging a slider, often several times per animation frame. Deferred the DOM-writing work (style/attribute mutation, font injection, resolver cache invalidation) inside each effect to a requestAnimationFrame callback that captures the latest values, cancelling any not-yet-fired frame from a superseded run via the effect's cleanup. This collapses a same-frame burst down to one apply using the last state, instead of one apply per keystroke. Reactive dependency tracking is unaffected — overrides/previewTheme/ loadCount/etc. are still read synchronously at the top of each effect, so Svelte's re-run triggering is unchanged; only the actual DOM writes are deferred. previewResolver.svelte.ts's bumpPreviewVersion() itself is untouched, preserving its existing "pure write, no reactive read" invariant. Verified via a manual rapid-drag simulation against a real built preview server (40 input events fired faster than one per frame): zero console/ page errors (no effect_update_depth loop), and the preview iframe's applied CSS exactly matched the last dragged value with no dropped trailing frame. --- .../src/components/shell/PreviewPanel.svelte | 107 +++++++++++------- 1 file changed, 64 insertions(+), 43 deletions(-) diff --git a/configurator/src/components/shell/PreviewPanel.svelte b/configurator/src/components/shell/PreviewPanel.svelte index 06d61482..5f76d003 100644 --- a/configurator/src/components/shell/PreviewPanel.svelte +++ b/configurator/src/components/shell/PreviewPanel.svelte @@ -481,35 +481,51 @@ ${BODIES[template]} let htmlLight = $derived(buildIframeHTML({}, "light", previewMotion, previewTemplate, frameworkCSSStatic)); let htmlDark = $derived(buildIframeHTML({}, "dark", previewMotion, previewTemplate, frameworkCSSStatic)); + // SL-020: the DOM writes below (style/attribute mutation on the iframe + // document, plus bumpPreviewVersion()'s resolveCache.clear()) are cheap + // individually but these effects re-run on every override tick — e.g. every + // input event while dragging a slider, many times per animation frame. + // rAF-coalescing collapses a burst of same-frame reruns into a single + // apply using the latest captured values ("coalesce to latest"), since only + // the frame actually painted to the user matters. The effect body above + // this comment still runs synchronously on every change (so Svelte's + // dependency tracking is unaffected) — only the DOM-writing work below is + // deferred. The `return () => cancelAnimationFrame(rafId)` cleanup fires + // before Svelte re-runs this effect (or on unmount), so a still-pending + // frame from a superseded run is cancelled before scheduling the next one. $effect(() => { const _ov = overrides; const _theme = previewTheme; const _count = loadCount; const _lock = lumlockerPreview.value; - const iframe = iframeEl; - if (_count === 0 || !iframe) return; - const doc = iframe.contentDocument; - if (!doc) return; + const rafId = requestAnimationFrame(() => { + const iframe = iframeEl; + if (_count === 0 || !iframe) return; + const doc = iframe.contentDocument; + if (!doc) return; - const styleEl = doc.getElementById("slashed-overrides"); - if (styleEl) { - styleEl.textContent = generateCSS(withDerivedOverrides(_ov), { mode: "root", banner: false }); - } + const styleEl = doc.getElementById("slashed-overrides"); + if (styleEl) { + styleEl.textContent = generateCSS(withDerivedOverrides(_ov), { mode: "root", banner: false }); + } + + injectFontsIntoDoc(doc, _ov); - injectFontsIntoDoc(doc, _ov); + // Framework activates dark mode via [data-theme="dark"] (color-scheme + + // light-dark()), NOT a class — keep this in sync with buildIframeHTML. + doc.documentElement.setAttribute("data-theme", _theme); - // Framework activates dark mode via [data-theme="dark"] (color-scheme + - // light-dark()), NOT a class — keep this in sync with buildIframeHTML. - doc.documentElement.setAttribute("data-theme", _theme); + // Luminance lock preview — mirrors :root[data-lumlocker] in core/themes.css. + if (_lock) doc.documentElement.setAttribute("data-lumlocker", ""); + else doc.documentElement.removeAttribute("data-lumlocker"); - // Luminance lock preview — mirrors :root[data-lumlocker] in core/themes.css. - if (_lock) doc.documentElement.setAttribute("data-lumlocker", ""); - else doc.documentElement.removeAttribute("data-lumlocker"); + // This single-mode iframe is the canonical resolver source. + registerPreviewDoc(doc); + bumpPreviewVersion(); + }); - // This single-mode iframe is the canonical resolver source. - registerPreviewDoc(doc); - bumpPreviewVersion(); + return () => cancelAnimationFrame(rafId); }); $effect(() => { @@ -517,34 +533,39 @@ ${BODIES[template]} const _lightCount = splitLightLoadCount; const _darkCount = splitDarkLoadCount; const _lock = lumlockerPreview.value; - const css = generateCSS(withDerivedOverrides(_ov), { mode: "root", banner: false }); - const applyLock = (doc: Document) => { - if (_lock) doc.documentElement.setAttribute("data-lumlocker", ""); - else doc.documentElement.removeAttribute("data-lumlocker"); - }; - - if (splitLightEl && _lightCount > 0) { - const doc = splitLightEl.contentDocument; - if (doc) { - const styleEl = doc.getElementById("slashed-overrides"); - if (styleEl) styleEl.textContent = css; - injectFontsIntoDoc(doc, _ov); - applyLock(doc); - // In split mode the light pane is the canonical resolver source. - registerPreviewDoc(doc); + const rafId = requestAnimationFrame(() => { + const css = generateCSS(withDerivedOverrides(_ov), { mode: "root", banner: false }); + + const applyLock = (doc: Document) => { + if (_lock) doc.documentElement.setAttribute("data-lumlocker", ""); + else doc.documentElement.removeAttribute("data-lumlocker"); + }; + + if (splitLightEl && _lightCount > 0) { + const doc = splitLightEl.contentDocument; + if (doc) { + const styleEl = doc.getElementById("slashed-overrides"); + if (styleEl) styleEl.textContent = css; + injectFontsIntoDoc(doc, _ov); + applyLock(doc); + // In split mode the light pane is the canonical resolver source. + registerPreviewDoc(doc); + } } - } - if (splitDarkEl && _darkCount > 0) { - const doc = splitDarkEl.contentDocument; - if (doc) { - const styleEl = doc.getElementById("slashed-overrides"); - if (styleEl) styleEl.textContent = css; - injectFontsIntoDoc(doc, _ov); - applyLock(doc); + if (splitDarkEl && _darkCount > 0) { + const doc = splitDarkEl.contentDocument; + if (doc) { + const styleEl = doc.getElementById("slashed-overrides"); + if (styleEl) styleEl.textContent = css; + injectFontsIntoDoc(doc, _ov); + applyLock(doc); + } } - } - bumpPreviewVersion(); + bumpPreviewVersion(); + }); + + return () => cancelAnimationFrame(rafId); }); let isConstrained = $derived(previewWidth !== "fluid"); From 0515efa864050e24096a37014ef6b9035fea31a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 10:14:37 +0000 Subject: [PATCH 2/2] fix(configurator): remove redundant previewVersion bumps and skip split-mode effect when inactive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Qodo review on PR6 flagged two real, pre-existing (not introduced by the rAF-coalescing itself) inefficiencies in the two effects this PR touches: 1. The single-iframe effect called bumpPreviewVersion() explicitly right after registerPreviewDoc(doc), but registerPreviewDoc() already bumps internally on every path (both its activeDoc-unchanged early-return and its replace-doc branch) — the explicit call double-bumped previewVersion and double-cleared resolveCache on every apply. 2. The split-mode effect unconditionally scheduled an rAF, computed CSS, and bumped previewVersion even when splitMode is false and no split iframes exist in the DOM (they're gated behind {#if splitMode}), making every apply pure overhead outside split mode. Fixed both: dropped the explicit bump in the single-iframe effect, added an early return on !splitMode in the split effect, and made its trailing bump conditional (only fires when the dark pane updated without the light pane also updating, since registerPreviewDoc's internal bump already covers the light-pane case) so consumers are still notified exactly once whenever a split pane actually changed. Re-verified via the same rapid-drag simulation as the original PR6 commit, now covering both single- and split-preview modes: zero console/ page errors, and both modes' applied CSS exactly matched the last dragged value. --- .../src/components/shell/PreviewPanel.svelte | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/configurator/src/components/shell/PreviewPanel.svelte b/configurator/src/components/shell/PreviewPanel.svelte index 5f76d003..fee78000 100644 --- a/configurator/src/components/shell/PreviewPanel.svelte +++ b/configurator/src/components/shell/PreviewPanel.svelte @@ -521,19 +521,28 @@ ${BODIES[template]} else doc.documentElement.removeAttribute("data-lumlocker"); // This single-mode iframe is the canonical resolver source. + // registerPreviewDoc() always bumps internally (both its + // activeDoc-unchanged early-return and its replace-doc path do), so an + // explicit bumpPreviewVersion() here would double-bump/double-clear + // resolveCache for no reason. registerPreviewDoc(doc); - bumpPreviewVersion(); }); return () => cancelAnimationFrame(rafId); }); $effect(() => { + const _splitMode = splitMode; const _ov = overrides; const _lightCount = splitLightLoadCount; const _darkCount = splitDarkLoadCount; const _lock = lumlockerPreview.value; + // Split iframes only exist in the DOM under {#if splitMode} below — skip + // scheduling entirely rather than doing per-frame no-op work while the + // single-iframe effect above is the one actually driving the preview. + if (!_splitMode) return; + const rafId = requestAnimationFrame(() => { const css = generateCSS(withDerivedOverrides(_ov), { mode: "root", banner: false }); @@ -542,6 +551,12 @@ ${BODIES[template]} else doc.documentElement.removeAttribute("data-lumlocker"); }; + // registerPreviewDoc() (light pane) already bumps internally; only + // bump explicitly when the dark pane was the sole one updated this + // frame, so consumers still get notified without double-bumping. + let lightApplied = false; + let darkApplied = false; + if (splitLightEl && _lightCount > 0) { const doc = splitLightEl.contentDocument; if (doc) { @@ -551,6 +566,7 @@ ${BODIES[template]} applyLock(doc); // In split mode the light pane is the canonical resolver source. registerPreviewDoc(doc); + lightApplied = true; } } if (splitDarkEl && _darkCount > 0) { @@ -560,9 +576,10 @@ ${BODIES[template]} if (styleEl) styleEl.textContent = css; injectFontsIntoDoc(doc, _ov); applyLock(doc); + darkApplied = true; } } - bumpPreviewVersion(); + if (darkApplied && !lightApplied) bumpPreviewVersion(); }); return () => cancelAnimationFrame(rafId);