From cdc7d442d8d74db78fab7e6a7f7d2a32176f2079 Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 20 Feb 2026 21:32:34 -0600 Subject: [PATCH 1/2] fix(clerk-js,ui): Preload component chunks in parallel during mount Replace the keepMounted Popover approach with chunk preloading. Pass preloadHint to ensureMounted() from all mount methods so component chunks download in parallel with the common chunk, eliminating the sequential waterfall on slow connections. Move preloadComponent() call outside the first-call guard so it fires on every ensureMounted() call. --- .changeset/orange-chefs-fail.md | 6 ++++++ packages/clerk-js/src/core/clerk.ts | 32 ++++++++++++++--------------- packages/ui/src/Components.tsx | 8 +++++--- 3 files changed, 27 insertions(+), 19 deletions(-) create mode 100644 .changeset/orange-chefs-fail.md diff --git a/.changeset/orange-chefs-fail.md b/.changeset/orange-chefs-fail.md new file mode 100644 index 00000000000..8e97810805b --- /dev/null +++ b/.changeset/orange-chefs-fail.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/ui': patch +--- + +Preload component chunks in parallel with the common chunk during mount, reducing first-render latency on slow connections. diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index be317c6dcc1..4be933079d0 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -972,7 +972,7 @@ export class Clerk implements ClerkInterface { this.assertComponentsReady(this.#clerkUI); const component = 'SignIn'; void this.#clerkUI - .then(ui => ui.ensureMounted()) + .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, @@ -994,7 +994,7 @@ export class Clerk implements ClerkInterface { this.assertComponentsReady(this.#clerkUI); const component = 'UserAvatar'; void this.#clerkUI - .then(ui => ui.ensureMounted()) + .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, @@ -1015,7 +1015,7 @@ export class Clerk implements ClerkInterface { this.assertComponentsReady(this.#clerkUI); const component = 'SignUp'; void this.#clerkUI - .then(ui => ui.ensureMounted()) + .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, @@ -1044,7 +1044,7 @@ export class Clerk implements ClerkInterface { this.assertComponentsReady(this.#clerkUI); const component = 'UserProfile'; void this.#clerkUI - .then(ui => ui.ensureMounted()) + .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, @@ -1090,7 +1090,7 @@ export class Clerk implements ClerkInterface { this.assertComponentsReady(this.#clerkUI); const component = 'OrganizationProfile'; void this.#clerkUI - .then(ui => ui.ensureMounted()) + .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, @@ -1125,7 +1125,7 @@ export class Clerk implements ClerkInterface { this.assertComponentsReady(this.#clerkUI); const component = 'CreateOrganization'; void this.#clerkUI - .then(ui => ui.ensureMounted()) + .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, @@ -1160,7 +1160,7 @@ export class Clerk implements ClerkInterface { this.assertComponentsReady(this.#clerkUI); const component = 'OrganizationSwitcher'; void this.#clerkUI - .then(ui => ui.ensureMounted()) + .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, @@ -1205,7 +1205,7 @@ export class Clerk implements ClerkInterface { this.assertComponentsReady(this.#clerkUI); const component = 'OrganizationList'; void this.#clerkUI - .then(ui => ui.ensureMounted()) + .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, @@ -1231,7 +1231,7 @@ export class Clerk implements ClerkInterface { this.assertComponentsReady(this.#clerkUI); const component = 'UserButton'; void this.#clerkUI - .then(ui => ui.ensureMounted()) + .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, @@ -1257,7 +1257,7 @@ export class Clerk implements ClerkInterface { this.assertComponentsReady(this.#clerkUI); const component = 'Waitlist'; void this.#clerkUI - .then(ui => ui.ensureMounted()) + .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, @@ -1286,7 +1286,7 @@ export class Clerk implements ClerkInterface { this.assertComponentsReady(this.#clerkUI); const component = 'PricingTable'; void this.#clerkUI - .then(ui => ui.ensureMounted()) + .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, @@ -1307,7 +1307,7 @@ export class Clerk implements ClerkInterface { this.assertComponentsReady(this.#clerkUI); const component = 'OAuthConsent'; void this.#clerkUI - .then(ui => ui.ensureMounted()) + .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, @@ -1362,7 +1362,7 @@ export class Clerk implements ClerkInterface { this.assertComponentsReady(this.#clerkUI); const component = 'APIKeys'; void this.#clerkUI - .then(ui => ui.ensureMounted()) + .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, @@ -1405,7 +1405,7 @@ export class Clerk implements ClerkInterface { this.assertComponentsReady(this.#clerkUI); const component = 'TaskChooseOrganization'; void this.#clerkUI - .then(ui => ui.ensureMounted()) + .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, @@ -1427,7 +1427,7 @@ export class Clerk implements ClerkInterface { const component = 'TaskResetPassword'; void this.#clerkUI - .then(ui => ui.ensureMounted()) + .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, @@ -1449,7 +1449,7 @@ export class Clerk implements ClerkInterface { const component = 'TaskSetupMFA'; void this.#clerkUI - .then(ui => ui.ensureMounted()) + .then(ui => ui.ensureMounted({ preloadHint: component })) .then(controls => controls.mountComponent({ name: component, diff --git a/packages/ui/src/Components.tsx b/packages/ui/src/Components.tsx index 647cd63eb78..8acc69facb7 100644 --- a/packages/ui/src/Components.tsx +++ b/packages/ui/src/Components.tsx @@ -221,15 +221,17 @@ export const mountComponentRenderer = ( return { ensureMounted: (opts?: { preloadHint: ClerkComponentName }) => { const { preloadHint } = opts || {}; + // Always preload, even if ensureMounted was already called. + // preloadComponent is idempotent (returns cached promise on subsequent calls). + if (preloadHint) { + void preloadComponent(preloadHint); + } // This mechanism ensures that mountComponentControls will only be called once // and any calls to .mount before mountComponentControls resolves will fire in order. // Otherwise, we risk having components rendered multiple times, or having // .unmountComponent incorrectly called before the component is rendered if (!componentsControlsResolver) { const deferredPromise = createDeferredPromise(); - if (preloadHint) { - void preloadComponent(preloadHint); - } componentsControlsResolver = import('./lazyModules/common').then(({ createRoot }) => { createRoot(clerkRoot).render( Date: Sat, 21 Feb 2026 10:13:53 -0600 Subject: [PATCH 2/2] fix(test): Mock @formkit/auto-animate/react in vitest setup The __mocks__ directory in src/elements/ was not detected by Vitest for node_module mocks, so auto-animate ran in tests and leaked timers that fired after jsdom teardown, causing requestAnimationFrame errors. --- packages/clerk-js/vitest.setup.mts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/clerk-js/vitest.setup.mts b/packages/clerk-js/vitest.setup.mts index ed0c190ea20..85786470155 100644 --- a/packages/clerk-js/vitest.setup.mts +++ b/packages/clerk-js/vitest.setup.mts @@ -262,6 +262,13 @@ if (typeof window !== 'undefined') { window.getComputedStyle = patchedGetComputedStyle; } +// Mock @formkit/auto-animate to prevent timers leaking after test teardown. +// The __mocks__ directory in src/elements/ is not detected by Vitest for +// node_module mocks, so we need an explicit vi.mock here. +vi.mock('@formkit/auto-animate/react', () => ({ + useAutoAnimate: () => [null], +})); + // Mock browser-tabs-lock to prevent window access errors in tests vi.mock('browser-tabs-lock', () => { return {