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
π #??? β BundleBudget Plugin PoC Implementation
Metric
Bundle size over time across key dimensions.
Parsed from --stats-json
output and grouped by logical artifact buckets.
Property | Value | Description |
---|---|---|
value | 132341 |
Total size of all chunks. |
displayValue | 13.4 MB |
Delta compared to base (cached or previous release). |
score | 1 |
1 if within budget or unchanged, 0 if regression is detected. |
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 Angular/Esbuild builds. - Identify and compare main, initial, and lazy chunks.
- 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.
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%
Integration Requirements
TODO:
- Scoring (compare against budget/trashhold)
- identify hashed files
- unified data
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
stats.json
The stats.json follows the following types
stats.json types
```ts /** * Describes the kind of an import. This is a string literal union * for better type safety and autocompletion. */ export type ImportKind = | 'entry-point' // An import statement, e.g., `import "foo"` | 'import-statement' // A require call, e.g., `require("foo")` | 'require-call' // A dynamic import, e.g., `import("foo")` | 'dynamic-import' // A require.resolve call, e.g., `require.resolve("foo")` | 'require-resolve' // An @import rule in CSS | 'import-rule' // A url() token in CSS | 'url-token';/**
- Represents a single import record within a file.
/
export interface MetafileImport {
/*- The path of the imported file, relative to the working directory.
/
path: string;
/* - The kind of the import.
/
kind: ImportKind;
/* - If true, this dependency is external and was not included in the bundle.
/
external?: boolean;
/* - The original path string as it appeared in the source code.
- Useful for context, especially with aliases or tsconfig paths.
*/
original?: string;
}
- The path of the imported file, relative to the working directory.
/**
- Details about a single input file that contributed to the bundle.
/
export interface MetafileInput {
/*- The size of the file in bytes.
/
bytes: number;
/* - A list of all imports within this file.
*/
imports: MetafileImport[];
}
- The size of the file in bytes.
/**
- Details about the contribution of a single input file to a specific output file.
/
export interface MetafileOutputInput {
/*- The number of bytes from this input file that are part of this specific output file.
*/
bytesInOutput: number;
}
- The number of bytes from this input file that are part of this specific output file.
/**
- Represents a single output file (a "chunk") from the build process.
/
export interface MetafileOutput {
/*- The total size of this output file in bytes.
/
bytes: number;
/* - A map of all input files that were bundled into this output file.
- The key is the input file path, and the value provides details about its contribution.
/
inputs: {
[path: string]: MetafileOutputInput;
};
/* - A list of imports that were not bundled and remain as import/require statements in the output.
- This is common for external packages.
/
imports: MetafileImport[];
/* - A list of all exported symbols from this output file.
/
exports: string[];
/* - The entry point that corresponds to this output file. This is only present if the
- output file is an entry point.
*/
entryPoint?: string;
}
- The total size of this output file in bytes.
/**
- The root structure of the metafile JSON generated by esbuild.
/
export interface EsbuildMetafile {
/*- A map of all input files involved in the build.
- The key is the file path relative to the working directory.
/
inputs: {
[path: string]: MetafileInput;
};
/* - A map of all output files generated by the build.
- The key is the file path relative to the
outdir
.
*/
outputs: {
[path: string]: MetafileOutput;
};
}
</details>
Example stats.json
<details>
<summary>Example stats.json</summary>
```json
{
"inputs": {
"node_modules/@angular/core/fesm2022/untracked-BKcld_ew.mjs": {
"bytes": 20949,
"imports": [
{
"path": "<runtime>",
"kind": "import-statement",
"external": true
}
],
"format": "esm"
},
"node_modules/@angular/core/fesm2022/primitives/di.mjs": {
"bytes": 1158,
"imports": [],
"format": "esm"
},
"node_modules/@angular/core/fesm2022/primitives/signals.mjs": {
"bytes": 3167,
"imports": [
{
"path": "node_modules/@angular/core/fesm2022/untracked-BKcld_ew.mjs",
"kind": "import-statement",
"original": "../untracked-BKcld_ew.mjs"
},
{
"path": "node_modules/@angular/core/fesm2022/untracked-BKcld_ew.mjs",
"kind": "import-statement",
"original": "../untracked-BKcld_ew.mjs"
},
{
"path": "<runtime>",
"kind": "import-statement",
"external": true
}
],
"format": "esm"
}
},
"outputs": {
"chunk-MXYP3LXH.js.map": {
"imports": [],
"exports": [],
"inputs": {},
"bytes": 15799
},
"chunk-MXYP3LXH.js": {
"imports": [
{
"path": "chunk-PH2Q42UX.js",
"kind": "import-statement"
},
{
"path": "chunk-A3IGLJVE.js",
"kind": "import-statement"
},
{
"path": "chunk-MMWVBYNW.js",
"kind": "import-statement"
},
{
"path": "chunk-KTVYRR64.js",
"kind": "import-statement"
}
],
"exports": [
"a",
"b"
],
"inputs": {
"lib/re-captcha.ts": {
"bytesInOutput": 165
},
"lib/re-captcha.service.ts": {
"bytesInOutput": 2408
},
"lib/re-captcha-noop.service.ts": {
"bytesInOutput": 342
},
"lib/recaptcha.module.ts": {
"bytesInOutput": 120
},
"index.ts": {
"bytesInOutput": 0
}
},
"bytes": 3345
},
"chunk-5HVHEFQE.js.map": {
"imports": [],
"exports": [],
"inputs": {},
"bytes": 3942
},
"chunk-5HVHEFQE.js": {
"imports": [
{
"path": "chunk-J5C35BAI.js",
"kind": "import-statement"
},
{
"path": "chunk-PUHQEZG3.js",
"kind": "import-statement"
},
{
"path": "chunk-ZQTK7NVT.js",
"kind": "import-statement"
},
{
"path": "chunk-KTVYRR64.js",
"kind": "import-statement"
}
],
"exports": [
"a"
],
"inputs": {
"auth-state.service.ts": {
"bytesInOutput": 732
}
},
"bytes": 946
}
}
}
Angular App - Tree example
π main-TVMA6NI7.js 98.7 kB
βββΆ chunk-5QRGP6BJ.js 105.9 kB
βββ· chunk-NY7Q4ZJ6.js 0.34 kB
β βββΆ chunk-5QRGP6BJ.js re-used
ββ β¦2 more imports (β·/βΆ) β€ 0.30 kB
βββ @angular/router 63.3 kB / 2 files
βββ @angular/common 5.2 kB / 3 files
βββ @angular/platform-browser 11.3 kB / 5 files
βββ src/app/app.component.ts 17.6 kB
βββ src/app/app.routes.ts 0.10 kB
βββ src/app/app.config.ts 0.05 kB
βββ src/main.ts 0.04 kB
βββ β¦2 more inputs β€ 0.30 kB
π¦ chunk-NY7Q4ZJ6.js 0.34 kB (lazy)
βββΆ chunk-5QRGP6BJ.js re-used
βββ src/app/route-1.component.ts 0.23 kB
π¦ chunk-5QRGP6BJ.js 105.9 kB (shared)
βββ @angular/core 86.1 kB / 3 files
βββ tslib 1.86 kB / 2 files
βββ rxjs 4.71 kB / 4 files
βββ β¦44 more inputs β€ 3.40 kB
π app.component.css 2.86 kB
βββ inline from HTML 2.93 kB
π polyfills.js 33.8 kB
βββ zone.js 33.8 kB / 1 file
βββ angular:polyfills 0 B
π styles.css 0 B
βββ src/styles.css 0 B
βββ angular:styles/global 0 B
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
Option Types
export interface BundleStatsConfig {
slug: string;
include: string[];
exclude: string[];
thresholds?: {
total?: TotalThresholds;
chunk?: ChunkThresholds;
};
}
export interface BundleThresholds {
bytesChange: MinMax;
percentageChange: MinMax;
}
export type MinMax = [number, number];
export interface TotalThresholds extends BundleThresholds {
chunks: MinMax;
}
export interface ChunkThresholds extends BundleThresholds {
files: MinMax;
}
Examples:
const pluginConfig = {
generateStats: "nx run app-1:build --stats";
statsPath: "./stats.json"
customAudits: [
{
{
name: 'Initial Chunks',
include: ['src/main.js', 'src/polyfills.js', 'src/runtime.js'],
thresholds: {
maxTotalSize: 102400
}
},
{
name: 'Auth Module',
include: ['src/app/auth/**'],
thresholds: {
bytes: 102400
}
},
{
name: 'Lazy Products',
include: ['src/app/products/**', '!src/app/products/shared/**'],
thresholds: {
percent: 0.02
}
}
]
}
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-chunk 26.0 KB
π main-TVMA6NI7.js 198.7 kB
ββΆ chunk-5QRGP6BJ.js 105.9 kB
ββ β¦2 more imports β€ 0.30 kB
ββ @angular/router 63.3 kB / 2 files
ββ @angular/common 5.2 kB / 3 files
ββ @angular/platform-browser 11.3 kB / 5 files
ββ β¦2 more inputs β€ 0.30 kB
π¦ chunk-5QRGP6BJ.js 105.9 kB
βββ @angular/core 86.1 kB / 3 files
βββ tslib 1.86 kB / 2 files
βββ rxjs 4.71 kB / 4 files
βββ β¦44 more inputs β€ 3.40 kB
π app.component.css 2.86 kB
π polyfills.js 33.8 kB
π styles.css 0 B
Issues
Severity | Message | Source file | Line(s) |
---|---|---|---|
π¨ error | Chunk too big - 200 KB (allowed 14KB) | β | β |
Size increase +5% (allowed 3β14%) | β | β | |
Chunk size - 9KB (allowed 3β14KB) | β | β | |
π¨ error | Chunk too small - 1KB (allowed 3β14KB) | β | β |
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β% |
To Discuss
| Severity | Message | Source file | Line(s) |
| π¨ error | Not enough files - 2 (allowed 3β14) | β | β |
| π¨ error | Too many files - 20 (allowed 14) | β | β |
| π¨ error | Too many chunks - 20 (allowed 14) | β | β |
| π¨ error | Huge size increase +20% (allowed 14%) | β | β |
|
|