Skip to content

Commit 6ab6559

Browse files
yangshunclaude
andauthored
perf(runtime): deduplicate concurrent client manifest loads (#528)
## Issue `createManifestLoader()` caches the client manifest after fetching and parsing it. Concurrent HTML renders on a fresh instance can all miss that cache and load separate copies. This duplicates work during overlapping cold loads. Warm requests already reuse the cached manifest. ## Fix Share one pending promise across concurrent manifest loads. Cache successful results and clear the pending promise when it settles, allowing missing manifests and failed loads to be retried. ## Verification Added unit coverage for concurrent loads sharing one fetch and manifest object, completed-cache reuse, and retries after a missing response or rejected fetch. - `vp test run packages/void/test/unit/routing-runtime.test.ts`: 40 tests passed on Node 24.21.0. Benchmarked eight simultaneous cold SSR requests using real generated manifests, with no artificial asset delay. Median burst completion times across nine shuffled runs on Node 24.21.0: | Generated pages | Before | After | Manifest reads/parses | |---|---:|---:|---:| | 1,000 | 28.52 ms | 21.27 ms | 8 → 1 | | 10,000 | 120.55 ms | 49.43 ms | 8 → 1 | Single-request controls performed one load on both revisions; warm bursts performed zero additional loads. HTML output matched, and browser hydration and navigation checks passed. These results describe concurrent cold bursts. Production frequency and full SSG build impact have not been measured. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 957026d commit 6ab6559

2 files changed

Lines changed: 52 additions & 8 deletions

File tree

‎packages/void/src/runtime/routing-assets.ts‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export type ManifestLoader = (
5454
) => Promise<ClientManifest | null>;
5555
export function createManifestLoader(): ManifestLoader {
5656
let cached: ClientManifest | null = null;
57+
let pending: Promise<ClientManifest | null> | null = null;
5758
return async (request, env) => {
5859
if (cached) {
5960
return cached;
@@ -68,14 +69,21 @@ export function createManifestLoader(): ManifestLoader {
6869
if (!assets || typeof assets.fetch !== 'function') {
6970
return null;
7071
}
71-
const response = await assets.fetch(
72-
new Request(new URL('/.vite/manifest.json', request.url).toString()),
73-
);
74-
if (!response.ok) {
75-
return null;
76-
}
77-
cached = (await response.json()) as ClientManifest;
78-
return cached;
72+
// Concurrent cold requests share one fetch and parse. A missing or failed
73+
// manifest is not cached, so a later request retries.
74+
pending ??= (async () => {
75+
const response = await assets.fetch(
76+
new Request(new URL('/.vite/manifest.json', request.url).toString()),
77+
);
78+
if (!response.ok) {
79+
return null;
80+
}
81+
cached = (await response.json()) as ClientManifest;
82+
return cached;
83+
})().finally(() => {
84+
pending = null;
85+
});
86+
return pending;
7987
};
8088
}
8189
export type ClientAssetOptions = {

‎packages/void/test/unit/routing-runtime.test.ts‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,42 @@ describe('typed client asset tags', () => {
805805
expect(islandTags.preloads).not.toContain('/md.js');
806806
expect(binding).toHaveBeenCalledTimes(1);
807807
});
808+
809+
it('shares one manifest fetch between concurrent cold loads', async () => {
810+
const binding = vi.fn(async () => Response.json({ page: { file: 'page.js' } }));
811+
const loader = createManifestLoader();
812+
const env = { ASSETS: { fetch: binding } };
813+
const request = new Request('https://app.test/page');
814+
const manifests = await Promise.all(Array.from({ length: 8 }, () => loader(request, env)));
815+
expect(binding).toHaveBeenCalledTimes(1);
816+
expect(new Set(manifests).size).toBe(1);
817+
expect(manifests[0]).toEqual({ page: { file: 'page.js' } });
818+
expect(await loader(request, env)).toBe(manifests[0]);
819+
expect(binding).toHaveBeenCalledTimes(1);
820+
});
821+
822+
it('retries the manifest after a missing response or a rejected fetch', async () => {
823+
const manifest = { page: { file: 'page.js' } };
824+
const binding = vi
825+
.fn<() => Promise<Response>>()
826+
.mockResolvedValueOnce(new Response('missing', { status: 404 }))
827+
.mockRejectedValueOnce(new Error('assets unavailable'))
828+
.mockImplementation(async () => Response.json(manifest));
829+
const loader = createManifestLoader();
830+
const env = { ASSETS: { fetch: binding } };
831+
const request = new Request('https://app.test/page');
832+
expect(await loader(request, env)).toBeNull();
833+
expect(binding).toHaveBeenCalledTimes(1);
834+
const rejected = [loader(request, env), loader(request, env)];
835+
await expect(rejected[0]).rejects.toThrow('assets unavailable');
836+
await expect(rejected[1]).rejects.toThrow('assets unavailable');
837+
expect(binding).toHaveBeenCalledTimes(2);
838+
expect(await loader(request, env)).toEqual(manifest);
839+
expect(binding).toHaveBeenCalledTimes(3);
840+
await loader(request, env);
841+
expect(binding).toHaveBeenCalledTimes(3);
842+
});
843+
808844
it('loads development CSS with active roots and omits client tags when production has no manifest', async () => {
809845
const devCss = vi.fn(
810846
async (

0 commit comments

Comments
 (0)