Skip to content

Commit 1c2f1c0

Browse files
authored
perf: process static assets in parallel (#3911)
1 parent f91c7b6 commit 1c2f1c0

2 files changed

Lines changed: 54 additions & 35 deletions

File tree

src/build/virtual/public-assets.ts

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { Nitro } from "nitro/types";
66
import type { PublicAsset } from "nitro/types";
77
import { relative, resolve } from "pathe";
88
import { joinURL, withTrailingSlash } from "ufo";
9+
import { runParallel } from "../../utils/parallel.ts";
910

1011
const readAssetHandler: Record<
1112
Exclude<Nitro["options"]["serveStatic"] | "true" | "false", boolean>,
@@ -30,41 +31,56 @@ export default function publicAssets(nitro: Nitro) {
3031
absolute: false,
3132
dot: true,
3233
});
33-
for (const id of files) {
34-
let mimeType =
35-
mime.getType(id.replace(/\.(gz|br)$/, "")) || "text/plain";
36-
if (mimeType.startsWith("text")) {
37-
mimeType += "; charset=utf-8";
38-
}
39-
const fullPath = resolve(nitro.options.output.publicDir, id);
40-
const assetData = await fsp.readFile(fullPath);
41-
const etag = createEtag(assetData);
42-
const stat = await fsp.stat(fullPath);
43-
44-
const assetId = joinURL(
45-
nitro.options.baseURL,
46-
decodeURIComponent(id)
47-
);
4834

49-
let encoding;
50-
if (id.endsWith(".gz")) {
51-
encoding = "gzip";
52-
} else if (id.endsWith(".br")) {
53-
encoding = "br";
54-
}
55-
56-
assets[assetId] = {
57-
type: nitro._prerenderMeta?.[assetId]?.contentType || mimeType,
58-
encoding,
59-
etag,
60-
mtime: stat.mtime.toJSON(),
61-
size: stat.size,
62-
path: relative(nitro.options.output.serverDir, fullPath),
63-
data:
64-
nitro.options.serveStatic === "inline"
65-
? assetData.toString("base64")
66-
: undefined,
67-
};
35+
const { errors } = await runParallel(
36+
new Set(files),
37+
async (id) => {
38+
let mimeType =
39+
mime.getType(id.replace(/\.(gz|br)$/, "")) || "text/plain";
40+
if (mimeType.startsWith("text")) {
41+
mimeType += "; charset=utf-8";
42+
}
43+
const fullPath = resolve(nitro.options.output.publicDir, id);
44+
const [assetData, stat] = await Promise.all([
45+
fsp.readFile(fullPath),
46+
fsp.stat(fullPath),
47+
]);
48+
49+
const etag = createEtag(assetData);
50+
51+
const assetId = joinURL(
52+
nitro.options.baseURL,
53+
decodeURIComponent(id)
54+
);
55+
56+
let encoding;
57+
if (id.endsWith(".gz")) {
58+
encoding = "gzip";
59+
} else if (id.endsWith(".br")) {
60+
encoding = "br";
61+
}
62+
63+
assets[assetId] = {
64+
type: nitro._prerenderMeta?.[assetId]?.contentType || mimeType,
65+
encoding,
66+
etag,
67+
mtime: stat.mtime.toJSON(),
68+
size: stat.size,
69+
path: relative(nitro.options.output.serverDir, fullPath),
70+
data:
71+
nitro.options.serveStatic === "inline"
72+
? assetData.toString("base64")
73+
: undefined,
74+
};
75+
},
76+
{ concurrency: 25 }
77+
);
78+
79+
if (errors.length > 0) {
80+
throw new Error(
81+
`Failed to process public assets:\n${errors.join("\n")}`,
82+
{ cause: errors }
83+
);
6884
}
6985

7086
return `export default ${JSON.stringify(assets, null, 2)};`;

src/utils/parallel.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ export async function runParallel<T>(
22
inputs: Set<T>,
33
cb: (input: T) => unknown | Promise<unknown>,
44
opts: { concurrency: number; interval?: number }
5-
) {
5+
): Promise<{ errors: unknown[] }> {
6+
const errors: unknown[] = [];
67
const tasks = new Set<Promise<unknown>>();
78

89
function queueNext(): undefined | Promise<unknown> {
@@ -37,4 +38,6 @@ export async function runParallel<T>(
3738
}
3839

3940
await refillQueue();
41+
42+
return { errors };
4043
}

0 commit comments

Comments
 (0)