From 67b3e14303819b247aff9e9b765b46aa50ed2c1b Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Mon, 19 Jan 2026 11:17:29 -0500 Subject: [PATCH] feat: add support for Zstandard compression algorithm --- client/components/ModulesTreemap.jsx | 2 ++ client/store.js | 2 +- src/analyzer.js | 2 ++ src/bin/analyzer.js | 2 +- src/sizeUtils.js | 1 + src/tree/ConcatenatedModule.js | 4 ++++ src/tree/ContentFolder.js | 5 +++++ src/tree/ContentModule.js | 4 ++++ src/tree/Folder.js | 7 ++++++- src/tree/Module.js | 12 +++++++++++- src/viewer.js | 2 +- test/analyzer.js | 5 +++++ test/plugin.js | 10 +++++++++- 13 files changed, 52 insertions(+), 6 deletions(-) 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..e65aefc3 100755 --- a/src/bin/analyzer.js +++ b/src/bin/analyzer.js @@ -11,7 +11,7 @@ const Logger = require('../Logger'); const utils = require('../utils'); const SIZES = new Set(['stat', 'parsed', 'gzip']); -const COMPRESSION_ALGORITHMS = new Set(['gzip', 'brotli']); +const COMPRESSION_ALGORITHMS = new Set(['gzip', 'brotli', 'zstd']); const program = commander .version(require('../../package.json').version) diff --git a/src/sizeUtils.js b/src/sizeUtils.js index 4d6bd855..7d181612 100644 --- a/src/sizeUtils.js +++ b/src/sizeUtils.js @@ -3,6 +3,7 @@ const zlib = require('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') 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..75756758 100644 --- a/test/analyzer.js +++ b/test/analyzer.js @@ -270,6 +270,11 @@ describe('Analyzer', function () { expect(await getCompressionAlgorithm()).to.equal('gzip'); }); + 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..324b24db 100644 --- a/test/plugin.js +++ b/test/plugin.js @@ -188,6 +188,12 @@ describe('Plugin', function () { await webpackCompile(config, '4.44.2'); await expectValidReport({parsedSize: 1311, gzipSize: undefined, brotliSize: 302}); }); + + 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 +214,9 @@ describe('Plugin', function () { label: bundleLabel, statSize, parsedSize, - gzipSize + gzipSize, + brotliSize: opts.brotliSize, + zstdSize: opts.zstdSize }); }