From 078c493448cf73241bd9eb885a206ea1ccd951cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 28 Sep 2026 04:03:36 +0000 Subject: [PATCH] perf: the preview loads and decodes only the images of scenes on screen or due next (#4578) * perf(core): skip hidden clips' content so the browser stops decoding their images Hidden timed clips get content-visibility:hidden while their inline visibility is hidden, from script evaluation on. A clip due within the look-ahead (2 s by default) keeps its content live, so its images load before it shows. * perf(studio-server): load preview images lazily so hidden scenes fetch nothing * fix(core): stop the readiness scan waiting on and decoding later scenes' images * perf(studio-server): keep first-frame images eager so the preview opens as fast as before Only images inside a clip with a numeric start after 0 go lazy; a start the route cannot read stays eager. * perf(core): keep clips that start at 0 laid out before the first pass so the first frame is on time * perf(core): open the preview on time by hiding only lazy images before the first pass GSAP reads the computed style of the elements it animates while timelines build. Inside a content-skipped clip every read forces a style update, which cost about 65 ms at open on the bench composition. Before the first pass the rule now takes only lazy images out of layout, so hidden later scenes still do not load theirs; after it, the skip rule keeps their content skipped. * fix(core): hold a paused jump on the previous picture until the next scene's images decode * fix(studio): swap in a reloaded preview only after its restore seek has painted * fix(core): apply a held paused jump after 1 s when an image never decodes * fix(core): keep hidden-scene image skipping and held jumps to Studio's preview, never renders * fix(studio): reapply manual edits once a held paused jump lands * fix(studio): copy a clip without the preview's lazy-loading marks * fix(studio-server): mark preview images lazy without re-encoding the document's entities * fix(core): share one preview-mark constant between the runtime and the preview route * fix(studio): reapply manual edits a second time only after a seek that was held * refactor(core): share the hidden-ancestor walk between clip visibility and image holds * fix(core): import the studio preview mark from its own subpath * fix(studio-server): find preview images with the linear html scanner * chore(core): drop two doc comments the scanner's names already carry * fix(studio-server): serve the preview mark as its own meta, apart from the GSAP fallback script * fix(studio-server): capture thumbnails and frames from the preview without lazy images or the mark * fix(core): let the full readiness scan wait on every image except lazy ones off the first frame * fix(studio-server): keep an authored loading attribute in any letter case on preview images * fix(core): leave template content out of findStartTags so preview images match the DOM * fix(studio): send Ask agent the live markup without the preview loading marks * chore(core): drop the template note from findStartTags, its test carries it * perf(studio-server): splice lazy attributes into the preview in one pass * fix(studio): keep authored image loading on copy and Ask agent by marking preview-made lazy images * test(studio-server): follow the capture preview URL back to its sub-composition for every project id * refactor: name the upcoming-clip and GSAP fallback marks once * fix(studio-server): keep capture builds out of the on-disk preview store --- packages/core/package-subpaths.json | 6 + packages/core/package.json | 10 + .../core/src/compiler/htmlDocument.test.ts | 21 ++ packages/core/src/compiler/htmlDocument.ts | 27 ++- .../core/src/compositionReadiness.test.ts | 19 ++ packages/core/src/compositionReadiness.ts | 10 +- packages/core/src/runtime/entry.test.ts | 194 ++++++++++++++++++ packages/core/src/runtime/init.ts | 191 ++++++++++++----- packages/core/src/runtime/timedClipHide.ts | 33 ++- packages/core/src/runtime/types.ts | 4 +- packages/core/src/studioPreviewMark.ts | 3 + .../src/services/coreRuntimeBrowser.test.ts | 59 ++++++ .../src/helpers/lazyPreviewImages.test.ts | 76 +++++++ .../src/helpers/lazyPreviewImages.ts | 32 +++ .../studio-server/src/routes/preview.test.ts | 55 ++++- packages/studio-server/src/routes/preview.ts | 32 ++- .../src/routes/projectIdEncoding.test.ts | 13 +- .../src/routes/thumbnail.test.ts | 18 ++ .../studio-server/src/routes/thumbnail.ts | 4 +- .../src/components/editor/manualEdits.test.ts | 24 +++ .../src/components/editor/manualEdits.ts | 5 +- .../src/hooks/useAskAgentModal.test.tsx | 78 +++++++ packages/studio/src/hooks/useAskAgentModal.ts | 4 +- .../src/hooks/useClipboard.source.test.ts | 21 ++ packages/studio/src/hooks/useClipboard.ts | 5 +- .../player/hooks/useShadowPreviewReload.ts | 19 +- .../useTimelinePlayer.shadowReload.test.ts | 19 ++ .../studio/src/player/lib/playbackTypes.ts | 1 + packages/studio/src/utils/authoredSource.ts | 15 ++ 29 files changed, 907 insertions(+), 91 deletions(-) create mode 100644 packages/core/src/studioPreviewMark.ts create mode 100644 packages/studio-server/src/helpers/lazyPreviewImages.test.ts create mode 100644 packages/studio-server/src/helpers/lazyPreviewImages.ts create mode 100644 packages/studio/src/hooks/useAskAgentModal.test.tsx diff --git a/packages/core/package-subpaths.json b/packages/core/package-subpaths.json index c520a7e9da..472887c5aa 100644 --- a/packages/core/package-subpaths.json +++ b/packages/core/package-subpaths.json @@ -50,6 +50,12 @@ "types": "./dist/compositionContract.d.ts", "environments": ["browser", "bun", "node"] }, + "./studio-preview-mark": { + "source": "./src/studioPreviewMark.ts", + "runtime": "./dist/studioPreviewMark.js", + "types": "./dist/studioPreviewMark.d.ts", + "environments": ["browser", "bun", "node"] + }, "./media-timing": { "source": "./src/mediaTiming.ts", "runtime": "./dist/mediaTiming.js", diff --git a/packages/core/package.json b/packages/core/package.json index fffe05154c..e8f255e63f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -64,6 +64,12 @@ "import": "./src/compositionContract.ts", "types": "./src/compositionContract.ts" }, + "./studio-preview-mark": { + "bun": "./src/studioPreviewMark.ts", + "node": "./dist/studioPreviewMark.js", + "import": "./src/studioPreviewMark.ts", + "types": "./src/studioPreviewMark.ts" + }, "./media-timing": { "bun": "./src/mediaTiming.ts", "node": "./dist/mediaTiming.js", @@ -466,6 +472,10 @@ "import": "./dist/compositionContract.js", "types": "./dist/compositionContract.d.ts" }, + "./studio-preview-mark": { + "import": "./dist/studioPreviewMark.js", + "types": "./dist/studioPreviewMark.d.ts" + }, "./media-timing": { "import": "./dist/mediaTiming.js", "types": "./dist/mediaTiming.d.ts" diff --git a/packages/core/src/compiler/htmlDocument.test.ts b/packages/core/src/compiler/htmlDocument.test.ts index c98b00b727..e9ae8a4730 100644 --- a/packages/core/src/compiler/htmlDocument.test.ts +++ b/packages/core/src/compiler/htmlDocument.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { + findStartTags, injectScriptsAtHeadStart, injectScriptsIntoHtml, injectTagsAtHeadStart, @@ -221,6 +222,26 @@ describe("htmlDocument helpers", () => { }); }); +describe("findStartTags", () => { + const at = (html: string, name: string) => + findStartTags(html, name).map((i) => html.slice(i, html.indexOf(">", i) + 1)); + + it("finds each start tag in any case, and none in comments, raw text or longer names", () => { + const html = + '' + + '
'; + expect(at(html, "img")).toEqual(["", ""]); + }); + + it("leaves out template content, nested templates included", () => { + const html = + "" + + '
'; + expect(at(html, "img")).toEqual(["", ""]); + expect(at(html, "template")).toEqual(["