UNPKG

rollup-plugin-visualizer

Version:

Visualize and analyze your bundle to quickly see which modules are taking up space.

258 lines (257 loc) 12.2 kB
const byteFormatter = new Intl.NumberFormat("en-US"); const formatBytes = (value) => { if (value < 1024) return `${byteFormatter.format(value)} B`; const units = ["KiB", "MiB", "GiB"]; let num = value; for (const unit of units) { num /= 1024; if (num < 1024) { return `${num.toFixed(2)} ${unit}`; } } return `${num.toFixed(2)} TiB`; }; const formatPercent = (value) => `${value.toFixed(1)}%`; const sharePercent = (value, total) => { if (total === 0) return "0.0%"; return formatPercent((value / total) * 100); }; const yesNo = (value) => (value ? "yes" : "no"); const formatFilters = (filters) => { if (filters == null) return "none"; const values = Array.isArray(filters) ? filters : [filters]; if (values.length === 0) return "none"; return `\`${JSON.stringify(values)}\``; }; export const outputMarkdown = (strData, reportConfig) => { const data = JSON.parse(strData); const SLICE_BUNDLE_COUNT = 8; const SLICE_MODULES_PER_BUNDLE = 8; const SLICE_MEMBERSHIP_MODULE_COUNT = 12; const SLICE_MEMBERSHIP_BUNDLE_PARTS = 6; const moduleRows = []; const bundleMap = new Map(); const bundleModuleParts = new Map(); for (const meta of Object.values(data.nodeMetas)) { const row = { id: meta.id, bundles: 0, renderedLength: 0, gzipLength: 0, brotliLength: 0, importedByCount: meta.importedBy.length, importedCount: meta.imported.length, parts: [], }; for (const [bundleId, partUid] of Object.entries(meta.moduleParts)) { const part = data.nodeParts[partUid]; if (!part) continue; row.bundles += 1; row.renderedLength += part.renderedLength; row.gzipLength += part.gzipLength; row.brotliLength += part.brotliLength; row.parts.push({ bundleId, renderedLength: part.renderedLength, gzipLength: part.gzipLength, brotliLength: part.brotliLength, }); const bundle = bundleMap.get(bundleId) ?? { bundleId, modules: 0, renderedLength: 0, gzipLength: 0, brotliLength: 0, }; bundle.modules += 1; bundle.renderedLength += part.renderedLength; bundle.gzipLength += part.gzipLength; bundle.brotliLength += part.brotliLength; bundleMap.set(bundleId, bundle); const bundleParts = bundleModuleParts.get(bundleId) ?? []; bundleParts.push({ id: row.id, renderedLength: part.renderedLength, gzipLength: part.gzipLength, brotliLength: part.brotliLength, }); bundleModuleParts.set(bundleId, bundleParts); } if (row.bundles > 0) { moduleRows.push(row); } } const bundleRows = [...bundleMap.values()].toSorted((a, b) => b.renderedLength - a.renderedLength); const hotModules = [...moduleRows].toSorted((a, b) => b.renderedLength - a.renderedLength); const duplicatedModules = [...moduleRows] .filter((row) => row.bundles > 1) .map((row) => { const biggestRendered = row.parts.reduce((max, part) => Math.max(max, part.renderedLength), 0); const duplicatedRendered = row.renderedLength - biggestRendered; return { ...row, duplicatedRendered }; }) .toSorted((a, b) => b.duplicatedRendered - a.duplicatedRendered); const packageRegex = /(?:^|[/\\])node_modules[/\\]((?:@[^/\\]+[/\\])?[^/\\]+)/; const packages = new Map(); for (const row of moduleRows) { const match = packageRegex.exec(row.id); if (!match) continue; const packageName = match[1].replace(/\\/g, "/"); const current = packages.get(packageName) ?? { modules: 0, renderedLength: 0 }; current.modules += 1; current.renderedLength += row.renderedLength; packages.set(packageName, current); } const packageRows = [...packages.entries()] .map(([name, value]) => ({ name, ...value })) .toSorted((a, b) => b.renderedLength - a.renderedLength); let staticImports = 0; let dynamicImports = 0; for (const meta of Object.values(data.nodeMetas)) { for (const imported of meta.imported) { if (imported.dynamic) { dynamicImports += 1; } else { staticImports += 1; } } } const totalRendered = moduleRows.reduce((sum, row) => sum + row.renderedLength, 0); const totalGzip = moduleRows.reduce((sum, row) => sum + row.gzipLength, 0); const totalBrotli = moduleRows.reduce((sum, row) => sum + row.brotliLength, 0); const entries = Object.values(data.nodeMetas).filter((meta) => meta.isEntry).length; const externals = Object.values(data.nodeMetas).filter((meta) => meta.isExternal).length; let output = ""; output += "# Bundle Report\n\n"; output += `| Bundles | Modules | Entries | Externals | Static Imports | Dynamic Imports |\n`; output += `| ---: | ---: | ---: | ---: | ---: | ---: |\n`; output += `| ${String(bundleRows.length)} | ${String(moduleRows.length)} | ${String(entries)} | ${String(externals)} | ${String(staticImports)} | ${String(dynamicImports)} |\n\n`; output += `| Metric | Total |\n`; output += `| --- | ---: |\n`; output += `| Rendered | ${formatBytes(totalRendered)} |\n`; if (data.options.gzip) { output += `| Gzip | ${formatBytes(totalGzip)} |\n`; } if (data.options.brotli) { output += `| Brotli | ${formatBytes(totalBrotli)} |\n`; } output += `\n`; output += "## Top 10\n\n"; const topRows = hotModules.slice(0, 10); if (topRows.length === 0) { output += "- none\n"; } else { for (const row of topRows) { output += `- \`${row.id}\`: ${sharePercent(row.renderedLength, totalRendered)} (${formatBytes(row.renderedLength)})\n`; } } output += "\n"; output += "## Hot Modules (Self Size)\n\n"; output += `| Rendered% | Rendered | Gzip | Brotli | Bundles | Imported By | Imports | Module |\n`; output += `| ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |\n`; for (const row of hotModules.slice(0, 15)) { output += `| ${sharePercent(row.renderedLength, totalRendered)} | ${formatBytes(row.renderedLength)} | ${formatBytes(row.gzipLength)} | ${formatBytes(row.brotliLength)} | ${String(row.bundles)} | ${String(row.importedByCount)} | ${String(row.importedCount)} | \`${row.id}\` |\n`; } output += "\n"; output += "## Bundle Breakdown\n\n"; output += `| Bundle | Modules | Rendered | Gzip | Brotli |\n`; output += `| --- | ---: | ---: | ---: | ---: |\n`; for (const bundle of bundleRows) { output += `| \`${bundle.bundleId}\` | ${String(bundle.modules)} | ${formatBytes(bundle.renderedLength)} | ${formatBytes(bundle.gzipLength)} | ${formatBytes(bundle.brotliLength)} |\n`; } output += "\n"; output += "## Duplicate Modules Across Bundles\n\n"; output += `| Duplicated Rendered | Total Rendered | Bundles | Module |\n`; output += `| ---: | ---: | ---: | --- |\n`; for (const row of duplicatedModules.slice(0, 15)) { output += `| ${formatBytes(row.duplicatedRendered)} | ${formatBytes(row.renderedLength)} | ${String(row.bundles)} | \`${row.id}\` |\n`; } if (duplicatedModules.length === 0) { output += `| 0 B | 0 B | 0 | none |\n`; } output += "\n"; output += "## Top Packages\n\n"; output += `| Package | Modules | Rendered |\n`; output += `| --- | ---: | ---: |\n`; for (const row of packageRows.slice(0, 15)) { output += `| \`${row.name}\` | ${String(row.modules)} | ${formatBytes(row.renderedLength)} |\n`; } if (packageRows.length === 0) { output += `| none | 0 | 0 B |\n`; } output += "\n"; output += "## Per-Bundle Top Modules (Slices)\n\n"; const sliceBundles = bundleRows.slice(0, SLICE_BUNDLE_COUNT); if (sliceBundles.length === 0) { output += "- none\n\n"; } else { for (const bundle of sliceBundles) { output += `### \`${bundle.bundleId}\`\n\n`; output += `| Module | Rendered% | Rendered | Gzip | Brotli |\n`; output += `| --- | ---: | ---: | ---: | ---: |\n`; const parts = (bundleModuleParts.get(bundle.bundleId) ?? []) .toSorted((a, b) => b.renderedLength - a.renderedLength) .slice(0, SLICE_MODULES_PER_BUNDLE); if (parts.length === 0) { output += `| none | 0.0% | 0 B | 0 B | 0 B |\n`; } else { for (const part of parts) { output += `| \`${part.id}\` | ${sharePercent(part.renderedLength, bundle.renderedLength)} | ${formatBytes(part.renderedLength)} | ${formatBytes(part.gzipLength)} | ${formatBytes(part.brotliLength)} |\n`; } } output += "\n"; } } output += "## Top Module Bundle Membership (Slice)\n\n"; output += `| Module | Bundles | Rendered | Bundle Parts |\n`; output += `| --- | ---: | ---: | --- |\n`; const membershipRows = hotModules.slice(0, SLICE_MEMBERSHIP_MODULE_COUNT); if (membershipRows.length === 0) { output += `| none | 0 | 0 B | none |\n`; } else { for (const row of membershipRows) { const parts = [...row.parts].toSorted((a, b) => b.renderedLength - a.renderedLength); const formattedParts = parts .slice(0, SLICE_MEMBERSHIP_BUNDLE_PARTS) .map((part) => `\`${part.bundleId}\`: ${formatBytes(part.renderedLength)}`); const moreParts = parts.length > SLICE_MEMBERSHIP_BUNDLE_PARTS ? `; +${String(parts.length - SLICE_MEMBERSHIP_BUNDLE_PARTS)} more` : ""; output += `| \`${row.id}\` | ${String(row.bundles)} | ${formatBytes(row.renderedLength)} | ${formattedParts.join("; ")}${moreParts} |\n`; } } output += "\n"; const hasRuntimeConfig = reportConfig != null; output += "## Plugin Settings\n\n"; output += `| Setting | Value |\n`; output += `| --- | --- |\n`; output += `| rollup version | \`${String(data.env.rollup ?? "unknown")}\` |\n`; output += `| sourcemap mode | \`${yesNo(hasRuntimeConfig ? reportConfig.sourcemap : data.options.sourcemap)}\` |\n`; output += `| output.sourcemap | \`${hasRuntimeConfig ? yesNo(reportConfig.outputSourcemap) : "not available"}\` |\n`; output += `| gzip requested | \`${hasRuntimeConfig ? yesNo(reportConfig.gzipSize.requested) : "not available"}\` |\n`; output += `| gzip enabled | \`${hasRuntimeConfig ? yesNo(reportConfig.gzipSize.enabled) : yesNo(data.options.gzip)}\` |\n`; output += `| brotli requested | \`${hasRuntimeConfig ? yesNo(reportConfig.brotliSize.requested) : "not available"}\` |\n`; output += `| brotli enabled | \`${hasRuntimeConfig ? yesNo(reportConfig.brotliSize.enabled) : yesNo(data.options.brotli)}\` |\n`; output += `| include filters | ${hasRuntimeConfig ? formatFilters(reportConfig.include) : "not available"} |\n`; output += `| exclude filters | ${hasRuntimeConfig ? formatFilters(reportConfig.exclude) : "not available"} |\n\n`; output += "## Notes\n\n"; output += "- Size precision depends on mode. With `sourcemap: true`, bytes are attributed via source maps. Otherwise bytes come from rendered module code.\n"; output += "- In plugin mode, enabling `sourcemap` disables gzip/brotli size collection for this report.\n"; output += "- `include`/`exclude` filters remove unmatched modules from statistics entirely.\n"; output += "- External modules can appear in dependency links but do not contribute per-bundle size parts.\n"; return output; };