├── .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 | --------------------------------------------------------------------------------