From 5130bea3a36e25c1b62f1682e18a76b19d92f754 Mon Sep 17 00:00:00 2001 From: maiieul <45822175+maiieul@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:11:26 +0200 Subject: [PATCH] feat: usePreloader --- .../docs/src/routes/api/qwik-city/api.json | 14 +++++++ .../docs/src/routes/api/qwik-city/index.mdx | 19 +++++++++ .../src/routes/api/qwik-optimizer/api.json | 19 ++++++++- .../src/routes/api/qwik-optimizer/index.mdx | 15 +++++++ .../src/routes/api/qwik-preloader/api.json | 5 +++ .../src/routes/api/qwik-preloader/index.mdx | 5 +++ packages/docs/src/routes/api/qwik/api.json | 2 +- packages/docs/src/routes/api/qwik/index.mdx | 2 +- .../src/runtime/src/client-navigate.ts | 1 - packages/qwik-city/src/runtime/src/index.ts | 8 +++- .../src/runtime/src/qwik-city.runtime.api.md | 8 +++- .../src/runtime/src/use-functions.ts | 34 +++++++++++++++ packages/qwik/package.json | 2 + packages/qwik/preloader.d.ts | 2 + .../src/core/preloader/api-extractor.json | 20 +++++++++ .../qwik/src/core/preloader/bundle-graph.ts | 3 ++ packages/qwik/src/core/preloader/queue.ts | 41 +++++++++++++------ .../src/core/preloader/qwik.preloader.api.md | 34 +++++++++++++++ packages/qwik/src/core/qrl/qrl-class.ts | 1 - .../qwik/src/optimizer/src/plugins/plugin.ts | 2 + .../src/optimizer/src/qwik.optimizer.api.md | 1 + scripts/api.ts | 7 ++++ .../apps/preloader-test/src/entry.ssr.tsx | 5 ++- .../apps/preloader-test/src/routes/layout.tsx | 21 +++++++++- starters/apps/preloader-test/vite.config.ts | 7 +++- 25 files changed, 252 insertions(+), 26 deletions(-) create mode 100644 packages/docs/src/routes/api/qwik-preloader/api.json create mode 100644 packages/docs/src/routes/api/qwik-preloader/index.mdx create mode 100644 packages/qwik/preloader.d.ts create mode 100644 packages/qwik/src/core/preloader/api-extractor.json create mode 100644 packages/qwik/src/core/preloader/qwik.preloader.api.md diff --git a/packages/docs/src/routes/api/qwik-city/api.json b/packages/docs/src/routes/api/qwik-city/api.json index fe22bfc12ca..798378c780a 100644 --- a/packages/docs/src/routes/api/qwik-city/api.json +++ b/packages/docs/src/routes/api/qwik-city/api.json @@ -898,6 +898,20 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/runtime/src/use-functions.ts", "mdFile": "qwik-city.usenavigate.md" }, + { + "name": "usePreloaderInfo", + "id": "usepreloaderinfo", + "hierarchy": [ + { + "name": "usePreloaderInfo", + "id": "usepreloaderinfo" + } + ], + "kind": "Function", + "content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nReactive hook exposing the current number of high-priority user-event preloads. Updates whenever the preloader recalculates the queue on the client.\n\n\n```typescript\nusePreloaderInfo: () => {\n userEventPreloadsCount: ReadonlySignal;\n activePreloadsLength: ReadonlySignal;\n}\n```\n**Returns:**\n\n{ userEventPreloadsCount: ReadonlySignal<number>; activePreloadsLength: ReadonlySignal; }", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/runtime/src/use-functions.ts", + "mdFile": "qwik-city.usepreloaderinfo.md" + }, { "name": "usePreventNavigate$", "id": "usepreventnavigate_", diff --git a/packages/docs/src/routes/api/qwik-city/index.mdx b/packages/docs/src/routes/api/qwik-city/index.mdx index db175d19d9f..2741230ad0e 100644 --- a/packages/docs/src/routes/api/qwik-city/index.mdx +++ b/packages/docs/src/routes/api/qwik-city/index.mdx @@ -2435,6 +2435,25 @@ useNavigate: () => RouteNavigate; [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/runtime/src/use-functions.ts) +## usePreloaderInfo + +> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. + +Reactive hook exposing the current number of high-priority user-event preloads. Updates whenever the preloader recalculates the queue on the client. + +```typescript +usePreloaderInfo: () => { + userEventPreloadsCount: ReadonlySignal; + activePreloadsLength: ReadonlySignal; +}; +``` + +**Returns:** + +\{ userEventPreloadsCount: ReadonlySignal<number>; activePreloadsLength: ReadonlySignal; } + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/src/runtime/src/use-functions.ts) + ## usePreventNavigate$ Prevent navigation attempts. This hook registers a callback that will be called before SPA or browser navigation. diff --git a/packages/docs/src/routes/api/qwik-optimizer/api.json b/packages/docs/src/routes/api/qwik-optimizer/api.json index 6d42fe4b24b..3fbd71d8b69 100644 --- a/packages/docs/src/routes/api/qwik-optimizer/api.json +++ b/packages/docs/src/routes/api/qwik-optimizer/api.json @@ -147,7 +147,7 @@ } ], "kind": "Enum", - "content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nUse `__EXPERIMENTAL__.x` to check if feature `x` is enabled. It will be replaced with `true` or `false` via an exact string replacement.\n\nAdd experimental features to this enum definition.\n\n\n```typescript\nexport declare enum ExperimentalFeatures \n```\n\n\n\n\n\n\n\n
\n\nMember\n\n\n\n\nValue\n\n\n\n\nDescription\n\n\n
\n\nenableRequestRewrite\n\n\n\n\n`\"enableRequestRewrite\"`\n\n\n\n\n**_(ALPHA)_** Enable request.rewrite()\n\n\n
\n\nnoSPA\n\n\n\n\n`\"noSPA\"`\n\n\n\n\n**_(ALPHA)_** Disable SPA navigation handler in Qwik City\n\n\n
\n\npreventNavigate\n\n\n\n\n`\"preventNavigate\"`\n\n\n\n\n**_(ALPHA)_** Enable the usePreventNavigate hook\n\n\n
\n\nvalibot\n\n\n\n\n`\"valibot\"`\n\n\n\n\n**_(ALPHA)_** Enable the Valibot form validation\n\n\n
", + "content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nUse `__EXPERIMENTAL__.x` to check if feature `x` is enabled. It will be replaced with `true` or `false` via an exact string replacement.\n\nAdd experimental features to this enum definition.\n\n\n```typescript\nexport declare enum ExperimentalFeatures \n```\n\n\n\n\n\n\n\n\n
\n\nMember\n\n\n\n\nValue\n\n\n\n\nDescription\n\n\n
\n\nenableRequestRewrite\n\n\n\n\n`\"enableRequestRewrite\"`\n\n\n\n\n**_(ALPHA)_** Enable request.rewrite()\n\n\n
\n\nnoSPA\n\n\n\n\n`\"noSPA\"`\n\n\n\n\n**_(ALPHA)_** Disable SPA navigation handler in Qwik City\n\n\n
\n\npreventNavigate\n\n\n\n\n`\"preventNavigate\"`\n\n\n\n\n**_(ALPHA)_** Enable the usePreventNavigate hook\n\n\n
\n\nusePreloaderInfo\n\n\n\n\n`\"usePreloaderInfo\"`\n\n\n\n\n**_(ALPHA)_** Enable the usePreloaderInfo hook\n\n\n
\n\nvalibot\n\n\n\n\n`\"valibot\"`\n\n\n\n\n**_(ALPHA)_** Enable the Valibot form validation\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/plugins/plugin.ts", "mdFile": "qwik.experimentalfeatures.md" }, @@ -949,6 +949,23 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts", "mdFile": "qwik.transpileoption.md" }, + { + "name": "usePreloaderInfo", + "id": "experimentalfeatures-usepreloaderinfo", + "hierarchy": [ + { + "name": "ExperimentalFeatures", + "id": "experimentalfeatures-usepreloaderinfo" + }, + { + "name": "usePreloaderInfo", + "id": "experimentalfeatures-usepreloaderinfo" + } + ], + "kind": "EnumMember", + "content": "", + "mdFile": "qwik.experimentalfeatures.usepreloaderinfo.md" + }, { "name": "valibot", "id": "experimentalfeatures-valibot", diff --git a/packages/docs/src/routes/api/qwik-optimizer/index.mdx b/packages/docs/src/routes/api/qwik-optimizer/index.mdx index 159d9d3c8e8..8356bf30616 100644 --- a/packages/docs/src/routes/api/qwik-optimizer/index.mdx +++ b/packages/docs/src/routes/api/qwik-optimizer/index.mdx @@ -413,6 +413,19 @@ preventNavigate +usePreloaderInfo + + + +`"usePreloaderInfo"` + + + +**_(ALPHA)_** Enable the usePreloaderInfo hook + + + + valibot @@ -3844,6 +3857,8 @@ export type TranspileOption = boolean | undefined | null; [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts) +## usePreloaderInfo + ## valibot ## versions diff --git a/packages/docs/src/routes/api/qwik-preloader/api.json b/packages/docs/src/routes/api/qwik-preloader/api.json new file mode 100644 index 00000000000..16176ea5019 --- /dev/null +++ b/packages/docs/src/routes/api/qwik-preloader/api.json @@ -0,0 +1,5 @@ +{ + "id": "qwik-preloader", + "package": "@builder.io/qwik/preloader", + "members": [] +} \ No newline at end of file diff --git a/packages/docs/src/routes/api/qwik-preloader/index.mdx b/packages/docs/src/routes/api/qwik-preloader/index.mdx new file mode 100644 index 00000000000..48bd9a7a72f --- /dev/null +++ b/packages/docs/src/routes/api/qwik-preloader/index.mdx @@ -0,0 +1,5 @@ +--- +title: \@builder.io/qwik/preloader API Reference +--- + +# [API](/api) › @builder.io/qwik/preloader diff --git a/packages/docs/src/routes/api/qwik/api.json b/packages/docs/src/routes/api/qwik/api.json index 91d5fcdccaa..8750f3fe20d 100644 --- a/packages/docs/src/routes/api/qwik/api.json +++ b/packages/docs/src/routes/api/qwik/api.json @@ -1774,7 +1774,7 @@ } ], "kind": "Function", - "content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\n> Warning: This API is now obsolete.\n> \n> This is no longer needed as the preloading happens automatically in qrl-class.ts. Leave this in your app for a while so it uninstalls existing service workers, but don't use it for new projects.\n> \n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n scope?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n nonce?: string;\n}) => JSXNode<'script'>\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nopts\n\n\n\n\n{ base?: string; scope?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; nonce?: string; }\n\n\n\n\n\n
\n\n**Returns:**\n\n[JSXNode](#jsxnode)<'script'>", + "content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\n> Warning: This API is now obsolete.\n> \n> This is no longer needed as the preloading happens automatically in qrl-class.ts. Leave this in your app for a while so it uninstalls existing service workers, but don't use it for new projects.\n> \n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n scope?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n nonce?: string;\n}) => JSXNode<'script'>\n```\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nopts\n\n\n\n\n{ base?: string; scope?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; nonce?: string; }\n\n\n\n\n\n
\n\n**Returns:**\n\nJSXNode<'script'>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts", "mdFile": "qwik.prefetchserviceworker.md" }, diff --git a/packages/docs/src/routes/api/qwik/index.mdx b/packages/docs/src/routes/api/qwik/index.mdx index 1ddbfe7a032..0b47efdd79d 100644 --- a/packages/docs/src/routes/api/qwik/index.mdx +++ b/packages/docs/src/routes/api/qwik/index.mdx @@ -3667,7 +3667,7 @@ opts **Returns:** -[JSXNode](#jsxnode)<'script'> +JSXNode<'script'> [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts) diff --git a/packages/qwik-city/src/runtime/src/client-navigate.ts b/packages/qwik-city/src/runtime/src/client-navigate.ts index 5e91e58ca8e..b3a9739fc13 100644 --- a/packages/qwik-city/src/runtime/src/client-navigate.ts +++ b/packages/qwik-city/src/runtime/src/client-navigate.ts @@ -1,5 +1,4 @@ import { isBrowser } from '@builder.io/qwik'; -// @ts-expect-error we don't have types for the preloader yet import { p as preload } from '@builder.io/qwik/preloader'; import type { NavigationType, ScrollState } from './types'; import { isSamePath, toPath } from './utils'; diff --git a/packages/qwik-city/src/runtime/src/index.ts b/packages/qwik-city/src/runtime/src/index.ts index 9c8df57f295..541f417d71d 100644 --- a/packages/qwik-city/src/runtime/src/index.ts +++ b/packages/qwik-city/src/runtime/src/index.ts @@ -55,7 +55,13 @@ export { } from './qwik-city-component'; export { type LinkProps, Link } from './link-component'; export { ServiceWorkerRegister } from './sw-component'; -export { useDocumentHead, useLocation, useContent, useNavigate } from './use-functions'; +export { + useDocumentHead, + useLocation, + useContent, + useNavigate, + usePreloaderInfo, +} from './use-functions'; export { usePreventNavigate$, usePreventNavigateQrl } from './use-functions'; export { routeAction$, routeActionQrl } from './server-functions'; export { globalAction$, globalActionQrl } from './server-functions'; diff --git a/packages/qwik-city/src/runtime/src/qwik-city.runtime.api.md b/packages/qwik-city/src/runtime/src/qwik-city.runtime.api.md index e4773ede255..c4eaac37ce2 100644 --- a/packages/qwik-city/src/runtime/src/qwik-city.runtime.api.md +++ b/packages/qwik-city/src/runtime/src/qwik-city.runtime.api.md @@ -15,7 +15,7 @@ import { QRL } from '@builder.io/qwik'; import { QRLEventHandlerMulti } from '@builder.io/qwik'; import { QwikIntrinsicElements } from '@builder.io/qwik'; import { QwikJSX } from '@builder.io/qwik'; -import type { ReadonlySignal } from '@builder.io/qwik'; +import { ReadonlySignal } from '@builder.io/qwik'; import { RequestEvent } from '@builder.io/qwik-city/middleware/request-handler'; import { RequestEventAction } from '@builder.io/qwik-city/middleware/request-handler'; import { RequestEventBase } from '@builder.io/qwik-city/middleware/request-handler'; @@ -485,6 +485,12 @@ export const useLocation: () => RouteLocation; // @public (undocumented) export const useNavigate: () => RouteNavigate; +// @alpha +export const usePreloaderInfo: () => { + userEventPreloadsCount: ReadonlySignal; + activePreloadsLength: ReadonlySignal; +}; + // @public export const usePreventNavigate$: (qrl: PreventNavigateCallback) => void; diff --git a/packages/qwik-city/src/runtime/src/use-functions.ts b/packages/qwik-city/src/runtime/src/use-functions.ts index 91bda8f6422..7084d8345cb 100644 --- a/packages/qwik-city/src/runtime/src/use-functions.ts +++ b/packages/qwik-city/src/runtime/src/use-functions.ts @@ -3,8 +3,11 @@ import { noSerialize, useContext, useServerData, + useSignal, useVisibleTask$, type QRL, + type ReadonlySignal, + type Signal, } from '@builder.io/qwik'; import { ContentContext, @@ -23,6 +26,8 @@ import type { PreventNavigateCallback, } from './types'; +import { isBrowser } from '@builder.io/qwik/build'; + /** @public */ export const useContent = () => useContext(ContentContext); @@ -95,3 +100,32 @@ export const usePreventNavigate$ = implicit$FirstArg(usePreventNavigateQrl); export const useAction = (): RouteAction => useContext(RouteActionContext); export const useQwikCityEnv = () => noSerialize(useServerData('qwikcity')); + +/** + * Reactive hook exposing the current number of high-priority user-event preloads. Updates whenever + * the preloader recalculates the queue on the client. + * + * @alpha + */ +export const usePreloaderInfo = (): { + userEventPreloadsCount: ReadonlySignal; + activePreloadsLength: ReadonlySignal; +} => { + const userEventPreloadsCount: Signal = useSignal(0); + const activePreloadsLength = useSignal(0); + + useVisibleTask$(() => { + if (!isBrowser) { + return; + } + const handler = (ev: Event) => { + const detail = (ev as CustomEvent).detail; + userEventPreloadsCount.value = detail?.userEventPreloads?.length; + activePreloadsLength.value = detail?.activePreloads.length; + }; + window.addEventListener('newPreloaderInfo', handler); + return () => window.removeEventListener('newPreloaderInfo', handler); + }); + + return { userEventPreloadsCount, activePreloadsLength }; +}; diff --git a/packages/qwik/package.json b/packages/qwik/package.json index 9015e82a484..f2d18160a2a 100644 --- a/packages/qwik/package.json +++ b/packages/qwik/package.json @@ -113,6 +113,7 @@ "require": "./dist/optimizer.cjs" }, "./preloader": { + "types": "./dist/preloader.d.ts", "import": "./dist/preloader.mjs", "require": "./dist/preloader.cjs" }, @@ -145,6 +146,7 @@ "optimizer.d.ts", "server.d.ts", "testing.d.ts", + "preloader.d.ts", "qwik-cli.cjs" ], "homepage": "https://qwik.dev/", diff --git a/packages/qwik/preloader.d.ts b/packages/qwik/preloader.d.ts new file mode 100644 index 00000000000..29533b87493 --- /dev/null +++ b/packages/qwik/preloader.d.ts @@ -0,0 +1,2 @@ +// re-export for typescript in old resolution mode +export * from './dist/loader'; diff --git a/packages/qwik/src/core/preloader/api-extractor.json b/packages/qwik/src/core/preloader/api-extractor.json new file mode 100644 index 00000000000..66895b370d2 --- /dev/null +++ b/packages/qwik/src/core/preloader/api-extractor.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "../../api-extractor.json", + "mainEntryPointFilePath": "/../../dist-dev/dts-out/packages/qwik/src/core/preloader/index.d.ts", + "newlineKind": "lf", + "apiReport": { + "enabled": true, + "reportFileName": "qwik.preloader", + "reportFolder": "/src/core/preloader/", + "reportTempFolder": "/../../dist-dev/api-extractor/preloader/" + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/preloader.d.ts" + }, + "docModel": { + "enabled": true, + "apiJsonFilePath": "/../../dist-dev/api/qwik/preloader/docs.api.json" + } +} diff --git a/packages/qwik/src/core/preloader/bundle-graph.ts b/packages/qwik/src/core/preloader/bundle-graph.ts index 7544a398670..eb19729d183 100644 --- a/packages/qwik/src/core/preloader/bundle-graph.ts +++ b/packages/qwik/src/core/preloader/bundle-graph.ts @@ -19,6 +19,7 @@ const makeBundle = (name: string, deps?: ImportProbability[]) => { }; }; +/** @internal */ export const parseBundleGraph = (serialized: (string | number)[]) => { const graph: BundleGraph = new Map(); let i = 0; @@ -64,6 +65,7 @@ export const getBundle = (name: string) => { }; /** Used in browser */ +/** @internal */ export const loadBundleGraph = ( basePath: string, serializedResponse?: ReturnType, @@ -118,6 +120,7 @@ export const loadBundleGraph = ( }; /** Used during SSR */ +/** @internal */ export const initPreloader = ( serializedBundleGraph?: (string | number)[], opts?: { diff --git a/packages/qwik/src/core/preloader/queue.ts b/packages/qwik/src/core/preloader/queue.ts index f31e4eb83ba..c5721fb0d89 100644 --- a/packages/qwik/src/core/preloader/queue.ts +++ b/packages/qwik/src/core/preloader/queue.ts @@ -14,12 +14,14 @@ export const bundles: BundleImports = new Map(); export let shouldResetFactor: boolean; let queueDirty: boolean; let preloadCount = 0; -const queue: BundleImport[] = []; + +export const internalQueue: BundleImport[] = []; +const activePreloads: BundleImport[] = []; export const log = (...args: any[]) => { // eslint-disable-next-line no-console console.log( - `Preloader ${Date.now() - loadStart}ms ${preloadCount}/${queue.length} queued>`, + `Preloader ${Date.now() - loadStart}ms ${preloadCount}/${internalQueue.length} queued>`, ...args ); }; @@ -29,11 +31,12 @@ export const resetQueue = () => { queueDirty = false; shouldResetFactor = true; preloadCount = 0; - queue.length = 0; + internalQueue.length = 0; }; + export const sortQueue = () => { if (queueDirty) { - queue.sort((a, b) => a.$inverseProbability$ - b.$inverseProbability$); + internalQueue.sort((a, b) => a.$inverseProbability$ - b.$inverseProbability$); queueDirty = false; } }; @@ -48,7 +51,7 @@ export const getQueue = () => { sortQueue(); let probability = 0.4; const result: (string | number)[] = []; - for (const b of queue) { + for (const b of internalQueue) { const nextProbability = Math.round((1 - b.$inverseProbability$) * 10); if (nextProbability !== probability) { probability = nextProbability; @@ -69,12 +72,12 @@ export const getQueue = () => { * We make sure to first preload the high priority items. */ export const trigger = () => { - if (!queue.length) { + if (!internalQueue.length) { return; } sortQueue(); - while (queue.length) { - const bundle = queue[0]; + while (internalQueue.length) { + const bundle = internalQueue[0]; const inverseProbability = bundle.$inverseProbability$; const probability = 1 - inverseProbability; const allowedPreloads = graph @@ -83,7 +86,7 @@ export const trigger = () => { 5; // When we're 99% sure, everything needs to be queued if (probability >= 0.99 || preloadCount < allowedPreloads) { - queue.shift(); + internalQueue.shift(); preloadOne(bundle); } else { break; @@ -93,7 +96,7 @@ export const trigger = () => { * The low priority bundles are opportunistic, and we want to give the browser some breathing room * for other resources, so we cycle between 4 and 10 outstanding modulepreloads. */ - if (config.$DEBUG$ && !queue.length) { + if (config.$DEBUG$ && !internalQueue.length) { const loaded = [...bundles.values()].filter((b) => b.$state$ > BundleImportState_None); const waitTime = loaded.reduce((acc, b) => acc + b.$waitedMs$, 0); const loadTime = loaded.reduce((acc, b) => acc + b.$loadedMs$, 0); @@ -126,18 +129,27 @@ const preloadOne = (bundle: BundleImport) => { // Needed when rel is 'preload' link.as = 'script'; // Handle completion of the preload - link.onload = link.onerror = () => { + link.onload = () => { preloadCount--; + activePreloads.shift(); const end = Date.now(); bundle.$loadedMs$ = end - start; bundle.$state$ = BundleImportState_Loaded; config.$DEBUG$ && log(`>> done after ${bundle.$loadedMs$}ms`, bundle.$name$); + + const userEventPreloads = activePreloads.filter((item) => item.$inverseProbability$ <= 0.01); + window.dispatchEvent( + new CustomEvent('newPreloaderInfo', { + detail: { userEventPreloads, activePreloads }, + }) + ); // Keep the clean link.remove(); + activePreloads.sort((a, b) => a.$inverseProbability$ - b.$inverseProbability$); + // More bundles may be ready to preload trigger(); }; - doc.head.appendChild(link); }; @@ -173,7 +185,8 @@ export const adjustProbabilities = ( ) { if (bundle.$state$ === BundleImportState_None) { bundle.$state$ = BundleImportState_Queued; - queue.push(bundle); + internalQueue.push(bundle); + activePreloads.push(bundle); config.$DEBUG$ && log(`queued ${Math.round((1 - bundle.$inverseProbability$) * 100)}%`, bundle.$name$); } @@ -222,6 +235,7 @@ export const adjustProbabilities = ( } }; +/** @internal */ export const handleBundle = (name: string, inverseProbability: number) => { const bundle = getBundle(name); if (bundle && bundle.$inverseProbability$ > inverseProbability) { @@ -231,6 +245,7 @@ export const handleBundle = (name: string, inverseProbability: number) => { let depsCount: number; +/** @internal */ export const preload = (name: string | (number | string)[], probability?: number) => { if (!name?.length) { return; diff --git a/packages/qwik/src/core/preloader/qwik.preloader.api.md b/packages/qwik/src/core/preloader/qwik.preloader.api.md new file mode 100644 index 00000000000..57cc40c30df --- /dev/null +++ b/packages/qwik/src/core/preloader/qwik.preloader.api.md @@ -0,0 +1,34 @@ +## API Report File for "@builder.io/qwik" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// Warning: (ae-forgotten-export) The symbol "BundleGraph" needs to be exported by the entry point index.d.ts +// Warning: (ae-internal-missing-underscore) The name "g" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const g: (serialized: (string | number)[]) => BundleGraph; + +// Warning: (ae-internal-missing-underscore) The name "h" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const h: (name: string, inverseProbability: number) => void; + +// Warning: (ae-internal-missing-underscore) The name "l" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const l: (basePath: string, serializedResponse?: ReturnType, opts?: { + debug?: boolean; + P?: number; + Q?: number; +}) => void; + +// Warning: (ae-internal-missing-underscore) The name "p" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const p: (name: string | (number | string)[], probability?: number) => void; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/qwik/src/core/qrl/qrl-class.ts b/packages/qwik/src/core/qrl/qrl-class.ts index 5f73755f45f..d20bbab1b36 100644 --- a/packages/qwik/src/core/qrl/qrl-class.ts +++ b/packages/qwik/src/core/qrl/qrl-class.ts @@ -15,7 +15,6 @@ import { getQFuncs, QInstance } from '../util/markers'; import { isPromise, maybeThen } from '../util/promises'; import { qDev, qSerialize, qTest, seal } from '../util/qdev'; import { isArray, isFunction, type ValueOrPromise } from '../util/types'; -// @ts-expect-error we don't have types for the preloader import { p as preload } from '@builder.io/qwik/preloader'; import type { QRLDev } from './qrl'; import type { QRL, QrlArgs, QrlReturn } from './qrl.public'; diff --git a/packages/qwik/src/optimizer/src/plugins/plugin.ts b/packages/qwik/src/optimizer/src/plugins/plugin.ts index 35ac8c78b3a..3bf97fb36db 100644 --- a/packages/qwik/src/optimizer/src/plugins/plugin.ts +++ b/packages/qwik/src/optimizer/src/plugins/plugin.ts @@ -66,6 +66,8 @@ const CLIENT_STRIP_CTX_NAME = [ * @alpha */ export enum ExperimentalFeatures { + /** Enable the usePreloaderInfo hook */ + usePreloaderInfo = 'usePreloaderInfo', /** Enable the usePreventNavigate hook */ preventNavigate = 'preventNavigate', /** Enable the Valibot form validation */ diff --git a/packages/qwik/src/optimizer/src/qwik.optimizer.api.md b/packages/qwik/src/optimizer/src/qwik.optimizer.api.md index 6030200d353..8d83f2cea29 100644 --- a/packages/qwik/src/optimizer/src/qwik.optimizer.api.md +++ b/packages/qwik/src/optimizer/src/qwik.optimizer.api.md @@ -55,6 +55,7 @@ export enum ExperimentalFeatures { enableRequestRewrite = "enableRequestRewrite", noSPA = "noSPA", preventNavigate = "preventNavigate", + usePreloaderInfo = "usePreloaderInfo", valibot = "valibot" } diff --git a/scripts/api.ts b/scripts/api.ts index 99e5fabd522..2648b34adfa 100644 --- a/scripts/api.ts +++ b/scripts/api.ts @@ -9,6 +9,13 @@ import { type BuildConfig, panic, copyFile, ensureDir } from './util'; * production build. */ export async function apiExtractorQwik(config: BuildConfig) { + // preloader + createTypesApi( + config, + join(config.srcQwikDir, 'core', 'preloader'), + join(config.distQwikPkgDir, 'preloader.d.ts'), + '.' + ); // core // Run the api extractor for each of the submodules createTypesApi( diff --git a/starters/apps/preloader-test/src/entry.ssr.tsx b/starters/apps/preloader-test/src/entry.ssr.tsx index 051c4fe32fd..2a0f96cfe0b 100644 --- a/starters/apps/preloader-test/src/entry.ssr.tsx +++ b/starters/apps/preloader-test/src/entry.ssr.tsx @@ -19,8 +19,9 @@ import Root from "./root"; export default function (opts: RenderToStreamOptions) { return renderToStream(, { preloader: { - debug: true, - ssrPreloads: 10, + debug: false, + ssrPreloads: 0, + maxIdlePreloads: 1, }, ...opts, // Use container attributes to set attributes on the html tag. diff --git a/starters/apps/preloader-test/src/routes/layout.tsx b/starters/apps/preloader-test/src/routes/layout.tsx index bf44e47e9d8..051b9b9d0ed 100644 --- a/starters/apps/preloader-test/src/routes/layout.tsx +++ b/starters/apps/preloader-test/src/routes/layout.tsx @@ -1,5 +1,16 @@ -import { component$, Slot, useSignal, useStyles$ } from "@builder.io/qwik"; -import { Link, type DocumentHead } from "@builder.io/qwik-city"; +import { + $, + component$, + Slot, + useSignal, + useStyles$, + useVisibleTask$, +} from "@builder.io/qwik"; +import { + Link, + type DocumentHead, + usePreloaderInfo, +} from "@builder.io/qwik-city"; export default component$(() => { useStyles$(` @@ -59,6 +70,8 @@ export default component$(() => { const isSPA = useSignal(true); const LinkCmp = isSPA.value ? Link : "a"; + const { userEventPreloadsCount, activePreloadsLength } = usePreloaderInfo(); + return (
@@ -73,6 +86,7 @@ export default component$(() => { > Counters + {userEventPreloadsCount.value > 0 && ...loading}
+

... user Event Preloads: {userEventPreloadsCount.value}

+

... active Preloads Length: {activePreloadsLength.value}

+
diff --git a/starters/apps/preloader-test/vite.config.ts b/starters/apps/preloader-test/vite.config.ts index 93a4fa9992a..88cc708ef93 100644 --- a/starters/apps/preloader-test/vite.config.ts +++ b/starters/apps/preloader-test/vite.config.ts @@ -64,8 +64,11 @@ export default defineConfig((): UserConfig => { return { plugins: [ qwikCity(), - qwikVite({ debug: true }), - createBulkPlugin(), + qwikVite({ + debug: true, + experimental: ["usePreloaderInfo"], + }), + // createBulkPlugin(), tsconfigPaths({ root: "." }), basicSsl(), ],