Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ _Note: Gaps between patch versions are faulty, broken or test releases._

## UNRELEASED

* **New Feature**
* Add support for Zstandard compression ([#693](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/693) by [@bjohansebas](https://github.com/bjohansebas))

* **Internal**
* Update `ws` dependency ([#691](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/691) by [@bjohansebas](https://github.com/bjohansebas))

Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ This module will help you:
4. Optimize it!

And the best thing is it supports minified bundles! It parses them to get real size of bundled modules.
And it also shows their gzipped or Brotli sizes!
And it also shows their gzipped, Brotli, or Zstandard sizes!

<h2 align="center">Options (for plugin)</h2>

Expand All @@ -62,7 +62,7 @@ new BundleAnalyzerPlugin(options?: object)
|**`reportFilename`**|`{String}`|Default: `report.html`. Path to bundle report file that will be generated in `static` mode. It can be either an absolute path or a path relative to a bundle output directory (which is output.path in webpack config).|
|**`reportTitle`**|`{String\|function}`|Default: function that returns pretty printed current date and time. Content of the HTML `title` element; or a function of the form `() => string` that provides the content.|
|**`defaultSizes`**|One of: `stat`, `parsed`, `gzip`, `brotli`|Default: `parsed`. Module sizes to show in report by default. [Size definitions](#size-definitions) section describes what these values mean.|
|**`compressionAlgorithm`**|One of: `gzip`, `brotli`|Default: `gzip`. Compression type used to calculate the compressed module sizes.|
|**`compressionAlgorithm`**|One of: `gzip`, `brotli`, `zstd`|Default: `gzip`. Compression type used to calculate the compressed module sizes.|
|**`openAnalyzer`**|`{Boolean}`|Default: `true`. Automatically open report in default browser.|
|**`generateStatsFile`**|`{Boolean}`|Default: `false`. If `true`, webpack stats JSON file will be generated in bundle output directory|
|**`statsFilename`**|`{String}`|Default: `stats.json`. Name of webpack stats JSON file that will be generated if `generateStatsFile` is `true`. It can be either an absolute path or a path relative to a bundle output directory (which is output.path in webpack config).|
Expand Down Expand Up @@ -122,9 +122,9 @@ Directory containing all generated bundles.
-r, --report <file> Path to bundle report file that will be generated in `static` mode. (default: report.html)
-t, --title <title> String to use in title element of html report. (default: pretty printed current date)
-s, --default-sizes <type> Module sizes to show in treemap by default.
Possible values: stat, parsed, gzip, brotli (default: parsed)
Possible values: stat, parsed, gzip, brotli, zstd (default: parsed)
--compression-algorithm <type> Compression algorithm that will be used to calculate the compressed module sizes.
Possible values: gzip, brotli (default: gzip)
Possible values: gzip, brotli, zstd (default: gzip)
-O, --no-open Don't open report in default browser automatically.
-e, --exclude <regexp> Assets that should be excluded from the report.
Can be specified multiple times.
Expand Down Expand Up @@ -158,6 +158,10 @@ This is the size of running the parsed bundles/modules through gzip compression.

This is the size of running the parsed bundles/modules through Brotli compression.

### `zstd`

This is the size of running the parsed bundles/modules through Zstandard compression. (Node.js 22.15.0+ is required for this feature)

<h2 align="center">Selecting Which Chunks to Display</h2>

When opened, the report displays all of the Webpack chunks for your project. It's possible to filter to a more specific list of chunks by using the sidebar or the chunk context menu.
Expand Down
2 changes: 2 additions & 0 deletions client/components/ModulesTreemap.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ function getSizeSwitchItems() {

if (window.compressionAlgorithm === 'brotli') items.push({label: 'Brotli', prop: 'brotliSize'});

if (window.compressionAlgorithm === 'zstd') items.push({label: 'Zstandard', prop: 'zstdSize'});

return items;
};

Expand Down
2 changes: 1 addition & 1 deletion client/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import localStorage from './localStorage';

export class Store {
cid = 0;
sizes = new Set(['statSize', 'parsedSize', 'gzipSize', 'brotliSize']);
sizes = new Set(['statSize', 'parsedSize', 'gzipSize', 'brotliSize', 'zstdSize']);

@observable.ref allChunks;
@observable.shallow selectedChunks;
Expand Down
2 changes: 2 additions & 0 deletions src/analyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
asset.parsedSize = Buffer.byteLength(assetSources.src);
if (compressionAlgorithm === 'gzip') asset.gzipSize = getCompressedSize('gzip', assetSources.src);
if (compressionAlgorithm === 'brotli') asset.brotliSize = getCompressedSize('brotli', assetSources.src);
if (compressionAlgorithm === 'zstd') asset.zstdSize = getCompressedSize('zstd', assetSources.src);
}

// Picking modules from current bundle script
Expand Down Expand Up @@ -169,6 +170,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
parsedSize: asset.parsedSize,
gzipSize: asset.gzipSize,
brotliSize: asset.brotliSize,
zstdSize: asset.zstdSize,
groups: Object.values(asset.tree.children).map(i => i.toChartData()),
isInitialByEntrypoint: chunkToInitialByEntrypoint[filename] ?? {}
}));
Expand Down
3 changes: 2 additions & 1 deletion src/bin/analyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ const analyzer = require('../analyzer');
const viewer = require('../viewer');
const Logger = require('../Logger');
const utils = require('../utils');
const {isZstdSupported} = require('../sizeUtils');

const SIZES = new Set(['stat', 'parsed', 'gzip']);
const COMPRESSION_ALGORITHMS = new Set(['gzip', 'brotli']);
const COMPRESSION_ALGORITHMS = new Set(isZstdSupported ? ['gzip', 'brotli', 'zstd'] : ['gzip', 'brotli']);

const program = commander
.version(require('../../package.json').version)
Expand Down
5 changes: 5 additions & 0 deletions src/sizeUtils.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
const zlib = require('zlib');

export const isZstdSupported = 'createZstdCompress' in zlib;

export function getCompressedSize(compressionAlgorithm, input) {
if (compressionAlgorithm === 'gzip') return zlib.gzipSync(input, {level: 9}).length;
if (compressionAlgorithm === 'brotli') return zlib.brotliCompressSync(input).length;
if (compressionAlgorithm === 'zstd' && isZstdSupported) {
return zlib.zstdCompressSync(input).length;
}

throw new Error(`Unsupported compression algorithm: ${compressionAlgorithm}.`);
}
4 changes: 4 additions & 0 deletions src/tree/ConcatenatedModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export default class ConcatenatedModule extends Module {
return this.getBrotliSize() ?? this.getEstimatedSize('brotliSize');
}

get zstdSize() {
return this.getZstdSize() ?? this.getEstimatedSize('zstdSize');
}

getEstimatedSize(sizeType) {
const parentModuleSize = this.parent[sizeType];

Expand Down
5 changes: 5 additions & 0 deletions src/tree/ContentFolder.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export default class ContentFolder extends BaseFolder {
return this.getSize('brotliSize');
}

get zstdSize() {
return this.getSize('zstdSize');
}

getSize(sizeType) {
const ownerModuleSize = this.ownerModule[sizeType];

Expand All @@ -33,6 +37,7 @@ export default class ContentFolder extends BaseFolder {
parsedSize: this.parsedSize,
gzipSize: this.gzipSize,
brotliSize: this.brotliSize,
zstdSize: this.zstdSize,
inaccurateSizes: true
};
}
Expand Down
4 changes: 4 additions & 0 deletions src/tree/ContentModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export default class ContentModule extends Module {
return this.getSize('brotliSize');
}

get zstdSize() {
return this.getSize('zstdSize');
}

getSize(sizeType) {
const ownerModuleSize = this.ownerModule[sizeType];

Expand Down
7 changes: 6 additions & 1 deletion src/tree/Folder.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export default class Folder extends BaseFolder {
return this.opts.compressionAlgorithm === 'brotli' ? this.getCompressedSize('brotli') : undefined;
}

get zstdSize() {
return this.opts.compressionAlgorithm === 'zstd' ? this.getCompressedSize('zstd') : undefined;
}

getCompressedSize(compressionAlgorithm) {
const key = `_${compressionAlgorithm}Size`;

Expand Down Expand Up @@ -71,7 +75,8 @@ export default class Folder extends BaseFolder {
...super.toChartData(),
parsedSize: this.parsedSize,
gzipSize: this.gzipSize,
brotliSize: this.brotliSize
brotliSize: this.brotliSize,
zstdSize: this.zstdSize
};
}

Expand Down
12 changes: 11 additions & 1 deletion src/tree/Module.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default class Module extends Node {
this.data.parsedSrc = value;
delete this._gzipSize;
delete this._brotliSize;
delete this._zstdSize;
}

get size() {
Expand All @@ -39,6 +40,10 @@ export default class Module extends Node {
return this.getBrotliSize();
}

get zstdSize() {
return this.getZstdSize();
}

getParsedSize() {
return this.src ? this.src.length : undefined;
}
Expand All @@ -51,6 +56,10 @@ export default class Module extends Node {
return this.opts.compressionAlgorithm === 'brotli' ? this.getCompressedSize('brotli') : undefined;
}

getZstdSize() {
return this.opts.compressionAlgorithm === 'zstd' ? this.getCompressedSize('zstd') : undefined;
}

getCompressedSize(compressionAlgorithm) {
const key = `_${compressionAlgorithm}Size`;
if (!(key in this)) {
Expand Down Expand Up @@ -78,7 +87,8 @@ export default class Module extends Node {
statSize: this.size,
parsedSize: this.parsedSize,
gzipSize: this.gzipSize,
brotliSize: this.brotliSize
brotliSize: this.brotliSize,
zstdSize: this.zstdSize
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function resolveTitle(reportTitle) {
}

function resolveDefaultSizes(defaultSizes, compressionAlgorithm) {
if (['gzip', 'brotli'].includes(defaultSizes)) return compressionAlgorithm;
if (['gzip', 'brotli', 'zstd'].includes(defaultSizes)) return compressionAlgorithm;
return defaultSizes;
}

Expand Down
8 changes: 8 additions & 0 deletions test/analyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const path = require('path');
const del = require('del');
const childProcess = require('child_process');
const puppeteer = require('puppeteer');
const {isZstdSupported} = require('../src/sizeUtils');

let browser;

Expand Down Expand Up @@ -270,6 +271,13 @@ describe('Analyzer', function () {
expect(await getCompressionAlgorithm()).to.equal('gzip');
});

if (isZstdSupported) {
it('should accept --compression-algorithm zstd', async function () {
generateReportFrom('with-modules-chunk.json', '--compression-algorithm zstd');
expect(await getCompressionAlgorithm()).to.equal('zstd');
});
}

it('should default to gzip', async function () {
generateReportFrom('with-modules-chunk.json');
expect(await getCompressionAlgorithm()).to.equal('gzip');
Expand Down
12 changes: 11 additions & 1 deletion test/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const del = require('del');
const path = require('path');
const puppeteer = require('puppeteer');
const BundleAnalyzerPlugin = require('../lib/BundleAnalyzerPlugin');
const {isZstdSupported} = require('../src/sizeUtils');

describe('Plugin', function () {
describe('options', function () {
Expand Down Expand Up @@ -188,6 +189,13 @@ describe('Plugin', function () {
await webpackCompile(config, '4.44.2');
await expectValidReport({parsedSize: 1311, gzipSize: undefined, brotliSize: 302});
});
if (isZstdSupported) {
it('should support zstd', async function () {
const config = makeWebpackConfig({analyzerOpts: {compressionAlgorithm: 'zstd'}});
await webpackCompile(config, '4.44.2');
await expectValidReport({parsedSize: 1311, gzipSize: undefined, brotliSize: undefined, zstdSize: 345});
});
}
});
});

Expand All @@ -208,7 +216,9 @@ describe('Plugin', function () {
label: bundleLabel,
statSize,
parsedSize,
gzipSize
gzipSize,
brotliSize: opts.brotliSize,
zstdSize: opts.zstdSize
});
}

Expand Down