From 912aa8c16c9d6c3fc692747f666d0d2a6d49446b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cs=C3=A1sz=C3=A1r?= Date: Sat, 10 Apr 2021 16:13:45 +0200 Subject: [PATCH 01/10] Drop gzip-size dependency --- package-lock.json | 13 ------------- package.json | 1 - src/analyzer.js | 4 ++-- src/sizeUtils.js | 5 +++++ src/tree/Folder.js | 4 ++-- src/tree/Module.js | 4 ++-- 6 files changed, 11 insertions(+), 20 deletions(-) create mode 100644 src/sizeUtils.js diff --git a/package-lock.json b/package-lock.json index 24e65f86..1a9c4b76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3991,11 +3991,6 @@ "is-obj": "^2.0.0" } }, - "duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" - }, "duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -5604,14 +5599,6 @@ "glogg": "^1.0.0" } }, - "gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "requires": { - "duplexer": "^0.1.2" - } - }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", diff --git a/package.json b/package.json index 386b224f..754d42f9 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "acorn-walk": "^8.0.0", "chalk": "^4.1.0", "commander": "^6.2.0", - "gzip-size": "^6.0.0", "lodash": "^4.17.20", "opener": "^1.5.2", "sirv": "^1.0.7", diff --git a/src/analyzer.js b/src/analyzer.js index be11bbf1..40372749 100644 --- a/src/analyzer.js +++ b/src/analyzer.js @@ -2,12 +2,12 @@ const fs = require('fs'); const path = require('path'); const _ = require('lodash'); -const gzipSize = require('gzip-size'); const Logger = require('./Logger'); const Folder = require('./tree/Folder').default; const {parseBundle} = require('./parseUtils'); const {createAssetsFilter} = require('./utils'); +const {gzipSize} = require('./sizeUtils'); const FILENAME_QUERY_REGEXP = /\?.*$/u; const FILENAME_EXTENSIONS = /\.(js|mjs)$/iu; @@ -102,7 +102,7 @@ function getViewerData(bundleStats, bundleDir, opts) { if (assetSources) { asset.parsedSize = Buffer.byteLength(assetSources.src); - asset.gzipSize = gzipSize.sync(assetSources.src); + asset.gzipSize = gzipSize(assetSources.src); } // Picking modules from current bundle script diff --git a/src/sizeUtils.js b/src/sizeUtils.js new file mode 100644 index 00000000..2ded5442 --- /dev/null +++ b/src/sizeUtils.js @@ -0,0 +1,5 @@ +const zlib = require('zlib'); + +export function gzipSize(input) { + return zlib.gzipSync(input, {level: 9}).length; +} diff --git a/src/tree/Folder.js b/src/tree/Folder.js index 9bcbc006..fb121be6 100644 --- a/src/tree/Folder.js +++ b/src/tree/Folder.js @@ -1,5 +1,5 @@ import _ from 'lodash'; -import gzipSize from 'gzip-size'; +import {gzipSize} from '../sizeUtils'; import Module from './Module'; import BaseFolder from './BaseFolder'; @@ -14,7 +14,7 @@ export default class Folder extends BaseFolder { get gzipSize() { if (!_.has(this, '_gzipSize')) { - this._gzipSize = this.src ? gzipSize.sync(this.src) : 0; + this._gzipSize = this.src ? gzipSize(this.src) : 0; } return this._gzipSize; diff --git a/src/tree/Module.js b/src/tree/Module.js index 5a7c87c5..00db7eeb 100644 --- a/src/tree/Module.js +++ b/src/tree/Module.js @@ -1,5 +1,5 @@ import _ from 'lodash'; -import gzipSize from 'gzip-size'; +import {gzipSize} from '../sizeUtils'; import Node from './Node'; @@ -33,7 +33,7 @@ export default class Module extends Node { get gzipSize() { if (!_.has(this, '_gzipSize')) { - this._gzipSize = this.src ? gzipSize.sync(this.src) : undefined; + this._gzipSize = this.src ? gzipSize(this.src) : undefined; } return this._gzipSize; From 6a4a3612070fc006af8abe026453dd95935cd1c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cs=C3=A1sz=C3=A1r?= Date: Sat, 10 Apr 2021 16:47:18 +0200 Subject: [PATCH 02/10] Single source of thruth for compressed size helper --- src/analyzer.js | 11 ++++++----- src/tree/Folder.js | 10 +++++++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/analyzer.js b/src/analyzer.js index 40372749..c58d5e93 100644 --- a/src/analyzer.js +++ b/src/analyzer.js @@ -20,7 +20,8 @@ module.exports = { function getViewerData(bundleStats, bundleDir, opts) { const { logger = new Logger(), - excludeAssets = null + excludeAssets = null, + compressedSize = gzipSize } = opts || {}; const isAssetIncluded = createAssetsFilter(excludeAssets); @@ -102,7 +103,7 @@ function getViewerData(bundleStats, bundleDir, opts) { if (assetSources) { asset.parsedSize = Buffer.byteLength(assetSources.src); - asset.gzipSize = gzipSize(assetSources.src); + asset.gzipSize = compressedSize(assetSources.src); } // Picking modules from current bundle script @@ -143,7 +144,7 @@ function getViewerData(bundleStats, bundleDir, opts) { } asset.modules = assetModules; - asset.tree = createModulesTree(asset.modules); + asset.tree = createModulesTree(asset.modules, {compressedSize}); return result; }, {}); @@ -203,8 +204,8 @@ function isRuntimeModule(statModule) { return statModule.moduleType === 'runtime'; } -function createModulesTree(modules) { - const root = new Folder('.'); +function createModulesTree(modules, opts) { + const root = new Folder('.', opts); modules.forEach(module => root.addModule(module)); root.mergeNestedFolders(); diff --git a/src/tree/Folder.js b/src/tree/Folder.js index fb121be6..15748227 100644 --- a/src/tree/Folder.js +++ b/src/tree/Folder.js @@ -1,5 +1,4 @@ import _ from 'lodash'; -import {gzipSize} from '../sizeUtils'; import Module from './Module'; import BaseFolder from './BaseFolder'; @@ -8,13 +7,18 @@ import {getModulePathParts} from './utils'; export default class Folder extends BaseFolder { + constructor(name, opts) { + super(name); + this.opts = opts; + } + get parsedSize() { return this.src ? this.src.length : 0; } get gzipSize() { if (!_.has(this, '_gzipSize')) { - this._gzipSize = this.src ? gzipSize(this.src) : 0; + this._gzipSize = this.src ? this.opts.compressedSize(this.src) : 0; } return this._gzipSize; @@ -42,7 +46,7 @@ export default class Folder extends BaseFolder { // See `test/stats/with-invalid-dynamic-require.json` as an example. !(childNode instanceof Folder) ) { - childNode = currentFolder.addChildFolder(new Folder(folderName)); + childNode = currentFolder.addChildFolder(new Folder(folderName, this.opts)); } currentFolder = childNode; From ec1fa85fb1b0c2b3121e4376954bd3ea3e262d63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cs=C3=A1sz=C3=A1r?= Date: Sat, 10 Apr 2021 20:43:59 +0200 Subject: [PATCH 03/10] Change public API from `gzip` to `compressed` To be in line with the new `compressedSize*` options. Supports `gzip` as a synonym for backwards compatibility. Internal naming is also still `gzip*`. --- README.md | 10 +++++----- src/bin/analyzer.js | 7 +++++-- src/viewer.js | 8 ++++++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e522ad75..c5a5bf01 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ new BundleAnalyzerPlugin(options?: object) |**`analyzerPort`**|`{Number}` or `auto`|Default: `8888`. Port that will be used in `server` mode to start HTTP server.| |**`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`|Default: `parsed`. Module sizes to show in report by default. [Size definitions](#size-definitions) section describes what these values mean.| +|**`defaultSizes`**|One of: `stat`, `parsed`, `compressed`|Default: `parsed`. Module sizes to show in report by default. [Size definitions](#size-definitions) section describes what these values mean.| |**`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).| @@ -121,7 +121,7 @@ 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 <type> Module sizes to show in treemap by default. - Possible values: stat, parsed, gzip (default: parsed) + Possible values: stat, parsed, compressed (default: parsed) -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. @@ -147,9 +147,9 @@ It is called "stat size" because it's obtained from Webpack's This is the "output" size of your files. If you're using a Webpack plugin such as Uglify, then this value will reflect the minified size of your code. -### `gzip` +### `compressed` -This is the size of running the parsed bundles/modules through gzip compression. +This is the size of running the parsed bundles/modules through compression. <h2 align="center">Selecting Which Chunks to Display</h2> @@ -169,7 +169,7 @@ The Chunk Context Menu can be opened by right-clicking or `Ctrl`-clicking on a s <h2 align="center">Troubleshooting</h2> -### I don't see `gzip` or `parsed` sizes, it only shows `stat` size +### I don't see `compressed` or `parsed` sizes, it only shows `stat` size It happens when `webpack-bundle-analyzer` analyzes files that don't actually exist in your file system, for example when you work with `webpack-dev-server` that keeps all the files in RAM. If you use `webpack-bundle-analyzer` as a plugin you won't get any errors, however if you run it via CLI you get the error message in terminal: ``` diff --git a/src/bin/analyzer.js b/src/bin/analyzer.js index 5303578c..0e9f6702 100755 --- a/src/bin/analyzer.js +++ b/src/bin/analyzer.js @@ -10,7 +10,8 @@ const viewer = require('../viewer'); const Logger = require('../Logger'); const utils = require('../utils'); -const SIZES = new Set(['stat', 'parsed', 'gzip']); +const SIZES = new Set(['stat', 'parsed', 'compressed']); +const ACCEPTED_SIZES = new Set([...SIZES, 'gzip']); const program = commander .version(require('../../package.json').version) @@ -104,7 +105,9 @@ if (mode === 'server') { port = port === 'auto' ? 0 : Number(port); if (isNaN(port)) showHelp('Invalid port. Should be a number or `auto`'); } -if (!SIZES.has(defaultSizes)) showHelp(`Invalid default sizes option. Possible values are: ${[...SIZES].join(', ')}`); +if (!ACCEPTED_SIZES.has(defaultSizes)) { + showHelp(`Invalid default sizes option. Possible values are: ${[...SIZES].join(', ')}`); +} bundleStatsFile = resolve(bundleStatsFile); diff --git a/src/viewer.js b/src/viewer.js index 87304f54..2068c929 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -22,6 +22,10 @@ function resolveTitle(reportTitle) { } } +function resolveDefaultSizes(defaultSizes) { + return defaultSizes === 'compressed' ? 'gzip' : defaultSizes; +} + module.exports = { startServer, generateReport, @@ -59,7 +63,7 @@ async function startServer(bundleStats, opts) { mode: 'server', title: resolveTitle(reportTitle), chartData, - defaultSizes, + defaultSizes: resolveDefaultSizes(defaultSizes), enableWebSocket: true }); res.writeHead(200, {'Content-Type': 'text/html'}); @@ -140,7 +144,7 @@ async function generateReport(bundleStats, opts) { mode: 'static', title: resolveTitle(reportTitle), chartData, - defaultSizes, + defaultSizes: resolveDefaultSizes(defaultSizes), enableWebSocket: false }); const reportFilepath = path.resolve(bundleDir || process.cwd(), reportFilename); From 4dfbb5ccf6623699dcb4ab3be87321e06f71ffaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cs=C3=A1sz=C3=A1r?= <dcsaszar@users.noreply.github.com> Date: Tue, 13 Apr 2021 14:18:29 +0200 Subject: [PATCH 04/10] Add Brotli size helper --- src/sizeUtils.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/sizeUtils.js b/src/sizeUtils.js index 2ded5442..d0f27179 100644 --- a/src/sizeUtils.js +++ b/src/sizeUtils.js @@ -3,3 +3,11 @@ const zlib = require('zlib'); export function gzipSize(input) { return zlib.gzipSync(input, {level: 9}).length; } + +export function brotliSize(input) { + if (typeof zlib.brotliCompressSync !== 'function') { + throw new Error('Brotli compression requires Node.js v10.16.0 or higher.'); + } + + return zlib.brotliCompressSync(input).length; +} From 80511a8f3e676dfcebb3713fd7413a38a3f73863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cs=C3=A1sz=C3=A1r?= <dcsaszar@users.noreply.github.com> Date: Tue, 13 Apr 2021 14:24:31 +0200 Subject: [PATCH 05/10] Add compressionAlgorithm option --- client/components/ModulesTreemap.jsx | 17 +++++++----- src/BundleAnalyzerPlugin.js | 3 +++ src/analyzer.js | 12 +++++++-- src/bin/analyzer.js | 17 +++++++++++- src/template.js | 3 ++- src/viewer.js | 19 ++++++++++---- test/analyzer.js | 22 ++++++++++++++++ test/plugin.js | 39 ++++++++++++++++++++++++++++ 8 files changed, 116 insertions(+), 16 deletions(-) diff --git a/client/components/ModulesTreemap.jsx b/client/components/ModulesTreemap.jsx index 1afe3a9b..14ccc62b 100644 --- a/client/components/ModulesTreemap.jsx +++ b/client/components/ModulesTreemap.jsx @@ -18,11 +18,13 @@ import Search from './Search'; import {store} from '../store'; import ModulesList from './ModulesList'; -const SIZE_SWITCH_ITEMS = [ - {label: 'Stat', prop: 'statSize'}, - {label: 'Parsed', prop: 'parsedSize'}, - {label: 'Gzipped', prop: 'gzipSize'} -]; +function allSizeSwitchItems() { + return [ + {label: 'Stat', prop: 'statSize'}, + {label: 'Parsed', prop: 'parsedSize'}, + {label: window.compressedSizeLabel, prop: 'gzipSize'} + ]; +} @observer export default class ModulesTreemap extends Component { @@ -138,7 +140,7 @@ export default class ModulesTreemap extends Component { renderModuleSize(module, sizeType) { const sizeProp = `${sizeType}Size`; const size = module[sizeProp]; - const sizeLabel = SIZE_SWITCH_ITEMS.find(item => item.prop === sizeProp).label; + const sizeLabel = allSizeSwitchItems().find(item => item.prop === sizeProp).label; const isActive = (store.activeSize === sizeProp); return (typeof size === 'number') ? @@ -162,7 +164,8 @@ export default class ModulesTreemap extends Component { }; @computed get sizeSwitchItems() { - return store.hasParsedSizes ? SIZE_SWITCH_ITEMS : SIZE_SWITCH_ITEMS.slice(0, 1); + const items = allSizeSwitchItems(); + return store.hasParsedSizes ? items : items.slice(0, 1); } @computed get activeSizeItem() { diff --git a/src/BundleAnalyzerPlugin.js b/src/BundleAnalyzerPlugin.js index bc37f1de..401c0e68 100644 --- a/src/BundleAnalyzerPlugin.js +++ b/src/BundleAnalyzerPlugin.js @@ -104,6 +104,7 @@ class BundleAnalyzerPlugin { host: this.opts.analyzerHost, port: this.opts.analyzerPort, reportTitle: this.opts.reportTitle, + compressionAlgorithm: this.opts.compressionAlgorithm, bundleDir: this.getBundleDirFromCompiler(), logger: this.logger, defaultSizes: this.opts.defaultSizes, @@ -115,6 +116,7 @@ class BundleAnalyzerPlugin { async generateJSONReport(stats) { await viewer.generateJSONReport(stats, { reportFilename: path.resolve(this.compiler.outputPath, this.opts.reportFilename || 'report.json'), + compressionAlgorithm: this.opts.compressionAlgorithm, bundleDir: this.getBundleDirFromCompiler(), logger: this.logger, excludeAssets: this.opts.excludeAssets @@ -126,6 +128,7 @@ class BundleAnalyzerPlugin { openBrowser: this.opts.openAnalyzer, reportFilename: path.resolve(this.compiler.outputPath, this.opts.reportFilename || 'report.html'), reportTitle: this.opts.reportTitle, + compressionAlgorithm: this.opts.compressionAlgorithm, bundleDir: this.getBundleDirFromCompiler(), logger: this.logger, defaultSizes: this.opts.defaultSizes, diff --git a/src/analyzer.js b/src/analyzer.js index c58d5e93..d7e79f37 100644 --- a/src/analyzer.js +++ b/src/analyzer.js @@ -7,11 +7,16 @@ const Logger = require('./Logger'); const Folder = require('./tree/Folder').default; const {parseBundle} = require('./parseUtils'); const {createAssetsFilter} = require('./utils'); -const {gzipSize} = require('./sizeUtils'); +const {gzipSize, brotliSize} = require('./sizeUtils'); const FILENAME_QUERY_REGEXP = /\?.*$/u; const FILENAME_EXTENSIONS = /\.(js|mjs)$/iu; +const COMPRESSED_SIZE = { + gzip: gzipSize, + brotli: brotliSize +}; + module.exports = { getViewerData, readStatsFromFile @@ -21,11 +26,14 @@ function getViewerData(bundleStats, bundleDir, opts) { const { logger = new Logger(), excludeAssets = null, - compressedSize = gzipSize + compressionAlgorithm } = opts || {}; const isAssetIncluded = createAssetsFilter(excludeAssets); + const compressedSize = COMPRESSED_SIZE[compressionAlgorithm]; + if (!compressedSize) throw new Error(`Unsupported compression algorithm: ${compressionAlgorithm}.`); + // Sometimes all the information is located in `children` array (e.g. problem in #10) if (_.isEmpty(bundleStats.assets) && !_.isEmpty(bundleStats.children)) { const {children} = bundleStats; diff --git a/src/bin/analyzer.js b/src/bin/analyzer.js index 0e9f6702..590d2f9f 100755 --- a/src/bin/analyzer.js +++ b/src/bin/analyzer.js @@ -13,6 +13,8 @@ const utils = require('../utils'); const SIZES = new Set(['stat', 'parsed', 'compressed']); const ACCEPTED_SIZES = new Set([...SIZES, 'gzip']); +const ALGORITHMS = new Set(['gzip', 'brotli']); + const program = commander .version(require('../../package.json').version) .usage( @@ -59,6 +61,12 @@ const program = commander br(`Possible values: ${[...SIZES].join(', ')}`), 'parsed' ) + .option( + '--compression-algorithm <type>', + 'Compression algorithm that will be used to calculate the compressed module sizes.' + + br(`Possible values: ${[...ALGORITHMS].join(', ')}`), + 'gzip' + ) .option( '-O, --no-open', "Don't open report in default browser automatically." @@ -84,6 +92,7 @@ let { report: reportFilename, title: reportTitle, defaultSizes, + compressionAlgorithm, logLevel, open: openBrowser, exclude: excludeAssets, @@ -108,6 +117,9 @@ if (mode === 'server') { if (!ACCEPTED_SIZES.has(defaultSizes)) { showHelp(`Invalid default sizes option. Possible values are: ${[...SIZES].join(', ')}`); } +if (!ALGORITHMS.has(compressionAlgorithm)) { + showHelp(`Invalid compression algorithm option. Possible values are: ${[...ALGORITHMS].join(', ')}`); +} bundleStatsFile = resolve(bundleStatsFile); @@ -128,6 +140,7 @@ if (mode === 'server') { port, host, defaultSizes, + compressionAlgorithm, reportTitle, bundleDir, excludeAssets, @@ -139,6 +152,7 @@ if (mode === 'server') { reportFilename: resolve(reportFilename || 'report.html'), reportTitle, defaultSizes, + compressionAlgorithm, bundleDir, excludeAssets, logger: new Logger(logLevel) @@ -146,6 +160,7 @@ if (mode === 'server') { } else if (mode === 'json') { viewer.generateJSONReport(bundleStats, { reportFilename: resolve(reportFilename || 'report.json'), + compressionAlgorithm, bundleDir, excludeAssets, logger: new Logger(logLevel) @@ -159,7 +174,7 @@ function showHelp(error) { } function br(str) { - return `\n${' '.repeat(28)}${str}`; + return `\n${' '.repeat(32)}${str}`; } function array() { diff --git a/src/template.js b/src/template.js index 4ef02634..01888944 100644 --- a/src/template.js +++ b/src/template.js @@ -39,7 +39,7 @@ function getScript(filename, mode) { } } -function renderViewer({title, enableWebSocket, chartData, defaultSizes, mode} = {}) { +function renderViewer({title, enableWebSocket, chartData, defaultSizes, compressedSizeLabel, mode} = {}) { return html`<!DOCTYPE html> <html> <head> @@ -59,6 +59,7 @@ function renderViewer({title, enableWebSocket, chartData, defaultSizes, mode} = <script> window.chartData = ${escapeJson(chartData)}; window.defaultSizes = ${escapeJson(defaultSizes)}; + window.compressedSizeLabel = ${escapeJson(compressedSizeLabel)}; </script> </body> </html>`; diff --git a/src/viewer.js b/src/viewer.js index 2068c929..e9f043f3 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -26,6 +26,10 @@ function resolveDefaultSizes(defaultSizes) { return defaultSizes === 'compressed' ? 'gzip' : defaultSizes; } +function resolveCompressedSizeLabel(compressionAlgorithm) { + return {gzip: 'Gzipped', brotli: 'Brotli'}[compressionAlgorithm]; +} + module.exports = { startServer, generateReport, @@ -43,10 +47,11 @@ async function startServer(bundleStats, opts) { logger = new Logger(), defaultSizes = 'parsed', excludeAssets = null, - reportTitle + reportTitle, + compressionAlgorithm = 'gzip' } = opts || {}; - const analyzerOpts = {logger, excludeAssets}; + const analyzerOpts = {logger, excludeAssets, compressionAlgorithm}; let chartData = getChartData(analyzerOpts, bundleStats, bundleDir); @@ -64,6 +69,7 @@ async function startServer(bundleStats, opts) { title: resolveTitle(reportTitle), chartData, defaultSizes: resolveDefaultSizes(defaultSizes), + compressedSizeLabel: resolveCompressedSizeLabel(compressionAlgorithm), enableWebSocket: true }); res.writeHead(200, {'Content-Type': 'text/html'}); @@ -130,13 +136,14 @@ async function generateReport(bundleStats, opts) { openBrowser = true, reportFilename, reportTitle, + compressionAlgorithm = 'gzip', bundleDir = null, logger = new Logger(), defaultSizes = 'parsed', excludeAssets = null } = opts || {}; - const chartData = getChartData({logger, excludeAssets}, bundleStats, bundleDir); + const chartData = getChartData({logger, excludeAssets, compressionAlgorithm}, bundleStats, bundleDir); if (!chartData) return; @@ -145,6 +152,7 @@ async function generateReport(bundleStats, opts) { title: resolveTitle(reportTitle), chartData, defaultSizes: resolveDefaultSizes(defaultSizes), + compressedSizeLabel: resolveCompressedSizeLabel(compressionAlgorithm), enableWebSocket: false }); const reportFilepath = path.resolve(bundleDir || process.cwd(), reportFilename); @@ -160,9 +168,10 @@ async function generateReport(bundleStats, opts) { } async function generateJSONReport(bundleStats, opts) { - const {reportFilename, bundleDir = null, logger = new Logger(), excludeAssets = null} = opts || {}; + const {reportFilename, bundleDir = null, logger = new Logger(), excludeAssets = null, + compressionAlgorithm = 'gzip'} = opts || {}; - const chartData = getChartData({logger, excludeAssets}, bundleStats, bundleDir); + const chartData = getChartData({logger, excludeAssets, compressionAlgorithm}, bundleStats, bundleDir); if (!chartData) return; diff --git a/test/analyzer.js b/test/analyzer.js index b15e2c05..6f88fefb 100644 --- a/test/analyzer.js +++ b/test/analyzer.js @@ -216,6 +216,23 @@ describe('Analyzer', function () { expect(generatedReportTitle).to.match(/^webpack-bundle-analyzer \[.* at \d{2}:\d{2}\]/u); }); }); + + describe('compression algorithm', function () { + it('should accept --compression-algorithm brotli', async function () { + generateReportFrom('with-modules-chunk.json', '--compression-algorithm brotli'); + expect(await getCompressedSizeLabel()).to.equal('Brotli'); + }); + + it('should accept --compression-algorithm gzip', async function () { + generateReportFrom('with-modules-chunk.json', '--compression-algorithm gzip'); + expect(await getCompressedSizeLabel()).to.equal('Gzipped'); + }); + + it('should default to gzip', async function () { + generateReportFrom('with-modules-chunk.json'); + expect(await getCompressedSizeLabel()).to.equal('Gzipped'); + }); + }); }); }); @@ -241,6 +258,11 @@ async function getChartData() { return await nightmare.goto(`file://${__dirname}/output/report.html`).evaluate(() => window.chartData); } +async function getCompressedSizeLabel() { + return await nightmare.goto(`file://${__dirname}/output/report.html`).evaluate( + () => window.compressedSizeLabel); +} + function forEachChartItem(chartData, cb) { for (const item of chartData) { cb(item); diff --git a/test/plugin.js b/test/plugin.js index 05c3f18c..ea0e695f 100644 --- a/test/plugin.js +++ b/test/plugin.js @@ -170,6 +170,45 @@ describe('Plugin', function () { expect(error).to.equal(reportTitleError); }); }); + + describe('compressionAlgorithm', function () { + it('should default to gzip', async function () { + const config = makeWebpackConfig({ + analyzerOpts: {} + }); + await webpackCompile(config, '4.44.2'); + await expectValidReport({ + parsedSize: 1311, + gzipSize: 342 + }); + }); + + it('should support gzip', async function () { + const config = makeWebpackConfig({ + analyzerOpts: { + compressionAlgorithm: 'gzip' + } + }); + await webpackCompile(config, '4.44.2'); + await expectValidReport({ + parsedSize: 1311, + gzipSize: 342 + }); + }); + + it('should support brotli', async function () { + const config = makeWebpackConfig({ + analyzerOpts: { + compressionAlgorithm: 'brotli' + } + }); + await webpackCompile(config, '4.44.2'); + await expectValidReport({ + parsedSize: 1311, + gzipSize: 302 + }); + }); + }); }); async function expectValidReport(opts) { From 70d8cf189ddd50846bea3fbb75ef97845640c5b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cs=C3=A1sz=C3=A1r?= <dcsaszar@users.noreply.github.com> Date: Thu, 15 Apr 2021 01:13:43 +0200 Subject: [PATCH 06/10] Add compression algorithm options to README --- README.md | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index c5a5bf01..7d4cb15d 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,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 sizes! +And it also shows their gzipped or Brotli sizes! <h2 align="center">Options (for plugin)</h2> @@ -62,6 +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`, `compressed`|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.| |**`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).| @@ -111,23 +112,25 @@ Directory containing all generated bundles. ### `options` ``` - -V, --version output the version number - -m, --mode <mode> Analyzer mode. Should be `server`, `static` or `json`. - In `server` mode analyzer will start HTTP server to show bundle report. - In `static` mode single HTML file with bundle report will be generated. - In `json` mode single JSON file with bundle report will be generated. (default: server) - -h, --host <host> Host that will be used in `server` mode to start HTTP server. (default: 127.0.0.1) - -p, --port <n> Port that will be used in `server` mode to start HTTP server. Should be a number or `auto` (default: 8888) - -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, compressed (default: parsed) - -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. - -l, --log-level <level> Log level. - Possible values: debug, info, warn, error, silent (default: info) - -h, --help output usage information + -V, --version output the version number + -m, --mode <mode> Analyzer mode. Should be `server`, `static` or `json`. + In `server` mode analyzer will start HTTP server to show bundle report. + In `static` mode single HTML file with bundle report will be generated. + In `json` mode single JSON file with bundle report will be generated. (default: server) + -h, --host <host> Host that will be used in `server` mode to start HTTP server. (default: 127.0.0.1) + -p, --port <n> Port that will be used in `server` mode to start HTTP server. Should be a number or `auto` (default: 8888) + -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, compressed (default: parsed) + --compression-algorithm <type> Compression algorithm that will be used to calculate the compressed module sizes. + Possible values: gzip, brotli (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. + -l, --log-level <level> Log level. + Possible values: debug, info, warn, error, silent (default: info) + -h, --help output usage information ``` <h2 align="center" id="size-definitions">Size definitions</h2> From 3f183c2ec55f930b82e3afbac8ec665b4ad0061d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cs=C3=A1sz=C3=A1r?= <dcsaszar@users.noreply.github.com> Date: Thu, 15 Apr 2021 11:21:51 +0200 Subject: [PATCH 07/10] Separate gzip and brotli data --- client/components/ModulesTreemap.jsx | 12 ++++++++---- client/store.js | 2 +- src/BundleAnalyzerPlugin.js | 1 + src/analyzer.js | 15 ++++----------- src/sizeUtils.js | 15 +++++++++++++-- src/template.js | 4 ++-- src/tree/ConcatenatedModule.js | 4 ++-- src/tree/ContentFolder.js | 5 +++++ src/tree/ContentModule.js | 4 ++++ src/tree/Folder.js | 21 ++++++++++++++++----- src/tree/Module.js | 24 ++++++++++++++++++------ src/viewer.js | 14 +++++--------- test/analyzer.js | 10 +++++----- test/plugin.js | 7 ++++--- 14 files changed, 88 insertions(+), 50 deletions(-) diff --git a/client/components/ModulesTreemap.jsx b/client/components/ModulesTreemap.jsx index 14ccc62b..1a1b0af1 100644 --- a/client/components/ModulesTreemap.jsx +++ b/client/components/ModulesTreemap.jsx @@ -19,11 +19,15 @@ import {store} from '../store'; import ModulesList from './ModulesList'; function allSizeSwitchItems() { - return [ + const items = [ {label: 'Stat', prop: 'statSize'}, - {label: 'Parsed', prop: 'parsedSize'}, - {label: window.compressedSizeLabel, prop: 'gzipSize'} + {label: 'Parsed', prop: 'parsedSize'} ]; + + if (window.compressionAlgorithm === 'gzip') items.push({label: 'Gzipped', prop: 'gzipSize'}); + if (window.compressionAlgorithm === 'brotli') items.push({label: 'Brotli', prop: 'brotliSize'}); + + return items; } @observer @@ -319,7 +323,7 @@ export default class ModulesTreemap extends Component { <br/> {this.renderModuleSize(module, 'stat')} {!module.inaccurateSizes && this.renderModuleSize(module, 'parsed')} - {!module.inaccurateSizes && this.renderModuleSize(module, 'gzip')} + {!module.inaccurateSizes && this.renderModuleSize(module, window.compressionAlgorithm)} {module.path && <div>Path: <strong>{module.path}</strong></div> } diff --git a/client/store.js b/client/store.js index 5710a8a0..a944a68b 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']); + sizes = new Set(['statSize', 'parsedSize', 'gzipSize', 'brotliSize']); @observable.ref allChunks; @observable.shallow selectedChunks; diff --git a/src/BundleAnalyzerPlugin.js b/src/BundleAnalyzerPlugin.js index 401c0e68..cb9eb602 100644 --- a/src/BundleAnalyzerPlugin.js +++ b/src/BundleAnalyzerPlugin.js @@ -12,6 +12,7 @@ class BundleAnalyzerPlugin { this.opts = { analyzerMode: 'server', analyzerHost: '127.0.0.1', + compressionAlgorithm: 'gzip', reportFilename: null, reportTitle: utils.defaultTitle, defaultSizes: 'parsed', diff --git a/src/analyzer.js b/src/analyzer.js index d7e79f37..3d200cb4 100644 --- a/src/analyzer.js +++ b/src/analyzer.js @@ -7,16 +7,11 @@ const Logger = require('./Logger'); const Folder = require('./tree/Folder').default; const {parseBundle} = require('./parseUtils'); const {createAssetsFilter} = require('./utils'); -const {gzipSize, brotliSize} = require('./sizeUtils'); +const {compressedSize} = require('./sizeUtils'); const FILENAME_QUERY_REGEXP = /\?.*$/u; const FILENAME_EXTENSIONS = /\.(js|mjs)$/iu; -const COMPRESSED_SIZE = { - gzip: gzipSize, - brotli: brotliSize -}; - module.exports = { getViewerData, readStatsFromFile @@ -31,9 +26,6 @@ function getViewerData(bundleStats, bundleDir, opts) { const isAssetIncluded = createAssetsFilter(excludeAssets); - const compressedSize = COMPRESSED_SIZE[compressionAlgorithm]; - if (!compressedSize) throw new Error(`Unsupported compression algorithm: ${compressionAlgorithm}.`); - // Sometimes all the information is located in `children` array (e.g. problem in #10) if (_.isEmpty(bundleStats.assets) && !_.isEmpty(bundleStats.children)) { const {children} = bundleStats; @@ -111,7 +103,7 @@ function getViewerData(bundleStats, bundleDir, opts) { if (assetSources) { asset.parsedSize = Buffer.byteLength(assetSources.src); - asset.gzipSize = compressedSize(assetSources.src); + asset[`${compressionAlgorithm}Size`] = compressedSize(compressionAlgorithm, assetSources.src); } // Picking modules from current bundle script @@ -152,7 +144,7 @@ function getViewerData(bundleStats, bundleDir, opts) { } asset.modules = assetModules; - asset.tree = createModulesTree(asset.modules, {compressedSize}); + asset.tree = createModulesTree(asset.modules, {compressionAlgorithm}); return result; }, {}); @@ -166,6 +158,7 @@ function getViewerData(bundleStats, bundleDir, opts) { statSize: asset.tree.size || asset.size, parsedSize: asset.parsedSize, gzipSize: asset.gzipSize, + brotliSize: asset.brotliSize, groups: _.invokeMap(asset.tree.children, 'toChartData') })); } diff --git a/src/sizeUtils.js b/src/sizeUtils.js index d0f27179..34140ca9 100644 --- a/src/sizeUtils.js +++ b/src/sizeUtils.js @@ -1,10 +1,21 @@ const zlib = require('zlib'); -export function gzipSize(input) { +const COMPRESSED_SIZE = { + gzip: gzipSize, + brotli: brotliSize +}; + +export function compressedSize(compressionAlgorithm, input) { + const fn = COMPRESSED_SIZE[compressionAlgorithm]; + if (!fn) throw new Error(`Unsupported compression algorithm: ${compressionAlgorithm}.`); + return fn(input); +} + +function gzipSize(input) { return zlib.gzipSync(input, {level: 9}).length; } -export function brotliSize(input) { +function brotliSize(input) { if (typeof zlib.brotliCompressSync !== 'function') { throw new Error('Brotli compression requires Node.js v10.16.0 or higher.'); } diff --git a/src/template.js b/src/template.js index 01888944..1e97eb79 100644 --- a/src/template.js +++ b/src/template.js @@ -39,7 +39,7 @@ function getScript(filename, mode) { } } -function renderViewer({title, enableWebSocket, chartData, defaultSizes, compressedSizeLabel, mode} = {}) { +function renderViewer({title, enableWebSocket, chartData, defaultSizes, compressionAlgorithm, mode} = {}) { return html`<!DOCTYPE html> <html> <head> @@ -59,7 +59,7 @@ function renderViewer({title, enableWebSocket, chartData, defaultSizes, compress <script> window.chartData = ${escapeJson(chartData)}; window.defaultSizes = ${escapeJson(defaultSizes)}; - window.compressedSizeLabel = ${escapeJson(compressedSizeLabel)}; + window.compressionAlgorithm = ${escapeJson(compressionAlgorithm)}; </script> </body> </html>`; diff --git a/src/tree/ConcatenatedModule.js b/src/tree/ConcatenatedModule.js index 19abe28d..f58a974c 100644 --- a/src/tree/ConcatenatedModule.js +++ b/src/tree/ConcatenatedModule.js @@ -7,8 +7,8 @@ import {getModulePathParts} from './utils'; export default class ConcatenatedModule extends Module { - constructor(name, data, parent) { - super(name, data, parent); + constructor(name, data, parent, opts) { + super(name, data, parent, opts); this.name += ' (concatenated)'; this.children = Object.create(null); this.fillContentModules(); diff --git a/src/tree/ContentFolder.js b/src/tree/ContentFolder.js index 5eb647cb..c58d668e 100644 --- a/src/tree/ContentFolder.js +++ b/src/tree/ContentFolder.js @@ -15,6 +15,10 @@ export default class ContentFolder extends BaseFolder { return this.getSize('gzipSize'); } + get brotliSize() { + return this.getSize('brotliSize'); + } + getSize(sizeType) { const ownerModuleSize = this.ownerModule[sizeType]; @@ -28,6 +32,7 @@ export default class ContentFolder extends BaseFolder { ...super.toChartData(), parsedSize: this.parsedSize, gzipSize: this.gzipSize, + brotliSize: this.brotliSize, inaccurateSizes: true }; } diff --git a/src/tree/ContentModule.js b/src/tree/ContentModule.js index a33f4097..116a23c2 100644 --- a/src/tree/ContentModule.js +++ b/src/tree/ContentModule.js @@ -15,6 +15,10 @@ export default class ContentModule extends Module { return this.getSize('gzipSize'); } + get brotliSize() { + return this.getSize('brotliSize'); + } + getSize(sizeType) { const ownerModuleSize = this.ownerModule[sizeType]; diff --git a/src/tree/Folder.js b/src/tree/Folder.js index 15748227..87570025 100644 --- a/src/tree/Folder.js +++ b/src/tree/Folder.js @@ -4,6 +4,7 @@ import Module from './Module'; import BaseFolder from './BaseFolder'; import ConcatenatedModule from './ConcatenatedModule'; import {getModulePathParts} from './utils'; +import {compressedSize} from '../sizeUtils'; export default class Folder extends BaseFolder { @@ -17,11 +18,20 @@ export default class Folder extends BaseFolder { } get gzipSize() { - if (!_.has(this, '_gzipSize')) { - this._gzipSize = this.src ? this.opts.compressedSize(this.src) : 0; + return this.opts.compressionAlgorithm === 'gzip' ? this.compressedSize('gzip') : undefined; + } + + get brotliSize() { + return this.opts.compressionAlgorithm === 'brotli' ? this.compressedSize('brotli') : undefined; + } + + compressedSize(compressionAlgorithm) { + const key = `_${compressionAlgorithm}Size`; + if (!_.has(this, key)) { + this[key] = this.src ? compressedSize(compressionAlgorithm, this.src) : 0; } - return this._gzipSize; + return this[key]; } addModule(moduleData) { @@ -53,7 +63,7 @@ export default class Folder extends BaseFolder { }); const ModuleConstructor = moduleData.modules ? ConcatenatedModule : Module; - const module = new ModuleConstructor(fileName, moduleData, this); + const module = new ModuleConstructor(fileName, moduleData, this, this.opts); currentFolder.addChildModule(module); } @@ -61,7 +71,8 @@ export default class Folder extends BaseFolder { return { ...super.toChartData(), parsedSize: this.parsedSize, - gzipSize: this.gzipSize + gzipSize: this.gzipSize, + brotliSize: this.brotliSize }; } diff --git a/src/tree/Module.js b/src/tree/Module.js index 00db7eeb..3502318e 100644 --- a/src/tree/Module.js +++ b/src/tree/Module.js @@ -1,13 +1,14 @@ import _ from 'lodash'; -import {gzipSize} from '../sizeUtils'; +import {compressedSize} from '../sizeUtils'; import Node from './Node'; export default class Module extends Node { - constructor(name, data, parent) { + constructor(name, data, parent, opts) { super(name, parent); this.data = data; + this.opts = opts; } get src() { @@ -17,6 +18,7 @@ export default class Module extends Node { set src(value) { this.data.parsedSrc = value; delete this._gzipSize; + delete this._brotliSize; } get size() { @@ -32,11 +34,20 @@ export default class Module extends Node { } get gzipSize() { - if (!_.has(this, '_gzipSize')) { - this._gzipSize = this.src ? gzipSize(this.src) : undefined; + return this.opts.compressionAlgorithm === 'gzip' ? this.compressedSize('gzip') : undefined; + } + + get brotliSize() { + return this.opts.compressionAlgorithm === 'brotli' ? this.compressedSize('brotli') : undefined; + } + + compressedSize(compressionAlgorithm) { + const key = `_${compressionAlgorithm}Size`; + if (!_.has(this, key)) { + this[key] = this.src ? compressedSize(compressionAlgorithm, this.src) : undefined; } - return this._gzipSize; + return this[key]; } mergeData(data) { @@ -56,7 +67,8 @@ export default class Module extends Node { path: this.path, statSize: this.size, parsedSize: this.parsedSize, - gzipSize: this.gzipSize + gzipSize: this.gzipSize, + brotliSize: this.brotliSize }; } diff --git a/src/viewer.js b/src/viewer.js index e9f043f3..6bf0abf1 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -26,10 +26,6 @@ function resolveDefaultSizes(defaultSizes) { return defaultSizes === 'compressed' ? 'gzip' : defaultSizes; } -function resolveCompressedSizeLabel(compressionAlgorithm) { - return {gzip: 'Gzipped', brotli: 'Brotli'}[compressionAlgorithm]; -} - module.exports = { startServer, generateReport, @@ -48,7 +44,7 @@ async function startServer(bundleStats, opts) { defaultSizes = 'parsed', excludeAssets = null, reportTitle, - compressionAlgorithm = 'gzip' + compressionAlgorithm } = opts || {}; const analyzerOpts = {logger, excludeAssets, compressionAlgorithm}; @@ -69,7 +65,7 @@ async function startServer(bundleStats, opts) { title: resolveTitle(reportTitle), chartData, defaultSizes: resolveDefaultSizes(defaultSizes), - compressedSizeLabel: resolveCompressedSizeLabel(compressionAlgorithm), + compressionAlgorithm, enableWebSocket: true }); res.writeHead(200, {'Content-Type': 'text/html'}); @@ -136,7 +132,7 @@ async function generateReport(bundleStats, opts) { openBrowser = true, reportFilename, reportTitle, - compressionAlgorithm = 'gzip', + compressionAlgorithm, bundleDir = null, logger = new Logger(), defaultSizes = 'parsed', @@ -152,7 +148,7 @@ async function generateReport(bundleStats, opts) { title: resolveTitle(reportTitle), chartData, defaultSizes: resolveDefaultSizes(defaultSizes), - compressedSizeLabel: resolveCompressedSizeLabel(compressionAlgorithm), + compressionAlgorithm, enableWebSocket: false }); const reportFilepath = path.resolve(bundleDir || process.cwd(), reportFilename); @@ -169,7 +165,7 @@ async function generateReport(bundleStats, opts) { async function generateJSONReport(bundleStats, opts) { const {reportFilename, bundleDir = null, logger = new Logger(), excludeAssets = null, - compressionAlgorithm = 'gzip'} = opts || {}; + compressionAlgorithm} = opts || {}; const chartData = getChartData({logger, excludeAssets, compressionAlgorithm}, bundleStats, bundleDir); diff --git a/test/analyzer.js b/test/analyzer.js index 6f88fefb..53db0e36 100644 --- a/test/analyzer.js +++ b/test/analyzer.js @@ -220,17 +220,17 @@ describe('Analyzer', function () { describe('compression algorithm', function () { it('should accept --compression-algorithm brotli', async function () { generateReportFrom('with-modules-chunk.json', '--compression-algorithm brotli'); - expect(await getCompressedSizeLabel()).to.equal('Brotli'); + expect(await getCompressionAlgorithm()).to.equal('brotli'); }); it('should accept --compression-algorithm gzip', async function () { generateReportFrom('with-modules-chunk.json', '--compression-algorithm gzip'); - expect(await getCompressedSizeLabel()).to.equal('Gzipped'); + expect(await getCompressionAlgorithm()).to.equal('gzip'); }); it('should default to gzip', async function () { generateReportFrom('with-modules-chunk.json'); - expect(await getCompressedSizeLabel()).to.equal('Gzipped'); + expect(await getCompressionAlgorithm()).to.equal('gzip'); }); }); }); @@ -258,9 +258,9 @@ async function getChartData() { return await nightmare.goto(`file://${__dirname}/output/report.html`).evaluate(() => window.chartData); } -async function getCompressedSizeLabel() { +async function getCompressionAlgorithm() { return await nightmare.goto(`file://${__dirname}/output/report.html`).evaluate( - () => window.compressedSizeLabel); + () => window.compressionAlgorithm); } function forEachChartItem(chartData, cb) { diff --git a/test/plugin.js b/test/plugin.js index ea0e695f..941002ca 100644 --- a/test/plugin.js +++ b/test/plugin.js @@ -205,7 +205,8 @@ describe('Plugin', function () { await webpackCompile(config, '4.44.2'); await expectValidReport({ parsedSize: 1311, - gzipSize: 302 + gzipSize: undefined, + brotliSize: 302 }); }); }); @@ -218,8 +219,8 @@ describe('Plugin', function () { bundleLabel = 'bundle.js', statSize = 141, parsedSize = 2821, - gzipSize = 770 - } = opts || {}; + gzipSize + } = {gzipSize: 770, ...opts}; expect(fs.existsSync(`${__dirname}/output/${bundleFilename}`), 'bundle file missing').to.be.true; expect(fs.existsSync(`${__dirname}/output/${reportFilename}`), 'report file missing').to.be.true; From b8dcd04a3f73dbc86518412f6ef07fd33d422c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cs=C3=A1sz=C3=A1r?= <dcsaszar@users.noreply.github.com> Date: Thu, 15 Apr 2021 11:56:35 +0200 Subject: [PATCH 08/10] compressedSize => getCompressedSize --- src/analyzer.js | 4 ++-- src/sizeUtils.js | 2 +- src/tree/Folder.js | 10 +++++----- src/tree/Module.js | 10 +++++----- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/analyzer.js b/src/analyzer.js index 3d200cb4..6520a01e 100644 --- a/src/analyzer.js +++ b/src/analyzer.js @@ -7,7 +7,7 @@ const Logger = require('./Logger'); const Folder = require('./tree/Folder').default; const {parseBundle} = require('./parseUtils'); const {createAssetsFilter} = require('./utils'); -const {compressedSize} = require('./sizeUtils'); +const {getCompressedSize} = require('./sizeUtils'); const FILENAME_QUERY_REGEXP = /\?.*$/u; const FILENAME_EXTENSIONS = /\.(js|mjs)$/iu; @@ -103,7 +103,7 @@ function getViewerData(bundleStats, bundleDir, opts) { if (assetSources) { asset.parsedSize = Buffer.byteLength(assetSources.src); - asset[`${compressionAlgorithm}Size`] = compressedSize(compressionAlgorithm, assetSources.src); + asset[`${compressionAlgorithm}Size`] = getCompressedSize(compressionAlgorithm, assetSources.src); } // Picking modules from current bundle script diff --git a/src/sizeUtils.js b/src/sizeUtils.js index 34140ca9..1ce66ae4 100644 --- a/src/sizeUtils.js +++ b/src/sizeUtils.js @@ -5,7 +5,7 @@ const COMPRESSED_SIZE = { brotli: brotliSize }; -export function compressedSize(compressionAlgorithm, input) { +export function getCompressedSize(compressionAlgorithm, input) { const fn = COMPRESSED_SIZE[compressionAlgorithm]; if (!fn) throw new Error(`Unsupported compression algorithm: ${compressionAlgorithm}.`); return fn(input); diff --git a/src/tree/Folder.js b/src/tree/Folder.js index 87570025..ab64e35e 100644 --- a/src/tree/Folder.js +++ b/src/tree/Folder.js @@ -4,7 +4,7 @@ import Module from './Module'; import BaseFolder from './BaseFolder'; import ConcatenatedModule from './ConcatenatedModule'; import {getModulePathParts} from './utils'; -import {compressedSize} from '../sizeUtils'; +import {getCompressedSize} from '../sizeUtils'; export default class Folder extends BaseFolder { @@ -18,17 +18,17 @@ export default class Folder extends BaseFolder { } get gzipSize() { - return this.opts.compressionAlgorithm === 'gzip' ? this.compressedSize('gzip') : undefined; + return this.opts.compressionAlgorithm === 'gzip' ? this.getCompressedSize('gzip') : undefined; } get brotliSize() { - return this.opts.compressionAlgorithm === 'brotli' ? this.compressedSize('brotli') : undefined; + return this.opts.compressionAlgorithm === 'brotli' ? this.getCompressedSize('brotli') : undefined; } - compressedSize(compressionAlgorithm) { + getCompressedSize(compressionAlgorithm) { const key = `_${compressionAlgorithm}Size`; if (!_.has(this, key)) { - this[key] = this.src ? compressedSize(compressionAlgorithm, this.src) : 0; + this[key] = this.src ? getCompressedSize(compressionAlgorithm, this.src) : 0; } return this[key]; diff --git a/src/tree/Module.js b/src/tree/Module.js index 3502318e..95075437 100644 --- a/src/tree/Module.js +++ b/src/tree/Module.js @@ -1,5 +1,5 @@ import _ from 'lodash'; -import {compressedSize} from '../sizeUtils'; +import {getCompressedSize} from '../sizeUtils'; import Node from './Node'; @@ -34,17 +34,17 @@ export default class Module extends Node { } get gzipSize() { - return this.opts.compressionAlgorithm === 'gzip' ? this.compressedSize('gzip') : undefined; + return this.opts.compressionAlgorithm === 'gzip' ? this.getCompressedSize('gzip') : undefined; } get brotliSize() { - return this.opts.compressionAlgorithm === 'brotli' ? this.compressedSize('brotli') : undefined; + return this.opts.compressionAlgorithm === 'brotli' ? this.getCompressedSize('brotli') : undefined; } - compressedSize(compressionAlgorithm) { + getCompressedSize(compressionAlgorithm) { const key = `_${compressionAlgorithm}Size`; if (!_.has(this, key)) { - this[key] = this.src ? compressedSize(compressionAlgorithm, this.src) : undefined; + this[key] = this.src ? getCompressedSize(compressionAlgorithm, this.src) : undefined; } return this[key]; From b71f9aee5e28ae0b68e7f319e435132e800aae2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cs=C3=A1sz=C3=A1r?= <dcsaszar@users.noreply.github.com> Date: Thu, 15 Apr 2021 12:03:29 +0200 Subject: [PATCH 09/10] Drop defaultSizes: 'compressed' but be forgiving with a gzip/brotli mismatch --- README.md | 14 +++++++++----- src/bin/analyzer.js | 5 ++--- src/viewer.js | 9 +++++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7d4cb15d..3ca5e1ad 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ new BundleAnalyzerPlugin(options?: object) |**`analyzerPort`**|`{Number}` or `auto`|Default: `8888`. Port that will be used in `server` mode to start HTTP server.| |**`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`, `compressed`|Default: `parsed`. Module sizes to show in report by default. [Size definitions](#size-definitions) section describes what these values mean.| +|**`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.| |**`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| @@ -122,7 +122,7 @@ 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, compressed (default: parsed) + Possible values: stat, parsed, gzip, brotli (default: parsed) --compression-algorithm <type> Compression algorithm that will be used to calculate the compressed module sizes. Possible values: gzip, brotli (default: gzip) -O, --no-open Don't open report in default browser automatically. @@ -150,9 +150,13 @@ It is called "stat size" because it's obtained from Webpack's This is the "output" size of your files. If you're using a Webpack plugin such as Uglify, then this value will reflect the minified size of your code. -### `compressed` +### `gzip` -This is the size of running the parsed bundles/modules through compression. +This is the size of running the parsed bundles/modules through gzip compression. + +### `brotli` + +This is the size of running the parsed bundles/modules through Brotli compression. <h2 align="center">Selecting Which Chunks to Display</h2> @@ -172,7 +176,7 @@ The Chunk Context Menu can be opened by right-clicking or `Ctrl`-clicking on a s <h2 align="center">Troubleshooting</h2> -### I don't see `compressed` or `parsed` sizes, it only shows `stat` size +### I don't see `gzip` or `parsed` sizes, it only shows `stat` size It happens when `webpack-bundle-analyzer` analyzes files that don't actually exist in your file system, for example when you work with `webpack-dev-server` that keeps all the files in RAM. If you use `webpack-bundle-analyzer` as a plugin you won't get any errors, however if you run it via CLI you get the error message in terminal: ``` diff --git a/src/bin/analyzer.js b/src/bin/analyzer.js index 590d2f9f..814615c1 100755 --- a/src/bin/analyzer.js +++ b/src/bin/analyzer.js @@ -10,8 +10,7 @@ const viewer = require('../viewer'); const Logger = require('../Logger'); const utils = require('../utils'); -const SIZES = new Set(['stat', 'parsed', 'compressed']); -const ACCEPTED_SIZES = new Set([...SIZES, 'gzip']); +const SIZES = new Set(['stat', 'parsed', 'gzip', 'brotli']); const ALGORITHMS = new Set(['gzip', 'brotli']); @@ -114,7 +113,7 @@ if (mode === 'server') { port = port === 'auto' ? 0 : Number(port); if (isNaN(port)) showHelp('Invalid port. Should be a number or `auto`'); } -if (!ACCEPTED_SIZES.has(defaultSizes)) { +if (!SIZES.has(defaultSizes)) { showHelp(`Invalid default sizes option. Possible values are: ${[...SIZES].join(', ')}`); } if (!ALGORITHMS.has(compressionAlgorithm)) { diff --git a/src/viewer.js b/src/viewer.js index 6bf0abf1..01b79140 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -22,8 +22,9 @@ function resolveTitle(reportTitle) { } } -function resolveDefaultSizes(defaultSizes) { - return defaultSizes === 'compressed' ? 'gzip' : defaultSizes; +function resolveDefaultSizes(defaultSizes, compressionAlgorithm) { + if (['gzip', 'brotli'].includes(defaultSizes)) return compressionAlgorithm; + return defaultSizes; } module.exports = { @@ -64,7 +65,7 @@ async function startServer(bundleStats, opts) { mode: 'server', title: resolveTitle(reportTitle), chartData, - defaultSizes: resolveDefaultSizes(defaultSizes), + defaultSizes: resolveDefaultSizes(defaultSizes, compressionAlgorithm), compressionAlgorithm, enableWebSocket: true }); @@ -147,7 +148,7 @@ async function generateReport(bundleStats, opts) { mode: 'static', title: resolveTitle(reportTitle), chartData, - defaultSizes: resolveDefaultSizes(defaultSizes), + defaultSizes: resolveDefaultSizes(defaultSizes, compressionAlgorithm), compressionAlgorithm, enableWebSocket: false }); From db1d33da4683d4dd8853d1aa70aab64d7563783c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cs=C3=A1sz=C3=A1r?= <dcsaszar@users.noreply.github.com> Date: Tue, 6 Sep 2022 20:01:11 +0200 Subject: [PATCH 10/10] Update brotli analyzer tests after merge --- test/analyzer.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/analyzer.js b/test/analyzer.js index f8e939da..7b3cc19b 100644 --- a/test/analyzer.js +++ b/test/analyzer.js @@ -269,8 +269,9 @@ async function getChartData() { } async function getCompressionAlgorithm() { - return await nightmare.goto(`file://${__dirname}/output/report.html`).evaluate( - () => window.compressionAlgorithm); + const page = await browser.newPage(); + await page.goto(`file://${__dirname}/output/report.html`); + return await page.evaluate(() => window.compressionAlgorithm); } function forEachChartItem(chartData, cb) {