// Parse rollup-plugin-visualizer HTML output → Top-15 chunks + critical-path total. // Reads storage/bundle-analyze.html, extracts the inline `const data = {...};` JSON, // computes per-chunk raw + gzip + brotli totals by walking each top-level chunk's tree // and summing the corresponding nodeParts entries. import { readFileSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const htmlPath = resolve(__dirname, '..', 'storage', 'bundle-analyze.html'); const html = readFileSync(htmlPath, 'utf8'); // Locate the data assignment. const marker = 'const data = '; const start = html.indexOf(marker); if (start === -1) { console.error('FATAL: marker "const data = " not found'); process.exit(2); } // Find the terminating `};\n` that closes the object literal. // The line ends with `}};` (closing data) — find first `};` after start. const afterMarker = start + marker.length; // Trailing semicolon: search for `};` then back up. Simpler: capture until matching brace. let depth = 0; let i = afterMarker; let inString = false; let escape = false; let firstBraceFound = false; for (; i < html.length; i++) { const ch = html[i]; if (escape) { escape = false; continue; } if (ch === '\\') { escape = true; continue; } if (ch === '"') { inString = !inString; continue; } if (inString) continue; if (ch === '{') { depth++; firstBraceFound = true; } else if (ch === '}') { depth--; if (firstBraceFound && depth === 0) { i++; break; } } } const jsonStr = html.slice(afterMarker, i); const data = JSON.parse(jsonStr); // Build uid -> nodePart map (raw=renderedLength, gzip=gzipLength, brotli=brotliLength). // In visualizer schema v2, each leaf uid lives in `nodeParts`. const nodeParts = data.nodeParts || {}; // Walk a tree node, collect all uids in subtree. function collectUids(node, out) { if (!node) return; if (node.uid) { out.push(node.uid); return; } if (Array.isArray(node.children)) { for (const c of node.children) collectUids(c, out); } } // Top-level: data.tree.children = array of chunks (name = "assets/.js" or "assets/.css"). const chunks = []; for (const chunkNode of data.tree.children) { const uids = []; collectUids(chunkNode, uids); let raw = 0; let gzip = 0; let brotli = 0; for (const uid of uids) { const part = nodeParts[uid]; if (!part) continue; raw += part.renderedLength || 0; gzip += part.gzipLength || 0; brotli += part.brotliLength || 0; } chunks.push({ name: chunkNode.name, raw, gzip, brotli, moduleCount: uids.length, }); } // Sort by raw size descending. chunks.sort((a, b) => b.raw - a.raw); // Format helper. const fmt = (n) => `${(n / 1024).toFixed(2)} kB`; console.log('=== Top-15 chunks (sorted by raw size) ==='); console.log('| # | name | raw | gzip | brotli | modules |'); console.log('|---|---|---|---|---|---|'); chunks.slice(0, 15).forEach((c, idx) => { console.log(`| ${idx + 1} | ${c.name} | ${fmt(c.raw)} | ${fmt(c.gzip)} | ${fmt(c.brotli)} | ${c.moduleCount} |`); }); console.log('\n=== All chunks (full list, for critical-path identification) ==='); console.log('| # | name | raw | gzip | brotli |'); console.log('|---|---|---|---|---|'); chunks.forEach((c, idx) => { console.log(`| ${idx + 1} | ${c.name} | ${fmt(c.raw)} | ${fmt(c.gzip)} | ${fmt(c.brotli)} |`); }); // Critical path: eager chunks loaded on initial page render. // For an SPA with vite-plugin + dynamic-import lazy routes, the eager set = entry chunks // (app-*.js + app-*.css) plus any chunks they statically import. // The entry is `resources/js/app.ts` → produces `app-.js`. // Static imports of entry from manifest = critical path; dynamic imports = lazy. // // Read the manifest.json (Laravel Vite plugin) to find the entry's static imports. const manifestPath = resolve(__dirname, '..', 'public', 'build', 'manifest.json'); const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); // Find entry (isEntry: true). const entries = Object.entries(manifest).filter(([, v]) => v.isEntry); console.log('\n=== Manifest entries (isEntry=true) ==='); for (const [key, val] of entries) { console.log(` ${key} -> ${val.file}`); if (val.imports) console.log(` imports: ${JSON.stringify(val.imports)}`); if (val.css) console.log(` css: ${JSON.stringify(val.css)}`); if (val.dynamicImports) console.log(` dynamicImports.count: ${val.dynamicImports.length}`); } // Resolve critical-path file set: entry.file + recursive entry.imports[].file + entry.css[]. const eagerFiles = new Set(); function addEager(manifestKey) { const entry = manifest[manifestKey]; if (!entry) return; if (entry.file && !eagerFiles.has(entry.file)) { eagerFiles.add(entry.file); // Recurse into static imports only (NOT dynamicImports). if (Array.isArray(entry.imports)) { for (const imp of entry.imports) addEager(imp); } if (Array.isArray(entry.css)) { for (const c of entry.css) eagerFiles.add(c); } } } for (const [key, val] of entries) { if (val.isEntry) addEager(key); } console.log('\n=== Critical-path eager file set (entry + static imports + css) ==='); for (const f of eagerFiles) console.log(` - ${f}`); // Sum the corresponding chunks' gzip + raw. let eagerRaw = 0; let eagerGzip = 0; let eagerBrotli = 0; for (const f of eagerFiles) { // Chunk name in visualizer is "assets/" stripping "assets/" prefix in manifest path. // Manifest file path is e.g. "assets/app-XXXX.js" already. const target = chunks.find((c) => c.name === f); if (target) { eagerRaw += target.raw; eagerGzip += target.gzip; eagerBrotli += target.brotli; } else { // CSS files are not in tree (visualizer only tracks JS bundle internals). // Read CSS size directly from disk. try { const cssPath = resolve(__dirname, '..', 'public', 'build', f); const css = readFileSync(cssPath); const zlib = await import('node:zlib'); const gzSize = zlib.gzipSync(css).length; const brSize = zlib.brotliCompressSync(css).length; eagerRaw += css.length; eagerGzip += gzSize; eagerBrotli += brSize; console.log(` (CSS direct measure) ${f}: raw=${fmt(css.length)} gzip=${fmt(gzSize)} brotli=${fmt(brSize)}`); } catch (e) { console.log(` WARN: could not measure ${f}: ${e.message}`); } } } console.log('\n=== Critical-path eager total (visualizer-attribution; pre-minify source bytes) ==='); console.log(`raw: ${fmt(eagerRaw)}`); console.log(`gzip: ${fmt(eagerGzip)}`); console.log(`brotli: ${fmt(eagerBrotli)}`); console.log(`(${eagerFiles.size} files)`); // ========================================================================== // DISK-TRUTH VIEW: measure emitted files directly. Visualizer's renderedLength // is pre-minify source-byte attribution; on-disk JS is post-minify. Vite's // build stdout reports on-disk truth. Re-compute Top-15 + critical-path from // disk for an apples-to-apples view against the Vite log. // ========================================================================== import { readdirSync, statSync } from 'node:fs'; import { gzipSync, brotliCompressSync } from 'node:zlib'; const buildDir = resolve(__dirname, '..', 'public', 'build', 'assets'); const allFiles = readdirSync(buildDir); const diskJs = []; const diskCss = []; for (const f of allFiles) { const full = resolve(buildDir, f); const st = statSync(full); if (!st.isFile()) continue; const content = readFileSync(full); const entry = { name: `assets/${f}`, raw: content.length, gzip: gzipSync(content).length, brotli: brotliCompressSync(content).length, }; if (f.endsWith('.js')) diskJs.push(entry); else if (f.endsWith('.css')) diskCss.push(entry); } diskJs.sort((a, b) => b.raw - a.raw); diskCss.sort((a, b) => b.raw - a.raw); console.log('\n=== Top-15 chunks (DISK-TRUTH, .js only, sorted by raw on-disk size) ==='); console.log('| # | name | raw | gzip | brotli |'); console.log('|---|---|---|---|---|'); diskJs.slice(0, 15).forEach((c, idx) => { console.log(`| ${idx + 1} | ${c.name} | ${fmt(c.raw)} | ${fmt(c.gzip)} | ${fmt(c.brotli)} |`); }); console.log('\n=== Top-10 CSS files (DISK-TRUTH) ==='); console.log('| # | name | raw | gzip | brotli |'); console.log('|---|---|---|---|---|'); diskCss.slice(0, 10).forEach((c, idx) => { console.log(`| ${idx + 1} | ${c.name} | ${fmt(c.raw)} | ${fmt(c.gzip)} | ${fmt(c.brotli)} |`); }); // Critical-path eager total — DISK TRUTH using same manifest set. let diskEagerRaw = 0; let diskEagerGzip = 0; let diskEagerBrotli = 0; const diskEagerDetails = []; for (const f of eagerFiles) { const target = [...diskJs, ...diskCss].find((c) => c.name === f); if (target) { diskEagerRaw += target.raw; diskEagerGzip += target.gzip; diskEagerBrotli += target.brotli; diskEagerDetails.push(target); } else { console.log(` WARN: ${f} not found on disk`); } } console.log('\n=== Critical-path eager files (DISK TRUTH details) ==='); console.log('| # | name | raw | gzip | brotli |'); console.log('|---|---|---|---|---|'); diskEagerDetails.forEach((c, idx) => { console.log(`| ${idx + 1} | ${c.name} | ${fmt(c.raw)} | ${fmt(c.gzip)} | ${fmt(c.brotli)} |`); }); console.log('\n=== Critical-path eager total (DISK TRUTH = sum of emitted files) ==='); console.log(`raw: ${fmt(diskEagerRaw)}`); console.log(`gzip: ${fmt(diskEagerGzip)}`); console.log(`brotli: ${fmt(diskEagerBrotli)}`); console.log(`(${eagerFiles.size} files)`);