From 80d725b9dfd06162ff90beff4f2903a4b8d435c3 Mon Sep 17 00:00:00 2001 From: sidgaikwad Date: Fri, 4 Sep 2026 12:13:00 +0530 Subject: [PATCH] perf: prefetch the bundle when the host page already loaded embed.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The load listener prefetches the versioned bundle so the first createEditor does not pay a second round trip, but the early return for an already-installed window.ImageEditor bypassed it entirely. So on exactly the pages the tag-reuse logic exists to support — where the host injected embed.js itself — the prefetch never happened and the first mount was a full round trip slower than the injected path. The embed loader caches its own promise, so the duplicate call is a no-op when the host already triggered it. Also make the reused-tag timeout a parameter (defaulting to the exported REUSED_TAG_TIMEOUT_MS) rather than a hardcoded constant, so the bound is overridable and directly testable. --- src/loadScript.ts | 12 +++++++++--- test/loadScript.test.ts | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/loadScript.ts b/src/loadScript.ts index 8cceb77..58bc9fe 100644 --- a/src/loadScript.ts +++ b/src/loadScript.ts @@ -3,7 +3,7 @@ const defaultScriptUrl = 'https://cdn.unlayer.com/image-editor/embed.js'; // When reusing a host-injected tag we cannot know whether it already fired // `error` (a dead tag never re-fires), so the wait is bounded instead of // letting the promise hang forever. -const REUSED_TAG_TIMEOUT_MS = 30_000; +export const REUSED_TAG_TIMEOUT_MS = 30_000; interface TrackedLoad { promise: Promise; @@ -32,12 +32,18 @@ const findScriptTag = (scriptUrl: string): HTMLScriptElement | null => { * host-injected tag, if it doesn't become ready within a bounded wait). */ export const loadScript = ( - scriptUrl: string = defaultScriptUrl + scriptUrl: string = defaultScriptUrl, + reusedTagTimeoutMs: number = REUSED_TAG_TIMEOUT_MS ): Promise => { // The embed loader assigns window.ImageEditor synchronously while // embed.js evaluates, so its presence means the script already ran // (whether we injected it or the host page did). if (window.ImageEditor) { + // Prefetch the versioned bundle, exactly as the load listener below + // does. Without this a page that injected embed.js itself pays a full + // extra round trip on the first createEditor. The embed loader caches + // its own promise, so a duplicate call is a no-op. + window.ImageEditor.load().catch(() => {}); return Promise.resolve(); } @@ -99,7 +105,7 @@ export const loadScript = ( `Timed out waiting for an existing embed script tag: ${scriptUrl}` ) ); - }, REUSED_TAG_TIMEOUT_MS); + }, reusedTagTimeoutMs); } else { tag.src = scriptUrl; document.head.appendChild(tag); diff --git a/test/loadScript.test.ts b/test/loadScript.test.ts index 90002b4..3093e74 100644 --- a/test/loadScript.test.ts +++ b/test/loadScript.test.ts @@ -192,3 +192,40 @@ it('resetLoader removes the global, the tag, and the cached promise', async () = fire(scriptTags()[0], 'load'); await retry; }); + +it('prefetches the bundle when the host page already installed the global', async () => { + // Without this the first createEditor on a host-injected page pays a full + // extra round trip that the injected path avoids. + const embed = mockEmbed(); + window.ImageEditor = embed; + + await loadScript(); + + expect(embed.load).toHaveBeenCalledTimes(1); + expect(scriptTags()).toHaveLength(0); +}); + +it('swallows a prefetch failure on the already-installed path', async () => { + const embed = mockEmbed(); + vi.mocked(embed.load).mockRejectedValueOnce(new Error('bundle 404')); + window.ImageEditor = embed; + + await expect(loadScript()).resolves.toBeUndefined(); +}); + +it('accepts a custom reused-tag timeout', async () => { + vi.useFakeTimers(); + try { + const hostTag = document.createElement('script'); + hostTag.src = 'https://cdn.unlayer.com/image-editor/embed.js'; + document.head.appendChild(hostTag); + + const rejection = expect( + loadScript('https://cdn.unlayer.com/image-editor/embed.js', 5_000) + ).rejects.toThrow(/Timed out/); + vi.advanceTimersByTime(5_000); + await rejection; + } finally { + vi.useRealTimers(); + } +});