Description
π¦ Bundle Budget Plugin
Bundle size tracking for your build artifacts
Track, compare, and prevent bundle size regressions to maintain web performance (e.g. LCP) across product areas.
π§ͺ Reference PR
π #PoC β BundleBudget Plugin PoC Implementation
User story
As a developer, I want to track bundle size regressions per product area and route,
so that we can avoid performance regressions and optimize LCP over time.
The plugin should:
- Analyze
stats.json
output from different bundler. - Identify and compare main, initial, and lazy chunks over glob matching
- Use chunk input fingerprinting to map renamed chunk files.
- Group chunk sizes by route/product (e.g.,
/route-1
,/route-2
). - Store and compare bundle stats across versions/releases.
Metric
Bundle size in bytes.
Parsed from --stats-json
output and grouped by file.
Property | Value | Description |
---|---|---|
value | 132341 |
Total size of all chunks. |
displayValue | 13.4 MB / 13 Files |
Display value inc. number of files. |
Integration Requirements
The plugin can be implemented in 2 ways:
- Using stats files
- Crawling the filesystem
As stats file serve significantly more details and are state of the art when debugging bundle size this issue favours this approach.
Using Stats Files
Bundle stats are detailed metadata about your build outputsβlisting each generated file, its original inputs, and any static importsβexported via a metafile (e.g. from ESBuild or other bundlers).
Generate a stats file with ESBuild:
esbuild src/index.js --bundle --outfile=dist/bundle.js --metafile=stats.json
The resulting file maintains the following data structure:
EsbuildBundleStats
βββ inputs (Record<string, MetafileInput>)
β βββ <inputPath>
β βββ bytes: number
β βββ imports (MetafileImport[])
β βββ [ ]
β βββ path: string
β βββ kind: ImportKind
β βββ external?: boolean
β βββ original?: string
βββ outputs (Record<string, MetafileOutput>)
βββ <outputPath>
βββ bytes: number
βββ inputs (Record<string, MetafileOutputInput>)
β βββ <inputPath>
β βββ bytesInOutput: number
βββ imports (MetafileImport[])
β βββ [ ]
β βββ path: string
β βββ kind: ImportKind
β βββ external?: boolean
β βββ original?: string
βββ exports: string[]
βββ entryPoint?: string
The plugin will use this information to gather the configured artefact groups.
Crawling the filesystem
Note
No research done as not scaleable
Setup and Requirements
π¦ Package Dependencies
- Dev Dependencies:
- None required, optional CLI runner for local debugging.
- Optional Dependencies:
- esbuild β or any tool that emits
--metafile
/stats.json
.
- esbuild β or any tool that emits
π Configuration Files
angular.json
/vite.config.ts
or equivalent β for custom build config.- No required config file for the plugin itself.
Implementation details
Data Processing Pipeline
flowchart LR
subgraph Stats Generation
A[Esbuild β stats.json]
B[Webpack β stats.json]
C[Vite β stats.json]
end
D[Unify Stats]
E[Apply Plugin Config]
F[Compute Size Scores]
G[Compute Issue Penalties]
H[Blend into Final Score]
I[Generate Audits & Tree Display & Issues]
A --> D
B --> D
C --> D
D --> E
E --> F
E --> G
F --> H
G --> H
H --> I
Plugin Types
export type SupportedBundlers = 'esbuild' | 'webpack' | 'vite' | 'rsbuild';
export type MinMax = [number, number];
export interface BundleStatsConfig {
name: string;
include?: string[];
exclude?: string[];
thresholds: {
totalSize: number | MinMax;
artefactSize?: number | MinMax;
};
// includeInputs?: string[];
}
export interface GroupingOptions {
name: string;
patterns: PatternList;
icon?: string;
depth?: number;
}
export interface PruningOptions {
maxChildren?: number;
startDepth?: number;
maxDepth?: number;
}
export type PluginOptions {
generateArtefacts?: string;
artefactsPath: string;
bundler: SupportedBundlers;
customAudits: BundleStatsConfig[];
tree: boolean | {
grouping?: GroupingOptions;
pruning?: PruningOptions;
}
}
Plugin Usage
const config = {
plugins: [
bundleStatsPlugin({
generateArtefacts: "nx run app-1:build --stats",
artefactsPath: "./stats.json",
bundler: 'esbuild',
configs: [
{
name: "Initial Bundles",
include: ["main.js", "vendor.js"],
thresholds : { totalSize: 5000 }
},
{
name: 'Lazy Products',
include: ['src/app/products/**', '!src/app/products/shared/**'],
thresholds: {
totalSize: 1000,
artefactSize: 100
}
}
]
})
]
}
Audits
A audit encapsulates a group of artefacts determined by BundleStatsConfig
.
Scoring
The plugin assigns a score in the range [0 β¦ 1]
to each artefact (or artefact group) based on its byte size relative to a configurable maximum threshold (maxArtefactSize
). A perfect score (1
) means βwithin budget,β and a zero score (0
) means βcritically over budget.β
Scoring Parameters
Parameter | Description |
---|---|
S |
Actual bytes |
M |
Size threshold bytes |
E |
Number of βToo Largeβ (π¨) errors |
W |
Number of βToo Smallβ ( |
we |
Weight per error (default 1) |
ww |
Weight per warning (default 0.5) |
Size score
Issues penalty
Final blended score
xychart-beta
title "Score vs Artifact Size (with penalty shift)"
x-axis [0, 1, 1.25, 1.5, 1.75, 2]
y-axis "Score" 0 --> 1
line Original [1, 1, 0.75, 0.5, 0.25, 0]
line Penalized [0.5, 0.5, 0.25, 0, 0, 0]
Issues
The plugin optionally creates diagnostic for the given artefacts.
The following table shows all different options:
Diagnostic | Description | Config Key | Default | Severity | Recommended Action |
---|---|---|---|---|---|
Too Large π¨ | Artifact exceeds the maximum allowed size. May indicate an unoptimized bundle or accidentally checked-in | binary. | maxArtifactSize | 5 MB | Error |
Too Small |
Artifact is below the minimum expected size. Could signal missing dependencies or incomplete build. | minArtifactSize | 1 KB | Warn | Verify that the build output is complete and dependencies are included. |
Example Issues:
Severity | Message | Source file | Line(s) |
---|---|---|---|
π¨ error | main.js is 6.12 MB (max 5 MB); enable code-splitting or lazy-loading. |
dist/lib/main.js |
|
π¨ error | vendor.js is 2.05 MB (max 1.5 MB); apply tree-shaking or serve libs via CDN. |
dist/lib/vendor.js |
|
utils.js is 50 kB (min 100 kB); verify bundling. |
dist/lib/utils.js |
||
styles.css is 10 B (min 1 kB); check CSS inclusion. |
dist/lib/styles.css |
Artefact Tree
Example:
ποΈ example-group 237.17 kB 101 files
βββ π entry-1.js 138 kB 2 files
β βββ π src/input-1.ts 101 kB
β βββ π src/input-2.ts 37 kB
βββ π entry-2.js 30 kB 2 files
β βββ π node_modules/@angular/router/provider.js
β βββ π node_modules/@angular/router/service.js
βββ π¨ styles.css 14 kB
βββ π static imports from π entry-1.js 104 kB
βββ π file-1.js
Artefact Types
Each group is also displayed as tree of artefacts, inputs and static imports.
The following types are detected.
- π - script file - JS/TS artefact
- π¨ - style files - CSS/SCSS artefacts
- π - entry file - JS/TS artefact
- π - static imports - List of S/TS artefacts statically imported by another file
ποΈ example-group 190 kB 4 files
βββ π entry-1.js 100 kB 2 files
βββ π file-1.js 30 kB 2 files
βββ π¨ styles.css 10 kB
βββ π static imports from π entry-1.js
βββ π file-2.js 50 kB
Artefact Inputs
Each artefact is maintains out of inputs ind imports. The inputs are listed under each chunk.
ποΈ example-group 60 kB 1 files
βββ π file-1.js 60 kB 3 files
βββ π src/lib/cli.js
β βββ π node_modules/yargs/yargs.js
βββ π node_modules/zod/helpers
βββ π node_modules/zod/helpers/description.js
Artefact Imports
Each artefact imports that are statically imported and directly contribute to the artefact group size are listed under dedicated import groups per file. This is helpful to understand the true size of you artefact group.
Static imports are loaded together with it's importing parent and therefore will end up in the loaded bytes. Displaying them helps to inderstand why they are part and where they aer imported from.
ποΈ example-group 150 kB 6 files
βββ π entry-1.js 100 kB
βββ π file-1.js 50 kB
βββ π static imports from π entry-1.js 60 kB 2 files
β βββ π file-2.js 40 kB
β βββ π file-3.js 40 kB
βββ π static imports from π file-1.js 80 kB 2 files
βββ π file-4.js 40 kB
βββ π file-5.js 40 kB
Artefact Grouping
Artefact inputs can be grouped over the configuration.
Ungrouped:
ποΈ example-group 150 kB 1 files
βββ π entry-2.js 150 kB 3 files
βββ π node_modules/@angular/router/provider.js
βββ π node_modules/@angular/router/service.js
βββ π node_modules/@angular/router/utils.js
Grouped by:
patterns
-node_modules/@angular/router
name
-@angular/router
icon
- "π °οΈ "depth
-1
ποΈ example-group 150 kB 1 files
βββ π entry-2.js 150 kB 1 files
ββ π
°οΈ @angular/router
Tree Pruning
The artefact tree can get quite dense in information. To avoid overload we reduce the amount of visible nodes.
Unpruned:
ποΈ example-group 800 kB 10 files
βββ π index.js 250 kB 4 files
β βββ π src/app.js 120 kB 3 files
β β βββ π src/components/Header.js 40 kB 2 files
β β β βββ π src/components/Logo.js 15 kB 1 file
β β β βββ π src/components/Nav.js 25 kB 1 file
β β βββ π src/utils/math.js 30 kB 1 file
β βββ π src/route-1.js 120 kB 3 files
β βββ π src/route-2.js 120 kB 3 files
β βββ π src/main.css 50 kB 1 file
βββ π vendor.js 300 kB 5 files
β βββ π node_modules/react.js 100 kB 1 file
β βββ π node_modules/react-dom.js 80 kB 1 file
β βββ π node_modules/redux.js 60 kB 1 file
β βββ π node_modules/react-router.js 40 kB 1 file
β βββ π node_modules/lodash.js 20 kB 1 file
βββ π logo.svg 250 kB 1 file
Pruning options:
startDepth
-1
maxChildren
-1
maxDepth
-3
Pruned:
ποΈ example-group 150 kB 10 files
βββ π index.js 250 kB 4 files
β βββ π src/app.js 120 kB
β βββ β¦ 3 more inputs 130 kB
βββ π vendor.js 300 kB 5 files
β βββ π node_modules/react.js 100 kB
β βββ β¦ 4 more inputs 200 kB
βββ π logo.svg 250 kB
Report examples
Code PushUp Report
π· Category | β Score | π‘ Audits |
---|---|---|
Bundle Size | π‘ 54 | 13 |
π· Categories
Performance
Bundle size metrics π Docs
π’ Score: 92
- π₯ Bundle size changes InitialChunk (BundleSize) - 1 warning
- π‘ Bundle size changes InitialChunk (BundleSize)
π‘οΈ Audits
Bundle size changes InitialChunks (CodePushUp)
π₯ 3 info (score: 0)
Initial Chunks
ποΈ initial-bundles 237.16 kB 101 files
βββ π main-XTHQ3HCO.js 98.01 kB 14 files
β βββ src/app/app.routes.ts 101 B 1 file
β βββ src/app/app.config.ts 53 B 1 file
β βββ src/app/app.component.ts 17.59 kB 1 file
β βββ src/main.ts 37 B 1 file
β βββ π
°οΈ @angular/common 5.46 kB 4 files
β βββ π
°οΈ @angular/platform-browser 11.47 kB 3 files
β βββ π
°οΈ @angular/router 63.31 kB 3 files
βββ π polyfills-B6TNHZQ6.js 33.77 kB 2 files
β βββ angular:polyfills:angular:polyfills 1 file
β βββ π¦ zone.js 33.77 kB 1 file
βββ π styles-EGIV7BA7.css 631 B 1 file
βββ π static imports from π main-XTHQ3HCO.js 104.76 kB 84 files
βββ π chunk-5QRGP6BJ.js
Issues
Severity | Message | Source file | Line(s) |
---|---|---|---|
Bundle size (236.51 kB) is below minimum threshold (300 kB) |
CI Comment
π Plugin | π‘οΈ Audit | π Previous value | π Current value | π Value change |
---|---|---|---|---|
Bundle size | Total | π¨ 12.0 MB | π₯ 13.1 MB | β +9.2β% |
Bundle size | Initial Chunks | π© 6.0 MB | π© 6.2 MB | β +3.3β% |
Bundle size | Lazy Chunks | π₯ 6.0 MB | π₯ 6.9 MB | β +15.0β% |
Bundle size | Main Bundle | π¨ 5.0 MB | π₯ 5.2 MB | β +4.0β% |
Bundle size | Auth Module | π¨ 1.1 MB | π₯ 1.3 MB | β +18.2β% |
Bundle size | Lazy Products | π© 3.0 MB | π© 3.1 MB | β +3.3β% |
Market Research
SizeLimit
Repo: https://github.com/ai/size-limit
Setup
import type { SizeLimitConfig } from '../../packages/size-limit'
module.exports = [
{
path: "index.js",
import: "{ createStore }",
limit: "500 ms"
}
] satisfies SizeLimitConfig
Relevant Options:
- path: relative paths to files. The only mandatory option.
It could be a path"index.js"
, a [pattern]"dist/app-*.js"
or an array["index.js", "dist/app-*.js", "!dist/app-exclude.js"]
. - import: partial import to test tree-shaking. It could be
"{ lib }"
to testimport { lib } from 'lib'
,*
to test all exports,
or{ "a.js": "{ a }", "b.js": "{ b }" }
to test multiple files. - limit: size or time limit for files from the
path
option. It should be
a string with a number and unit, separated by a space.
Format:100 B
,10 kB
,500 ms
,1 s
. - name: the name of the current section. It will only be useful
if you have multiple sections. - message: an optional custom message to display additional information,
such as guidance for resolving errors, relevant links, or instructions
for next steps when a limit is exceeded.
- gzip: with
true
it will use Gzip compression and disable
Brotli compression. - brotli: with
false
it will disable any compression. - ignore: an array of files and dependencies to exclude from
the project size calculation.
Bundle Stats
repo: https://github.com/relative-ci/bundle-stats?tab=readme-ov-file
Setup
const { BundleStatsWebpackPlugin } = require('bundle-stats-webpack-plugin');
module.exports = {
plugins: [
new BundleStatsWebpackPlugin({
compare: true,
baseline: true,
html: true
})
]
};
Relevant Options
compare
| Enable comparison to baseline bundlebaseline
| Save stats as baseline for future runshtml
| Output visual HTML reportjson
| Output JSON snapshotstats
| (advanced) Customize Webpack stats passed into pluginsilent
| Disable logging
BundleMon
Repo: https://github.com/LironEr/bundlemon
Setup
"bundlemon": {
"baseDir": "./build",
"files": [
{
"path": "index.html",
"maxSize": "2kb",
"maxPercentIncrease": 5
},
{
"path": "bundle.<hash>.js",
"maxSize": "10kb"
},
{
"path": "assets/**/*.{png,svg}"
}
]
}
Relevant Options
path
(string, required) β Glob pattern relative to baseDir (e.g."**/*.js"
)friendlyName
(string, optional) β Human-readable name (e.g."Main Bundle"
)compression
("none" | "gzip", optional) β Override default compression (e.g."gzip"
)maxSize
(string, optional) β Max size:"2000b"
,"20kb"
,"1mb"
maxPercentIncrease
(number, optional) β Max % increase:0.5
= 0.5%,4
= 4%,200
= 200%