diff --git a/CHANGELOG.md b/CHANGELOG.md
index e30017e9..a2c2a39d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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))
diff --git a/README.md b/README.md
index 99b8a156..662ceeba 100644
--- a/README.md
+++ b/README.md
@@ -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!
Options (for plugin)
@@ -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).|
@@ -122,9 +122,9 @@ Directory containing all generated bundles.
-r, --report Path to bundle report file that will be generated in `static` mode. (default: report.html)
-t, --title String to use in title element of html report. (default: pretty printed current date)
-s, --default-sizes 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 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 Assets that should be excluded from the report.
Can be specified multiple times.
@@ -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)
+
Selecting Which Chunks to Display
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.
diff --git a/client/components/ModulesTreemap.jsx b/client/components/ModulesTreemap.jsx
index f5cf5d1c..14d707e5 100644
--- a/client/components/ModulesTreemap.jsx
+++ b/client/components/ModulesTreemap.jsx
@@ -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;
};
diff --git a/client/store.js b/client/store.js
index 59bbeaf3..79608027 100644
--- a/client/store.js
+++ b/client/store.js
@@ -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;
diff --git a/src/analyzer.js b/src/analyzer.js
index d684928f..93600a25 100644
--- a/src/analyzer.js
+++ b/src/analyzer.js
@@ -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
@@ -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] ?? {}
}));
diff --git a/src/bin/analyzer.js b/src/bin/analyzer.js
index f551d55d..688b4dee 100755
--- a/src/bin/analyzer.js
+++ b/src/bin/analyzer.js
@@ -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)
diff --git a/src/sizeUtils.js b/src/sizeUtils.js
index 4d6bd855..3eef93cd 100644
--- a/src/sizeUtils.js
+++ b/src/sizeUtils.js
@@ -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}.`);
}
diff --git a/src/tree/ConcatenatedModule.js b/src/tree/ConcatenatedModule.js
index 34ee7c05..c3871776 100644
--- a/src/tree/ConcatenatedModule.js
+++ b/src/tree/ConcatenatedModule.js
@@ -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];
diff --git a/src/tree/ContentFolder.js b/src/tree/ContentFolder.js
index c58d668e..70e8c24c 100644
--- a/src/tree/ContentFolder.js
+++ b/src/tree/ContentFolder.js
@@ -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];
@@ -33,6 +37,7 @@ export default class ContentFolder extends BaseFolder {
parsedSize: this.parsedSize,
gzipSize: this.gzipSize,
brotliSize: this.brotliSize,
+ zstdSize: this.zstdSize,
inaccurateSizes: true
};
}
diff --git a/src/tree/ContentModule.js b/src/tree/ContentModule.js
index 116a23c2..872c8297 100644
--- a/src/tree/ContentModule.js
+++ b/src/tree/ContentModule.js
@@ -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];
diff --git a/src/tree/Folder.js b/src/tree/Folder.js
index 5d9aeb97..169921b4 100644
--- a/src/tree/Folder.js
+++ b/src/tree/Folder.js
@@ -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`;
@@ -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
};
}
diff --git a/src/tree/Module.js b/src/tree/Module.js
index 079871d8..9adb2d5c 100644
--- a/src/tree/Module.js
+++ b/src/tree/Module.js
@@ -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() {
@@ -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;
}
@@ -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)) {
@@ -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
};
}
diff --git a/src/viewer.js b/src/viewer.js
index 81028f21..016a867f 100644
--- a/src/viewer.js
+++ b/src/viewer.js
@@ -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;
}
diff --git a/test/analyzer.js b/test/analyzer.js
index ee019a31..16ee23e7 100644
--- a/test/analyzer.js
+++ b/test/analyzer.js
@@ -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;
@@ -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');
diff --git a/test/plugin.js b/test/plugin.js
index ff097d16..1ae77719 100644
--- a/test/plugin.js
+++ b/test/plugin.js
@@ -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 () {
@@ -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});
+ });
+ }
});
});
@@ -208,7 +216,9 @@ describe('Plugin', function () {
label: bundleLabel,
statSize,
parsedSize,
- gzipSize
+ gzipSize,
+ brotliSize: opts.brotliSize,
+ zstdSize: opts.zstdSize
});
}