-
Notifications
You must be signed in to change notification settings - Fork 16
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
π #1024 β 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
). - give penalties for site and blacklisted imports
- visualise inputs as well as static imports as they directly contribute to the real bundle size.
- 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 # Root object containing all bundle stats
βββ inputs (Record<string, MetafileInput>) # Map of each source input file to its metadata
β βββ <inputPath> # File path of a specific input module
β βββ bytes: number # Size of this input module in bytes
β βββ imports (MetafileImport[]) # List of static imports declared by this input
β βββ [ ] # Array of import entries
β βββ path: string # Resolved filesystem path of the imported module
β βββ kind: ImportKind # Import type (e.g. "import", "require", "dynamic")
β βββ external?: boolean # True if marked external (excluded from bundle)
β βββ original?: string # Original import specifier in source code
βββ outputs (Record<string, MetafileOutput>)# Map of each generated output file to its metadata
βββ <outputPath> # File path of a specific output chunk
βββ bytes: number # Total size of this output file in bytes
βββ inputs (Record<string,MetafileOutputInput>) # Map of input modules contributing to this output
β βββ <inputPath> # Path of an input that fed into this output
β βββ bytesInOutput: number # Number of bytes this input contributed to the output
βββ imports (MetafileImport[]) # List of static imports found in this output
β βββ [ ] # Array of import entries
β βββ path: string # Resolved filesystem path of the imported module
β βββ kind: ImportKind # Import type (e.g. "import", "require", "dynamic")
β βββ external?: boolean # True if marked external (excluded from bundle)
β βββ original?: string # Original import specifier in source code
βββ exports: string[] # List of named exports provided by this bundle
βββ entryPoint?: string # Entry-point module path for this output chunk, if any
File Type Definitions
Type | Structure | Description |
---|---|---|
inputs |
Record<string, MetafileInput> |
Map of each source file path to its metadata (bytes and imports). |
imports |
MetafileImport[] |
Array of import entries, each with path , kind , and optional flags. |
π outputs |
Record<string, MetafileOutput> |
Map of each output file path to its metadata (bytes, inputs, imports, exports, entryPoint). |
π entryPoint |
string (optional) |
The entry-point module path for this output chunk, if present. |
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:
π Configuration Files
angular.json
/vite.config.ts
or equivalent β for custom build config.- No required config file for the plugin itself.
Bundle Stats
The following is a minimal stats representation used to explain different features of the plugin. It will be referred to as Example Stats.
stats.json
βββ outputs
βββ dist/index.js // entryPoint: src/index.ts
β βββ inputs
β β βββ src/index.ts
β β βββ src/lib/feature-1.ts // import-statement
β β β βββ src/lib/utils/format.ts // import-statement
β β βββ src/lib/utils/math.ts // import-statement
β β βββ src/lib/feature-2.ts // dynamic-import
β βββ imports
β βββ dist/chunks/chunk-U6O5K65G.js // import-statement
β βββ dist/chunks/feature-2-X2YVDBQK.js // dynamic-import
βββ dist/bin.js // entryPoint: src/bin.ts
β βββ inputs
β β βββ src/bin.ts
β β βββ src/lib/feature-1.ts // import-statement
β β β βββ src/lib/utils/format.ts // import-statement
β β βββ src/lib/utils/math.ts // import-statement
β βββ imports
β βββ dist/chunks/chunk-U6O5K65G.js // import-statement
βββ dist/chunks/chunk-U6O5K65G.js
β βββ inputs
β βββ src/lib/utils/format.ts
β βββ src/lib/feature-1.ts
β βββ src/lib/utils/math.ts
βββ dist/chunks/feature-2-X2YVDBQK.js // entryPoint: src/lib/feature-2.ts
βββ inputs
βββ src/lib/feature-2.ts
Features
General
The audit name is provided over the title
property. Internally a audit slug
is derived from the
title
: A unique identifier for this group.description
: One two sentences explaining the purpose of the audit
Types
type Audit = {
slug?: string;
title: string;
description?: string;
};
Example Configuration
const audit1: Audit = {
title: 'Initial Bundles',
};
const audit2: Audit = {
slug: 'app-core',
title: 'π§±App Core',
description: 'This audit checks the core functionality of the app.',
};
Every audit gets the merged configuration of the global and audit specific configuration listed in the description.
Configuration Example
const config: BundleStatsConfig = {
title: 'Initial Bundles',
description: 'This audit checks the initial bundles of the app.',
};
Report Output
This audit checks the initial bundles of the app.
<details>
<summary>βοΈ Config Summary</summary>
**Selection**
β’ `includeOutputs`: `**/*`
**Scoring**
β’ `totalSize`: `0 B β 97.66 kB`
**Insights**
β’ π§ `**/math.ts`
β’ π§ `**/format.ts`
β’ π§© `**/*feature-2*`
β’ π `src/index.ts, src/bin.ts`
β’ π€ `dist/chunks/chunk-*.js`
β’ π¦ `**/node_modules/**`
β’ π¦ `dist/index.js, dist/bin.js`
</details>
Selection
To select files for an audit, glob patterns are used to include and exclude parts of the output files.
All options are provided as glob patterns matching either path
, path
in inputs
or entryPoint
.
Types
export interface SelectionOptions {
mode: 'bundle' | 'onlyMatching' | 'withAllDeps' | 'withStartupDeps'
// targeting output path of a `OutputNode`
includeOutputs: string[];
excludeOutputs: string[];
// targeting input paths of a `OutputNode`
includeInputs: string[];
excludeInputs: string[];
}
Example Configuration
const selection: SelectionOptions = {
mode: 'bundle',
includeOutputs: ['**/features/*'],
excludeOutputs: ['**/features/legacy/**'],
excludeInputs: ['**/ui/**']
};
Selection Behaviour
- π―Glob syntax: Supports standard glob patterns like
*
,**
,?
,[abc]
. - Include β Exclude: Selection starts with
include*
patterns (for outputs and inputs), followed byexclude*
to remove matches. - Precedence: If a file matches both
include
andexclude
, it will be excluded. - π Dependency handling is controlled by
mode
:'bundle'
and'onlyMatching'
ignore imports.'withAllDependencies'
and'withStartupDependencies'
: preserve imports even if excluded
All examples target this stats data.
Example Stats
The following is a minimal stats representation used to explain different features of the selection process.
stats.json βββ outputs βββ dist/index.js 309kB // entryPoint: src/index.ts β βββ inputs β β βββ src/index.ts β β βββ src/lib/feature-1.ts 100kB // import-statement β β β βββ src/lib/utils/format.ts 100kB // import-statement β β βββ src/lib/utils/math.ts 100kB // import-statement β β βββ src/lib/feature-2.ts 100kB // dynamic-import β βββ imports β βββ dist/chunks/chunk-U6O5K65G.js // import-statement β βββ dist/chunks/feature-2-X2YVDBQK.js // dynamic-import βββ dist/bin.js 309kB // entryPoint: src/bin.ts β βββ inputs β β βββ src/lib/feature-1.ts 100kB // import-statement β β β βββ src/lib/utils/format.ts 100kB // import-statement β β βββ src/lib/utils/math.ts 100kB // import-statement β βββ imports β βββ dist/chunks/chunk-U6O5K65G.js // import-statement βββ dist/chunks/chunk-U6O5K65G.js 309kB β βββ inputs β βββ src/lib/utils/format.ts 100kB β βββ src/lib/feature-1.ts 100kB β βββ src/lib/utils/math.ts 100kB βββ dist/chunks/feature-2-X2YVDBQK.js 109kB // entryPoint: src/lib/feature-2.ts βββ inputs βββ src/lib/feature-2.ts 100kB
Include Output
Select only dist/index.js
and its dependencies.
Selection Options
{
includeOutputs: ['**/dist/index.js']
}
Selection Result:
stats.json
βββ outputs
βββ π― dist/index.js // entryPoint: src/index.ts
β βββ inputs
β βββ imports
β βββ dist/chunks/chunk-U6O5K65G.js // import-statement
βββ π dist/chunks/chunk-U6O5K65G.js // imported by `dist/index.js`
βββ inputs
The target output and its imported dependencies are included.
Include/Exclude Output
Select all outputs except bin files.
Selection Options
{
includeOutputs: ["**/*"],
excludeOutputs: ["**/bin.js"]
}
Selection Result:
stats.json
βββ outputs
βββ π― dist/index.js // entryPoint: src/index.ts
βββ π dist/chunks/chunk-U6O5K65G.js // imported by `dist/index.js`
βββ π dist/chunks/feature-2-X2YVDBQK.js // imported by `dist/index.js`
βββ // excluded: dist/bin.js
All outputs are included except those matching the exclude pattern.
Include Input
Select outputs that contain specific input files.
Selection Options
{
includeInputs: ['**/feature-2.ts']
}
Selection Result:
stats.json
βββ outputs
βββ dist/index.js // entryPoint: src/index.ts
β βββ inputs
β βββ src/index.ts
β βββ π― src/lib/feature-2.ts // dynamic-import
βββ π dist/chunks/feature-2-X2YVDBQK.js // contains feature-2.ts
βββ inputs
βββ π― src/lib/feature-2.ts
Only outputs containing the specified input files are included.
Include/Exclude Input
Select all outputs but exclude those containing feature-2 files.
Selection Options
{
includeOutputs: ["**/*"],
excludeInputs: ["**/feature-2.ts"]
}
Selection Result:
stats.json
βββ outputs
βββ π― dist/bin.js // entryPoint: src/bin.ts
βββ π dist/chunks/chunk-U6O5K65G.js // imported by `dist/bin.js`
βββ // excluded: dist/index.js (contains feature-2.ts)
βββ // excluded: dist/chunks/feature-2-X2YVDBQK.js (contains feature-2.ts)
Outputs containing the excluded input files are filtered out.
Include/Exclude Mixed
Select feature outputs but exclude utility files.
Selection Options
{
includeOutputs: ['**/features/*', '**/index.js'],
excludeOutputs: ['**/bin.js'],
excludeInputs: ['**/utils/**']
}
Selection Result:
stats.json
βββ outputs
βββ π― dist/index.js // matches includeOutputs
β βββ inputs
β βββ src/index.ts
β βββ src/lib/feature-1.ts 100kB
β βββ src/lib/feature-2.ts 100kB
βββ π dist/chunks/feature-2-X2YVDBQK.js // imported by `dist/index.js`
βββ inputs
βββ src/lib/feature-2.ts 100kB
Complex filtering combines output and input patterns for precise selection.
Mode: onlyMatching
Select only input files that match a pattern β exclude outputs, imports, and bundler overhead.
Selection Options
{
mode: 'onlyMatching',
includeInputs: ['**/lib/utils/format.ts']
}
Selection Result:
stats.json
βββ outputs
βββ dist/chunks/chunk-U6O5K65G.js 100kB // excludes overhead
βββ inputs
βββ π― src/lib/utils/format.ts 100kB // matches `includeInputs`
Only the bytes from matching input files are counted, excluding bundler overhead.
Mode: bundle
Include the full output bundle with overhead and its bundled inputs but not external chunks.
Selection Options
{
mode: 'bundle',
includeOutputs: ['**/dist/index.js']
}
Selection Result:
stats.json
βββ outputs
βββ π― dist/index.js 209kB // matches `includeOutputs`
βββ inputs
βββ src/lib/utils/format.ts 100kB
βββ src/lib/utils/math.ts 100kB
Only what's bundled directly in the output file is included.
Mode: withStartupDeps
Include the output and all static imports required at startup.
Selection Options
{
mode: 'withStartupDeps',
includeOutputs: ['**/dist/index.js']
}
Selection Result:
stats.json
βββ outputs
βββ π― dist/index.js 209kB
β βββ inputs
β βββ src/lib/utils/format.ts 100kB
β βββ src/lib/utils/math.ts 100kB
βββ π dist/chunks/chunk-U6O5K65G.js 109kB // statically imported by `dist/index.js`
βββ inputs
βββ src/lib/utils/log.ts 100kB
Static imports are preserved even if they would be excluded by patterns.
Mode: withAllDeps
Include the output and all imported files β both static and dynamic.
Selection Options
{
mode: 'withAllDeps',
includeOutputs: ['**/dist/index.js']
}
Selection Result:
stats.json
βββ outputs
βββ π― dist/index.js 209kB
β βββ inputs
β βββ src/lib/utils/format.ts 100kB
β βββ src/lib/utils/math.ts 100kB
βββ π dist/chunks/chunk-U6O5K65G.js 109kB // static import
β βββ inputs
β βββ src/lib/utils/log.ts 100kB
βββ π dist/chunks/feature-2-X2YVDBQK.js 109kB // dynamic import
βββ inputs
βββ src/lib/feature-2.ts 100kB
Both static and dynamic dependencies are included in their entirety.
Scoring
What makes up the output bytes:
- Inputs: Sum of
bytesInOutput
from contributing files- Overhead: Bundler-generated code (runtime, wrappers, loaders, etc.)
π
output.bytes = inputs + overhead
The plugin assigns a score in the range [0 β¦ 1]
to each artefact (or artefact selection) based on:
- Size vs. a configurable maximum threshold
- Diagnostics penalties (errors & warnings, including blacklist violations as warnings)
A perfect score (1
) means βwithin budgetβ; lower values indicate regressions.
The selection process of a scored set of files is explained in detail in File Selection
Types
export interface ScoringOptions {
// targeting output path of a `OutputNode`
totalSize: number | MinMax;
penalty: {
artefactSize?: number | MinMax;
blacklist?: string[];
}
Example Configuration
const selection: SelectionOptions = {
includeOutputs: ['**/features/*'],
excludeOutputs: ['**/features/legacy/**'],
excludeInputs: ['**/ui/**'],
includeEntryPoints: ['**/features/auth/*.ts'],
};
Total Size
Every artefact selection has budgets assigned under budget
.
totalSize
: Total bytes of all files in a selection.
Examples
const scoring1: Scoring = {
totalSize: 300_000,
};
const scoring2: Scoring = {
totalSize: [10_000, 300_000],
};
Panelties
To give actionable feedback to users of the plugin you can add penalties a set of penalties:
artefactSize
: Byte size of a files in a selection.blacklist
: List of globs to flag inputs as well as imports as forbidden
Types
type Penalty = {
artefactSize?: number | MinMax;
blacklist?: string[];
};
Example Configuration
const penalty1: Penalty = {
artefactSize: 50_000,
};
const penalty2: Penalty = {
artefactSize: [1_000, 50_000],
blacklist: ['node_modules/old-charting-lib'],
};
Scoring Parameters
Parameter | Description |
---|---|
S |
Actual bytes |
M |
Size threshold bytes |
E |
Count of issues of severity errors (π¨) |
W |
Count of issues of severity warnings ( |
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
To give users actionable feedback we need to be able to tell WHAT happened, WHERE it is, and HOW to fix it.
Issues are configured per audit under the scoring.penalty
property.
The plugin creates diagnostics for each penalty. The table below shows all diagnostic types:
Diagnostic | Description | Config Key | Default | Severity | Recommended Action |
---|---|---|---|---|---|
Blacklisted | Artifact contains an import matching a forbidden glob pattern. | blacklist |
β | π¨ Error | Remove or replace the forbidden dependency. |
Too Large | Artifact exceeds the maximum allowed size. May indicate an unoptimized bundle or accidental check-in. | maxArtifactSize |
5 MB |
π¨ Error | Review and optimize (e.g. code splitting, compression). |
Too Small | Artifact is below the minimum expected size. Could signal missing dependencies or incomplete build. | minArtifactSize |
1 KB |
Verify that the build output is complete and dependencies are included. |
Too Large Issues
Artifacts that exceed the maximum allowed size threshold. This typically indicates unoptimized bundles, accidental inclusion of large files, or missing code splitting strategies.
Configuration: scoring.artifactSize
Example Issues:
Severity | Message | Source file | Location |
---|---|---|---|
π¨ error | main.js is 6.12 MB (exceeds 5 MB); consider splitting or compressing this bundle. |
dist/lib/main.js |
|
π¨ error | vendor.js is 2.05 MB (exceeds 1.5 MB); apply tree-shaking or extract shared dependencies. |
dist/lib/vendor.js |
Use Cases:
- Code Splitting: Break large bundles into smaller chunks
- Tree Shaking: Remove unused code from dependencies
- Compression: Enable gzip/brotli compression
- Asset Optimization: Optimize images and other assets
- Lazy Loading: Load code only when needed
Too Small Issues
Artifacts that fall below the minimum expected size threshold. This could signal missing dependencies, incomplete builds, or configuration issues.
Configuration: scoring.artifactSize
Example Issues:
Severity | Message | Source file | Location |
---|---|---|---|
utils.js is 50 kB (below 100 kB); confirm that expected dependencies are included. |
dist/lib/utils.js |
||
styles.css is 10 B (below 1 kB); confirm that expected dependencies are included. |
dist/lib/styles.css |
Use Cases:
- Dependency Check: Verify all required dependencies are included
- Build Verification: Ensure the build process completed successfully
- Configuration Review: Check bundler configuration for missing entries
- Source Validation: Confirm source files contain expected content
Blacklisted Issues
Artifacts containing imports that match forbidden glob patterns. This helps enforce dependency policies and prevent usage of deprecated or unsafe libraries.
Configuration: scoring.blacklist
Example Issues:
Severity | Message | Source file | Location |
---|---|---|---|
π¨ error | node_modules/old-charting-lib/index.js matches a blacklist pattern; remove or replace this dependency. |
src/main-ASDFGAH.js |
Use Cases:
- Dependency Replacement: Replace blacklisted libraries with approved alternatives
- Code Refactoring: Remove usage of forbidden dependencies
- Policy Review: Update dependency policies if needed
- Security Audit: Investigate security implications of blacklisted dependencies
Insights Table
The insights table summarizes different areas of the selected files by grouping data based on patterns. It aggregates bytes from outputs, their inputs, and overhead.
Process for accurate data:
For each group, evaluate its patterns and:
- Sum bytes from inputs whose paths match the pattern
- Include output file bytes if the file path matches the pattern (including bundler overhead)
Each byte can only be assigned once to avoid duplication.
After processing:
- Unmatched bytes go to the fallback "Rest" group
- Remaining bundler overhead (from unmatched outputs) is also included in Rest
Types
type InsightsOptions = {
title?: string;
patterns: string[];
icon?: string;
}[];
Complete Example Configuration
const insightsOptions: InsightsOptions = [
{
title: "App Shell",
icon: "π₯οΈ",
patterns: ["**/app/**", "**/main.ts"]
},
{
title: "Angular",
icon: "π
°οΈ",
patterns: [
'**/node_modules/@angular/**',
'**/node_modules/ngx-*/**',
'**/node_modules/@ngrx/**',
'**/node_modules/ng-*/**',
'**/node_modules/*angular*',
"**/zone.js/**"
]
},
{
title: "Styles",
icon: "π¨",
patterns: ["**/*.css", "**/*.scss"]
},
{
title: "Shared Utilities",
icon: "π§©",
patterns: ["**/shared/**", "**/utils/**"]
},
{
title: "Feature: Checkout",
icon: "π",
patterns: ["**/features/checkout/**"]
},
{
title: "Feature: Profile",
icon: "π€",
patterns: ["**/features/profile/**"]
},
{
title: "Third-party Vendors",
icon: "π¦",
patterns: ["**/node_modules/**"]
}
];
Example Output:
Group | Size | Modules |
---|---|---|
π¦ Third-party Vendors | 1.78 MB | 152 |
π₯οΈ App Shell | 85.32 kB | 14 |
236.4 kB | 19 | |
π¨ Styles | 12.2 kB | 7 |
π§© Shared Utilities | 98.3 kB | 28 |
π Feature: Checkout | 142.9 kB | 33 |
π€ Feature: Profile | 65.7 kB | 21 |
Rest | 27.4 kB | 6 |
Dependency Tree
The dependency tree provides users with a quick understanding of the dependencies graph of selected artifacts. It serves as a replacement for opening bundle stats in the browser to search for respective files.
Types
export type ViewMode = 'all' | 'onlyMatching';
export type GeneralViewConfig = {
enabled: boolean;
mode: ViewMode;
}
export type DependencyTreeConfig = GeneralViewConfig & {
groups: GroupingRule[] | false;
pruning: PruningConfig;
}
export interface GroupingRule {
include: string | string[];
exclude?: string | string[];
title?: string;
icon?: string;
numSegments?: number;
}
export interface PruningConfig {
maxChildren?: number;
maxDepth?: number;
startDepth?: number;
minSize?: number;
pathLength?: number | false;
}
Example Configuration
const treeConfig: DependencyTreeConfig = {
mode: 'all',
groups: [
{
title: 'Angular Router',
include: ['node_modules/@angular/router/**'],
exclude: ['**/*.spec.ts'],
icon: 'π
°οΈ',
numSegments: 3
}
],
pruning: {
maxChildren: 3,
maxDepth: 2,
startDepth: 1,
minSize: 10_000,
pathLength: 50
}
};
All examples target this stats data structure.
Example Stats
stats.json βββ outputs βββ dist/index.js 400kB βββ inputs β βββ src/index.ts 50kB β βββ src/lib/feature-1.ts 100kB β βββ src/lib/utils/format.ts 75kB βββ imports βββ dist/chunks/vendor.js
Mode: all
Shows complete dependency tree including all outputs, inputs, and imports with full bundler overhead.
Tree Configuration
{
mode: 'all'
}
Tree Result:
example-group
βββ dist/index.js
β βββ src/index.ts
β βββ src/lib/feature-1.ts
β βββ src/lib/utils/format.ts
βββ dist/chunks/vendor.js
β βββ node_modules/@angular/router/index.ts
βββ dist/styles.css
Mode: onlyMatching
Shows only the bytes that match selection patterns, excluding bundler overhead.
Tree Configuration
{
mode: 'onlyMatching'
}
Tree Result:
example-group
βββ dist/index.js
β βββ src/index.ts
β βββ src/lib/feature-1.ts
β βββ src/lib/utils/format.ts
βββ dist/chunks/vendor.js
β βββ node_modules/@angular/router/index.ts
βββ dist/styles.css
Grouping - Disabled
Tree Result:
example-group
βββ entry-2.js
βββ node_modules/@angular/router/provider.ts
βββ node_modules/@angular/router/service.ts
β βββ node_modules/@angular/router/utils.ts
βββ node_modules/lodash/chunk.js
Grouping - Basic
Grouping Configuration
{
groups: [
{
title: 'Angular Router',
include: ['node_modules/@angular/router/**'],
exclude: ['**/*.spec.ts'],
icon: 'π
°οΈ'
}
]
}
Tree Result:
example-group
βββ entry-2.js
βββ π
°οΈ Angular Router
β βββ node_modules/@angular/router/provider.ts
β βββ node_modules/@angular/router/service.ts
β βββ node_modules/@angular/router/utils.ts
βββ node_modules/lodash/chunk.js
Grouping - Include/Exclude
GroupingRule supports flexible pattern matching with include/exclude logic:
include
: Patterns to match files for inclusion in the groupexclude
: Patterns to exclude from the group (optional)- Pattern precedence: Files matching both include and exclude will be excluded
Advanced Grouping Configuration
{
groups: [
{
title: 'React Components',
include: ['**/components/**/*.tsx', '**/components/**/*.jsx'],
exclude: ['**/*.test.*', '**/*.spec.*'],
icon: 'βοΈ'
},
{
title: 'Node Modules',
include: ['node_modules/**'],
exclude: ['node_modules/**/*.d.ts'],
icon: 'π¦'
}
]
}
Grouping - NumSegments
The numSegments
property controls how files are grouped by their path structure.
Without numSegments:
example-group
βββ entry.js
βββ src/components/ui/Button.tsx
βββ src/components/ui/Modal.tsx
βββ src/components/forms/Input.tsx
βββ src/components/forms/Select.tsx
Grouping Configuration
{
groups: [
{
title: 'Components',
include: ['**/components/**'],
numSegments: 2
}
]
}
With numSegments:
example-group
βββ entry.js
βββ Components
βββ ui
β βββ Button.tsx
β βββ Modal.tsx
βββ forms
βββ Input.tsx
βββ Select.tsx
Pruning - MaxChildren
Unpruned:
example-group
βββ index.js
β βββ src/app.ts
β βββ src/components/Header.ts
β βββ src/components/Footer.ts
β βββ src/utils/math.ts
β βββ src/main.css
βββ vendor.js
β βββ node_modules/react.ts
β βββ node_modules/react-dom.ts
β βββ node_modules/lodash.js
βββ logo.svg
Pruning Configuration
{
pruning: {
maxChildren: 3,
maxDepth: 2
}
}
Pruned Result:
example-group
βββ index.js
β βββ src/app.ts
β βββ src/components/Header.ts
β βββ β¦ 3 more inputs
βββ vendor.js
β βββ node_modules/react.ts
β βββ β¦ 2 more inputs
βββ logo.svg
Pruning - MinSize
With small files:
example-group 840 kB 10 files
βββ index.js 840 kB 9 files
βββ src/app.js 400 kB
βββ src/large-1.js 200 kB
βββ src/medium-1.js 100 kB
βββ src/small-1.js 30 kB
βββ src/small-2.js 25 kB
βββ src/small-3.js 20 kB
βββ β¦ 4 more files 65 kB
Pruning Configuration
{
pruning: {
minSize: 50_000
}
}
Pruned Result:
example-group 840 kB 10 files
βββ index.js 840 kB 9 files
βββ src/app.js 400 kB
βββ src/large-1.js 200 kB
βββ src/medium-1.js 100 kB
βββ β¦ 6 more files 140 kB
Pruning - StartDepth
Controls the depth at which the tree analysis begins, useful for skipping top-level wrapper nodes.
Without startDepth:
example-group
βββ main-bundle
βββ app-core
βββ src/components/Button.tsx
βββ src/components/Modal.tsx
βββ src/utils/helpers.ts
Pruning Configuration
{
pruning: {
startDepth: 2
}
}
With startDepth:
example-group
βββ src/components/Button.tsx
βββ src/components/Modal.tsx
βββ src/utils/helpers.ts
Pruning - PathLength
Controls how long file paths can be before truncation, or disables truncation entirely.
Full path:
example-group
βββ src/lib/utils/helper/format/left-pad.js
Pruning Configuration
{
pruning: {
pathLength: 30
}
}
Shortened path:
example-group
βββ src/.../left-pad.js
Disable truncation:
{
pruning: {
pathLength: false
}
}
Formatting - Size
Raw bytes:
example-group 537170
βββ main.js 300000
Formatted:
example-group 537.17 kB
βββ main.js 300 kB
Formatting - Pluralization
Unpluralized:
example-group 3
βββ main.js 1
βββ utils.js 2
Pluralized:
example-group 3 modules
βββ main.js 1 module
βββ utils.js 2 modules
Formatting - RedundantInfo
With redundancy:
example-group 300 kB 3 modules
βββ index.js 100 kB 1 module
βββ src/app.js 100 kB 1 module
βββ β¦ 2 more inputs 200 kB 2 modules
Cleaned up:
example-group 300 kB 3 modules
βββ index.js 100 kB
βββ src/app.js
βββ β¦ 2 more inputs 200 kB
Insights Table
The grouping table provides a summary view of bundle statistics organized by user-defined groups. It aggregates file sizes and counts for quick analysis of different parts of your bundle.
Types
export type PatternList = readonly string[];
export type SharedViewConfig = {
enabled?: boolean;
mode?: 'onlyMatching' | 'all';
};
export interface TablePruningConfig {
enabled?: boolean;
maxChildren?: number;
minSize?: number;
}
export type InsightsTableConfig = SharedViewConfig & {
groups: GroupingRule[];
pruning?: TablePruningConfig;
};
export interface GroupingRule {
includeInputs: string | PatternList;
excludeInputs?: string | PatternList;
title?: string;
icon?: string;
numSegments?: number;
}
Example Configuration
const tableConfig: InsightsTableConfig = {
mode: 'all',
groups: [
{
title: 'Features',
includeInputs: ['**/feature-*.ts'],
excludeInputs: ['**/*.spec.ts'],
icon: 'π―'
},
{
title: 'Utils',
includeInputs: ['**/utils/**'],
icon: 'π§'
}
],
pruning: {
enabled: true,
maxChildren: 10,
minSize: 1000
}
};
All examples target this stats data structure.
Example Stats
stats.json βββ outputs βββ dist/app.js 300kB β βββ inputs β β βββ src/index.ts 50kB β β βββ src/feature-1.ts 100kB β β βββ src/feature-2.ts 75kB β β βββ src/utils/format.ts 25kB β βββ imports β βββ dist/chunks/vendor.js βββ dist/chunks/vendor.js 200kB βββ inputs βββ node_modules/lodash/index.js 150kB
Mode: all
Shows complete bundle statistics including outputs, inputs, imports and bundler overhead.
Table Configuration
{
mode: 'all',
groups: [
{
title: 'Features',
includeInputs: ['**/feature-*.ts']
}
]
}
Table Result:
Group | Modules | Size |
---|---|---|
π― Features | 2 | 175 kB |
Rest | - | 325 kB |
Mode: onlyMatching
Shows only the bytes that match selection patterns, excluding bundler overhead.
Table Configuration
{
mode: 'onlyMatching',
groups: [
{
title: 'Features',
includeInputs: ['**/feature-*.ts']
}
]
}
Table Result:
Group | Modules | Size |
---|---|---|
π― Features | 2 | 175 kB |
Grouping - Include
Select files using single or multiple glob patterns.
Group Configuration
{
groups: [
{
title: 'Source Code',
includeInputs: ['**/src/**/*.ts', '**/src/**/*.tsx']
}
]
}
Table Result:
Group | Modules | Size |
---|---|---|
Source Code | 4 | 250 kB |
Rest | - | 250 kB |
Grouping - Include/Exclude
Combine include and exclude patterns for precise file selection.
Group Configuration
{
groups: [
{
title: 'Application Code',
includeInputs: '**/src/**',
excludeInputs: ['**/utils/**', '**/*.spec.ts']
}
]
}
File Matching:
stats.json
βββ outputs
βββ dist/app.js
βββ π― src/index.ts // included by **/src/**
βββ π― src/feature-1.ts // included by **/src/**
βββ π― src/feature-2.ts // included by **/src/**
βββ β src/utils/format.ts // excluded by **/utils/**
Table Result:
Group | Modules | Size |
---|---|---|
Application Code | 3 | 225 kB |
Rest | - | 275 kB |
Grouping - Icons and Titles
Customize group display with icons and titles, or let titles be auto-generated from patterns.
Manual Titles with Icons:
{
groups: [
{
title: 'Core Features',
includeInputs: '**/features/**/*.ts',
icon: 'π―'
},
{
title: 'Shared Utils',
includeInputs: '**/shared/**',
icon: 'π§'
},
{
title: 'Third Party',
includeInputs: 'node_modules/**',
icon: 'π¦'
}
]
}
Table Result:
Group | Modules | Size |
---|---|---|
π― Core Features | 8 | 450 kB |
π§ Shared Utils | 12 | 125 kB |
π¦ Third Party | 45 | 2.1 MB |
Rest | - | 75 kB |
Auto-Generated Titles:
{
groups: [
{
includeInputs: '**/components/feature-*.tsx'
},
{
includeInputs: ['**/utils/**', '**/helpers/**']
}
]
}
Table Result:
Group | Modules | Size |
---|---|---|
components/feature-* | 5 | 175 kB |
utils/, helpers/ | 8 | 125 kB |
Rest | - | 200 kB |
Grouping - NumSegments
Control how paths are grouped using the numSegments
property for hierarchical organization.
Without numSegments:
βββ src/components/ui/Button.tsx
βββ src/components/ui/Modal.tsx
βββ src/components/forms/Input.tsx
βββ src/components/forms/Select.tsx
Group Configuration
{
groups: [
{
title: 'Components',
includeInputs: '**/components/**',
numSegments: 2
}
]
}
With numSegments (creates subgroups):
βββ Components
βββ ui (Button.tsx, Modal.tsx)
βββ forms (Input.tsx, Select.tsx)
Rest Group - Unmatched Files
Files that don't match any group pattern are collected in the "Rest" group.
Group Configuration
{
groups: [
{
title: 'Features Only',
includeInputs: '**/feature-*.ts'
}
]
}
File Matching:
stats.json
βββ outputs
βββ dist/app.js 300kB
βββ π― src/feature-1.ts // matches group
βββ π― src/feature-2.ts // matches group
βββ β src/index.ts // unmatched -> Rest
βββ β src/utils/format.ts // unmatched -> Rest
Table Result:
Group | Modules | Size |
---|---|---|
Features Only | 2 | 175 kB |
Rest | - | 275 kB |
Rest Group - Bundler Overhead
When using onlyMatching
mode, bundler overhead becomes part of the Rest group.
Group Configuration
{
mode: 'onlyMatching',
groups: [
{
title: 'Source Files',
includeInputs: '**/src/**'
}
]
}
File Analysis:
dist/app.js total: 300kB
βββ src files: 250kB // matches group
βββ bundler overhead: 50kB // becomes Rest
Table Result:
Group | Modules | Size |
---|---|---|
Source Files | 4 | 250 kB |
Rest | - | 50 kB |
Pruning - Max Children
Limit the number of groups displayed in the table.
Group Configuration
{
groups: [
{ title: 'Features', includeInputs: '**/feature-*.ts' },
{ title: 'Utils', includeInputs: '**/utils/**' },
{ title: 'Components', includeInputs: '**/components/**' },
{ title: 'Services', includeInputs: '**/services/**' },
{ title: 'Helpers', includeInputs: '**/helpers/**' }
],
pruning: {
enabled: true,
maxChildren: 3
}
}
Table Result:
Group | Modules | Size |
---|---|---|
Features | 2 | 175 kB |
Components | 5 | 125 kB |
Services | 3 | 100 kB |
Rest | - | 200 kB |
Utils and Helpers groups were moved to Rest due to maxChildren limit
Pruning - Min Size
Filter out groups smaller than the specified threshold.
Group Configuration
{
groups: [
{ title: 'Large Feature', includeInputs: '**/large-feature.ts' }, // 100kB
{ title: 'Medium Feature', includeInputs: '**/medium-feature.ts' }, // 50kB
{ title: 'Small Feature', includeInputs: '**/small-feature.ts' }, // 10kB
{ title: 'Tiny Feature', includeInputs: '**/tiny-feature.ts' } // 2kB
],
pruning: {
enabled: true,
minSize: 25000 // 25kB threshold
}
}
Table Result:
Group | Modules | Size |
---|---|---|
Large Feature | 1 | 100 kB |
Medium Feature | 1 | 50 kB |
Rest | - | 262 kB |
Small and Tiny features were moved to Rest due to minSize threshold
Implementation details
Limitations
From feedback sessions on this issue we collected a couple of things that popped up regularly but are not directly possible with the plugin.
- Any action triggered by a comparison of 2 measures.
- increased size by X%
- increased size by XkB
- increased files by X
For example to implement a failing CI on 20% increase for a specific audit you would have to read the comparison json created by the GH action or over the @code-pushup/ci
package directly and process it with your custom logic.
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[Merge Options]
F[Group Stats by Audits]
G[Compute Size Scores]
H[Compute Issue Penalties]
I[Score Audit]
subgraph Generate Audits
J[Add Issues]
K[Add Table]
L[Add Tree]
end
A --> D
B --> D
C --> D
D --> E
E --> F
F --> G
F --> H
G --> I
H --> I
I --> J
J --> K
K --> L
Plugin Configuration
The plugin integrates with supported bundlers to analyze bundle statistics and generate audit reports. Configure the plugin with bundler type, artifact paths, and audit settings.
Types
export type BundleStatsPluginOptions = {
bundler: SUPPORTED_BUNDLERS;
artefactsPaths: string;
generateArtefacts?: string;
audits: BundleStatsAuditOption[];
artefactTree?: TreeOptions;
insightsTable?: InsightsOptions;
scoring?: Omit<ScoringOptions, 'totalSize'>;
}
Minimal Example Configuration
const pluginOptions: PluginOptions = {
bundler: 'webpack',
artefactsPaths: './dist/stats.json',
generateArtefacts: 'esbuild src/index.js --bundle --outfile=dist/bundle.js --metafile=stats.json',
audits: [
{
title: 'Initial Bundles',
selection: {
includeOutputs: ['**/*.js']
},
scoring: {
totalSize: 500_000
}
}
]
};
Artefacts Gathering
The plugin can generate artefacts from the stats.json
file. The plugin can either use an existing stats.json
file or generate a new one if the generateArtefacts
option is provided.
Types
type ArtefactsOptions = {
bundler: SUPPORTED_BUNDLERS;
artefactsPaths: string;
generateArtefacts?: string;
}
Example Configuration:
const options: PluginOptions = {
bundler: 'esbuild',
artefactsPaths: './dist/stats.json'
// ...
};
Full Example Configuration:
const options: PluginOptions = {
bundler: 'esbuild',
artefactsPaths: './dist/stats.json',
generateArtefacts: 'esbuild src/index.js --bundle --outfile=dist/bundle.js --metafile=stats.json',
// ...
};
Options Merging
The Plugin and audit share a set of options:
insightsTable
- The insights to use (grouping)selection
- The filtering of output files to analysescoring
- The scoring penalty (not the totalSize) to use (penalty)artefactTree
- The artefact tree to use (pruning, grouping, formatting)
// π Options Merging β Plugin vs Audit-Level Config
const pluginOptions: PluginOptions = {
artefactTree: {
groups: ['shared/'], // π₯ merged into audit.artefactTree.groups
pruning: { maxDepth: 3 }, // β overwritten by audit.artefactTree.pruning
},
insightsTable: ['node_modules', 'zone.js'], // π₯ merged into audit.insightsTable
selection: {
}
scoring: {
penalty: {
artefactSize: [0, 3_000_000],
blacklist: ['/*.min.js'], // π₯ merged into audit.penalty.blacklist
},
},
audits: [
{
title: 'Initial Size Audit',
scoring: {
totalSize: 500_000, // π overwrites plugin totalSize
penalty: {
blacklist: ['/legacy/'], // π₯ merged with plugin.penalty.blacklist
},
},
insightsTable: ['feature:core'], // π₯ merged with plugin.insightsTable
artefactTree: {
groups: ['features/**'], // π₯ merged with plugin.artefactTree.groups
pruning: { maxChildren: 10 }, // β overwrites plugin.artefactTree.pruning
},
},
],
};
Audit Configuration
Each audit defines a specific bundle analysis with its own selection criteria, scoring thresholds, and reporting options. Audits can override plugin-level settings or inherit them for consistency across multiple audits.
Types
export type BundleStatsAuditOption = {
slug?: string;
title: string;
description?: string;
selection: SelectionOptions;
scoring: ScoringOptions;
artefactTree?: TreeOptions;
insightsTable?: InsightsOptions;
}
Minimal Example Configuration
const auditConfig: BundleStatsAuditOption = {
title: 'All Bundles',
selection: { includeOutputs: ['**/*.js'] },
scoring: { totalSize: 500_000 }
};
Full Example Configuration
const auditConfig: BundleStatsAuditOption = {
slug: 'initial-bundles',
title: 'Initial Bundle Size',
description: 'Monitors the size of initial JavaScript bundles loaded on page load.',
selection: {
includeOutputs: ['**/main.js', '**/vendor.js'],
excludeOutputs: ['**/chunks/**']
},
scoring: {
totalSize: 500_000,
penalty: {
artefactSize: [50_000, 200_000],
blacklist: ['**/legacy/**', '**/deprecated/**']
}
},
artefactTree: {
groups: [
{
title: 'App Code',
patterns: ['**/src/**'],
icon: 'π―'
},
{
title: 'Node Modules',
patterns: ['**/node_modules/**'],
icon: 'π¦'
}
],
pruning: {
maxChildren: 3,
maxDepth: 1
}
},
insightsTable: [
{
title: 'App Code',
patterns: ['**/src/**'],
icon: 'π―'
},
{
title: 'Node Modules',
patterns: ['**/node_modules/**'],
icon: 'π¦'
}
]
};
Market Research - Viewer
sonda.dev
Repo: https://sonda.dev/
Market Research - CI
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%