├── .gitignore
├── README.md
├── bundled.html
├── cert
├── cert.pem
└── key.pem
├── codegen.js
├── index.html
├── package.json
└── server.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .idea/
3 | node_modules/
4 | src/
5 | pnpm-lock.yaml
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Comparing bundled vs unbundled perf
2 |
3 | ## How to Run
4 |
5 | ```bash
6 | npm run build
7 | npm run dev
8 | ```
9 |
10 | Compare `localhost:4000` and `localhost:4000/bundled.html`
11 |
12 | ## Details
13 |
14 | Running build generates:
15 |
16 | - 5000 modules under `src/esm`
17 | - 5 levels deep import chain, with each level importing 1k modules in parallel.
18 | - Also generates `src/esm/bundled.js`
19 | - You can change the total number of modules by running e.g. `npm run build -- 3000`
20 |
21 | Running dev starts a Node.js http2 server with self-signed cert, with `maxConcurrentStreams` set to 1,000.
22 |
--------------------------------------------------------------------------------
/bundled.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Test
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/cert/cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDqzCCApOgAwIBAgIUC7bUPR+UyYbzaQ0sgBhfW4iejWEwDQYJKoZIhvcNAQEL
3 | BQAwZTELMAkGA1UEBhMCU0cxCzAJBgNVBAgMAlNHMQswCQYDVQQHDAJTRzEPMA0G
4 | A1UECgwGZm9vYmFyMQwwCgYDVQQDDANmZWYxHTAbBgkqhkiG9w0BCQEWDmV2YW5A
5 | dnVlanMub3JnMB4XDTI0MTIyNTA4NDgwMVoXDTI1MTIyNTA4NDgwMVowZTELMAkG
6 | A1UEBhMCU0cxCzAJBgNVBAgMAlNHMQswCQYDVQQHDAJTRzEPMA0GA1UECgwGZm9v
7 | YmFyMQwwCgYDVQQDDANmZWYxHTAbBgkqhkiG9w0BCQEWDmV2YW5AdnVlanMub3Jn
8 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApCoPAItTIbV6zaczz7zl
9 | l7preZwE8ys9BIiia4fERNiaeVYbXen0MQr8IV8YTcfBA56ZW03k+KpCX8eqoxrV
10 | XYZxhNNgsYNbuj3kJFtVNAXOZs12vGZJ+pXJTTsnYCpwcEBaoPGijoS1SCb8snTc
11 | PKyJs42N0tupUhv7TOK7S2jazROa4+yzx9jvQqaakQ1UgH8BpwkaiDGtBIAEe0pk
12 | PIiouPqsJyn/KVVuM7ogd7NGvO1+wc9wsWnHTroC5u8xGmRiXe80vExSCRvWQ9tv
13 | 416fNf2h7O4OHx/bxQJicJfr6sgpFP4/TCOEkCXVaWGtryyu88/rbStGvq5v0sbK
14 | 0wIDAQABo1MwUTAdBgNVHQ4EFgQUdlY0y0BinCblxK1jgDJjx3ABqQIwHwYDVR0j
15 | BBgwFoAUdlY0y0BinCblxK1jgDJjx3ABqQIwDwYDVR0TAQH/BAUwAwEB/zANBgkq
16 | hkiG9w0BAQsFAAOCAQEAGHH0a2xObQaOCAqXWP6eiEFchOQaq0CfAQCxEtsd4PQr
17 | 7FoJsfGmUtCxR9ogxICCdLWOvRjoSBOOhHgkHEFPMIj22V/4VL1575ota8YQS3oe
18 | f7jcyYr2fL/9rlpPd5N+867Cy6CZpUxHn+Q5ol06kXFAuCzUtj8d59SE85VPPG4z
19 | eOvy6QeG3H07NfVQZ0uXx+ImhRC6fpjFrU6miANHWGMuYKhfrNBfOU/nWTLWtZVW
20 | GhTXEqbdLtXFq5/mxtpUa5xQUJF7yJiJEjxJeL265RhLRjZXh3HIUJULL5WFlP6q
21 | DXGePHmN+6SOcmnTaBhDOtMxiQkDRuhFXvqf1IpoPw==
22 | -----END CERTIFICATE-----
23 |
--------------------------------------------------------------------------------
/cert/key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCkKg8Ai1MhtXrN
3 | pzPPvOWXumt5nATzKz0EiKJrh8RE2Jp5Vhtd6fQxCvwhXxhNx8EDnplbTeT4qkJf
4 | x6qjGtVdhnGE02Cxg1u6PeQkW1U0Bc5mzXa8Zkn6lclNOydgKnBwQFqg8aKOhLVI
5 | JvyydNw8rImzjY3S26lSG/tM4rtLaNrNE5rj7LPH2O9CppqRDVSAfwGnCRqIMa0E
6 | gAR7SmQ8iKi4+qwnKf8pVW4zuiB3s0a87X7Bz3CxacdOugLm7zEaZGJd7zS8TFIJ
7 | G9ZD22/jXp81/aHs7g4fH9vFAmJwl+vqyCkU/j9MI4SQJdVpYa2vLK7zz+ttK0a+
8 | rm/SxsrTAgMBAAECggEAOp+oXtXiOIHx2jnw5yj6Rl40nidonxvcPN+8PLEtljZR
9 | p63ntPhkkxTsp9ApQjFozG2ZL0FYWLKYZFLNrVRaSH1H0ZqYh5u0qIQMki0pdpvy
10 | MsUBt1LB7WILEnDi5VUSdQuOMbZIiiN7B0qg5+uScHpe7EdLE7cMtr02VyUKpGk5
11 | TeTBSfk+tSXq1/rMRNCts9UuI2WGres7IH0aSj1kKcbMa3YccmP8JwGfizLLSidc
12 | pm/8amU3nmH2KujeR1ct8RFXhb9AO9pxdB52b4q3Dua4XsQLJwXGwlYgMKeGEtlN
13 | SgOFL0pezujh4DLn730IUYIENNttja11Pt/a8qIMQQKBgQDXTKgrhBwuI8ODzmF2
14 | UNVozcVQ/bNYe+d1cZWuA0xkxTyOrD4uA58Ucqca7DRmv1RCLAlDDKm4gSloUY5H
15 | I1P9DFG/QrgEWnj5vo6aqFHrjZ2/KElru79EVSv1cTBkpNvYbINVomDeCnHmDiNw
16 | qhDxkEY6MdKHtDcWtueubuT/wQKBgQDDMrvJm/WvCJXnY49XJu9BfSvr6ME71pf3
17 | uo3a2T0+3pAcJajPUbQ5yzhzfABzU2mMXr4JmBDX1/A1G1Epvkj936sei/TBBtY+
18 | 5vZ0mk4QnWzU26wKn4rcOuIoOTtZTxubOHjejWnf7scT1AX9qIOsGGYpQB13v/GG
19 | YYlyZzOvkwKBgQCdajnnHz+vaIyyGTpfVHjEmPa04dm4T1eLIhIrWdQINcGyGzOX
20 | VyR/wdh2Mc+adDe57hKTiHWrJhzNXhvl3WmyMcivS6vZ2wBqsa34XCsRS8jTlDQi
21 | ZfJRluh+G1UjsPngwm5ANWoXzpE6gIne/SlIRZFjSTxB5j0FCZRZkFE0wQKBgQCG
22 | UmEbCTF1cykEg2ReCrLVk/cnbZGbRb5pgHyhPqCApNLyK5AQqh6lLoz4GzqMIbge
23 | GgwXugbNIRFw2g1t9j1wRfwRFyojsjq4KmMqNgzGLi54A3pODR+XEQYTu951RJSw
24 | qZmzPAjBSiTAJkTBGrcRM/EJLc+ZWgIwAOp+STot9QKBgQDH1fU4jncKpXCHVGLe
25 | IJqqQiNu7MK3Wvql6PN4s1tlUDBOOZ+5EClTNWkM4RupB3VIHBHeSwIMK6UIj0GW
26 | QouZM53f4K9UNTWrNWVJE8WaNzdEQ6k8v2Hc1O1UftTp7SeviFQ6iq6TRXDj43xi
27 | 7okBBX1ggucTixFFi0w3U3b3dQ==
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/codegen.js:
--------------------------------------------------------------------------------
1 | import { writeFileSync, mkdirSync, rmSync } from "fs";
2 |
3 | rmSync("src", { recursive: true, force: true });
4 | mkdirSync("./src");
5 |
6 | const arg = process.argv[2];
7 | const TOTAL = arg ? Number(arg) : 5_000;
8 | const DEPTH = 5;
9 | const WIDTH = Math.round(TOTAL / DEPTH);
10 |
11 | const generate = (dir, ext, nextImp) => {
12 | mkdirSync(`./src/${dir}`);
13 | for (let i = 1; i < DEPTH + 1; i++) {
14 | let code = "";
15 | for (let j = 1; j < WIDTH + 1; j++) {
16 | const childFile = nextImp(`${i}_${j}`);
17 | code += `\nimport "${childFile}"`;
18 | let childCode = "";
19 | // filler code
20 | for (let k = 0; k < 10; k++) {
21 | childCode += `\nfunction foo${k}() { window.side = ${k} }`;
22 | childCode += `\nfoo${k}()`;
23 | }
24 | writeFileSync(`src/${dir}${childFile.slice(1)}`, childCode);
25 | }
26 | code += `\nimport "${nextImp(i + 1)}"`;
27 | writeFileSync(`src/${dir}/${i}.${ext}`, code);
28 | }
29 | writeFileSync(
30 | `src/${dir}/${DEPTH + 1}.${ext}`,
31 | `
32 | const app = document.createElement("div");
33 | app.id = "app";
34 | app.textContent = 'Loaded in ' + (performance.now() - window.start).toFixed(2) + 'ms';
35 | document.body.appendChild(app);
36 | `,
37 | );
38 | };
39 |
40 | generate("esm", "js", (i) => `./${i}.js`);
41 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Test
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "esm-dev-server-limit",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "dev": "node server.js",
7 | "build": "node codegen.js",
8 | "postbuild": "esbuild src/esm/1.js --bundle --outfile=src/esm/bundled.js"
9 | },
10 | "prettier": {
11 | "trailingComma": "all"
12 | },
13 | "dependencies": {
14 | "esbuild": "^0.24.2"
15 | },
16 | "packageManager": "pnpm@9.15.1"
17 | }
18 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | import http2 from "node:http2";
2 | import { createReadStream, existsSync, statSync } from "node:fs";
3 | import { readFileSync } from "node:fs";
4 | import { join } from "node:path";
5 |
6 | // Load self-signed certificate and private key
7 | const options = {
8 | key: readFileSync("cert/key.pem"),
9 | cert: readFileSync("cert/cert.pem"),
10 | peerMaxConcurrentStreams: 1000
11 | };
12 |
13 | const server = http2.createSecureServer(options);
14 |
15 | server.on("stream", (stream, headers) => {
16 | const path = headers[":path"] === "/" ? "index.html" : headers[":path"].slice(1);
17 | const filePath = join(process.cwd(), path);
18 |
19 | if (!existsSync(filePath) || !statSync(filePath).isFile()) {
20 | stream.respond({ ":status": 404, "content-type": "text/plain" });
21 | stream.end("404 Not Found");
22 | return;
23 | }
24 |
25 | const contentType = getContentType(path);
26 | stream.respond({
27 | "content-type": contentType,
28 | ":status": 200,
29 | });
30 |
31 | const fileStream = createReadStream(filePath);
32 | fileStream.pipe(stream);
33 |
34 | fileStream.on("error", (err) => {
35 | console.error("File stream error:", err);
36 | stream.respond({ ":status": 500 });
37 | stream.end("Internal Server Error");
38 | });
39 | });
40 |
41 | server.listen(4000, () => {
42 | console.log("HTTP/2 server running at https://localhost:4000");
43 | });
44 |
45 | // Utility function to determine the content type
46 | function getContentType(filePath) {
47 | if (filePath.endsWith(".html")) return "text/html";
48 | if (filePath.endsWith(".svg")) return "image/svg+xml";
49 | if (filePath.endsWith(".js")) return "text/javascript";
50 | return "application/octet-stream"; // Default for unknown file types
51 | }
52 |
--------------------------------------------------------------------------------