Skip to content

Commit 783ee10

Browse files
authored
feat: Brotli compression support (#663)
* Add Brotli support to README documentation-driven development For now, we accept potential inconsistencies between `compressionAlgorithm` and `defaultSizes`. Consolidating the compression API is left for a later major release. See discussions: * https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/432/files#r613949751 * https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/432/files#r614300494 * Add compressionAlgorithm option * Introduce sizeUtils helper * Implement Brotli compression * Add Brotli size support in viewer * Add changelog entry for Brotli support
1 parent 6a8879f commit 783ee10

17 files changed

+222
-73
lines changed

CHANGELOG.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ _Note: Gaps between patch versions are faulty, broken or test releases._
1919
* **Improvement**
2020
* Parse bundles as ES modules based on stats JSON information ([#649](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/649) by [@eamodio](https://github.com/eamodio))
2121

22+
* **New Feature**
23+
* Add support for Brotli compression ([#663](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/663) by [@dcsaszar](https://github.com/dcsaszar))
24+
2225
## 4.10.2
2326

2427
* **Bug Fix**
@@ -75,7 +78,7 @@ _Note: Gaps between patch versions are faulty, broken or test releases._
7578

7679
## 4.6.0
7780

78-
* **New Feature**
81+
* **New Feature**
7982
* Support outputting different URL in server mode ([#520](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/520) by [@southorange1228](https://github.com/southorange1228))
8083
* Use deterministic chunk colors (#[501](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/501) by [@CreativeTechGuy](https://github.com/CreativeTechGuy))
8184

@@ -108,19 +111,19 @@ _Note: Gaps between patch versions are faulty, broken or test releases._
108111

109112
* **Improvement**
110113
* Keep treemap labels visible during zooming animations for better user experience ([#414](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/414) by [@stanislawosinski](https://github.com/stanislawosinski))
111-
114+
112115
* **Bug Fix**
113116
* Don't show an empty tooltip when hovering over the FoamTree attribution group or between top-level groups ([#413](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/413) by [@stanislawosinski](https://github.com/stanislawosinski))
114-
117+
115118
* **Internal**
116119
* Upgrade FoamTree to version 3.5.0, replace vendor dependency with an NPM package ([#412](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/412) by [@stanislawosinski](https://github.com/stanislawosinski))
117-
120+
118121
## 4.3.0
119122

120123
* **Improvement**
121124
* Replace express with builtin node server, reducing number of dependencies ([#398](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/398) by [@TrySound](https://github.com/TrySound))
122125
* Move `filesize` to dev dependencies, reducing number of dependencies ([#401](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/401) by [@realityking](https://github.com/realityking))
123-
126+
124127
* **Internal**
125128
* Replace Travis with GitHub actions ([#402](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/402) by [@valscion](https://github.com/valscion))
126129

@@ -145,10 +148,10 @@ _Note: Gaps between patch versions are faulty, broken or test releases._
145148

146149
* **Improvement**
147150
* Support for Webpack 5
148-
151+
149152
* **Bug Fix**
150153
* Prevent crashes when `openAnalyzer` was set to true in environments where there's no program to handle opening. ([#382](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/382) by [@wbobeirne](https://github.com/wbobeirne))
151-
154+
152155
* **Internal**
153156
* Updated dependencies
154157
* Added support for multiple Webpack versions in tests
@@ -157,7 +160,7 @@ _Note: Gaps between patch versions are faulty, broken or test releases._
157160

158161
* **New Feature**
159162
* Adds option `reportTitle` to set title in HTML reports; default remains date of report generation ([#354](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/354) by [@eoingroat](https://github.com/eoingroat))
160-
163+
161164
* **Improvement**
162165
* Added capability to parse bundles that have child assets generated ([#376](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/376) by [@masterkidan](https://github.com/masterkidan) and [#378](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/378) by [@https://github.com/dabbott](https://github.com/https://github.com/dabbott))
163166

@@ -184,7 +187,7 @@ _Note: Gaps between patch versions are faulty, broken or test releases._
184187

185188
* **Bug Fix**
186189
* Add leading zero to hour & minute on `<title />` when needed ([#314](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/314) by [@mhxbe](https://github.com/mhxbe))
187-
190+
188191
* **Internal**
189192
* Update some dependencies to get rid of vulnerability warnings ([#339](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/339))
190193

@@ -297,7 +300,7 @@ _Note: Gaps between patch versions are faulty, broken or test releases._
297300

298301
* **Improvements**
299302
* Nested folders that contain only one child folder are now visually merged i.e. `folder1 => folder2 => file1` is now shown like `folder1/folder2 => file1` (thanks to [@varun-singh-1](https://github.com/varun-singh-1) for the idea)
300-
303+
301304
* **Internal**
302305
* Dropped support for Node.js v4
303306
* Using MobX for state management
@@ -307,10 +310,10 @@ _Note: Gaps between patch versions are faulty, broken or test releases._
307310

308311
* **Improvement**
309312
* Pretty-format the generated stats.json ([#180](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/180)) [@edmorley](https://github.com/edmorley))
310-
313+
311314
* **Bug Fix**
312315
* Properly parse Webpack 4 async chunk with `Array.concat` optimization ([#184](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/184), fixes [#183](https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/183))
313-
316+
314317
* **Internal**
315318
* Refactor bundle parsing logic ([#184](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/184))
316319

README.md

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ This module will help you:
4545
4. Optimize it!
4646

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

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

@@ -61,7 +61,8 @@ new BundleAnalyzerPlugin(options?: object)
6161
|**`analyzerUrl`**|`{Function}` called with `{ listenHost: string, listenHost: string, boundAddress: server.address}`. [server.address comes from Node.js](https://nodejs.org/api/net.html#serveraddress)| Default: `http://${listenHost}:${boundAddress.port}`. The URL printed to console with server mode.|
6262
|**`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).|
6363
|**`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.|
64-
|**`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.|
64+
|**`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.|
65+
|**`compressionAlgorithm`**|One of: `gzip`, `brotli`|Default: `gzip`. Compression type used to calculate the compressed module sizes.|
6566
|**`openAnalyzer`**|`{Boolean}`|Default: `true`. Automatically open report in default browser.|
6667
|**`generateStatsFile`**|`{Boolean}`|Default: `false`. If `true`, webpack stats JSON file will be generated in bundle output directory|
6768
|**`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.
111112
### `options`
112113

113114
```
114-
-V, --version output the version number
115-
-m, --mode <mode> Analyzer mode. Should be `server`, `static` or `json`.
116-
In `server` mode analyzer will start HTTP server to show bundle report.
117-
In `static` mode single HTML file with bundle report will be generated.
118-
In `json` mode single JSON file with bundle report will be generated. (default: server)
119-
-h, --host <host> Host that will be used in `server` mode to start HTTP server. (default: 127.0.0.1)
120-
-p, --port <n> Port that will be used in `server` mode to start HTTP server. Should be a number or `auto` (default: 8888)
121-
-r, --report <file> Path to bundle report file that will be generated in `static` mode. (default: report.html)
122-
-t, --title <title> String to use in title element of html report. (default: pretty printed current date)
123-
-s, --default-sizes <type> Module sizes to show in treemap by default.
124-
Possible values: stat, parsed, gzip (default: parsed)
125-
-O, --no-open Don't open report in default browser automatically.
126-
-e, --exclude <regexp> Assets that should be excluded from the report.
127-
Can be specified multiple times.
128-
-l, --log-level <level> Log level.
129-
Possible values: debug, info, warn, error, silent (default: info)
130-
-h, --help output usage information
115+
-V, --version output the version number
116+
-m, --mode <mode> Analyzer mode. Should be `server`, `static` or `json`.
117+
In `server` mode analyzer will start HTTP server to show bundle report.
118+
In `static` mode single HTML file with bundle report will be generated.
119+
In `json` mode single JSON file with bundle report will be generated. (default: server)
120+
-h, --host <host> Host that will be used in `server` mode to start HTTP server. (default: 127.0.0.1)
121+
-p, --port <n> Port that will be used in `server` mode to start HTTP server. Should be a number or `auto` (default: 8888)
122+
-r, --report <file> Path to bundle report file that will be generated in `static` mode. (default: report.html)
123+
-t, --title <title> String to use in title element of html report. (default: pretty printed current date)
124+
-s, --default-sizes <type> Module sizes to show in treemap by default.
125+
Possible values: stat, parsed, gzip, brotli (default: parsed)
126+
--compression-algorithm <type> Compression algorithm that will be used to calculate the compressed module sizes.
127+
Possible values: gzip, brotli (default: gzip)
128+
-O, --no-open Don't open report in default browser automatically.
129+
-e, --exclude <regexp> Assets that should be excluded from the report.
130+
Can be specified multiple times.
131+
-l, --log-level <level> Log level.
132+
Possible values: debug, info, warn, error, silent (default: info)
133+
-h, --help output usage information
131134
```
132135

133136
<h2 align="center" id="size-definitions">Size definitions</h2>
@@ -151,6 +154,10 @@ as Uglify, then this value will reflect the minified size of your code.
151154

152155
This is the size of running the parsed bundles/modules through gzip compression.
153156

157+
### `brotli`
158+
159+
This is the size of running the parsed bundles/modules through Brotli compression.
160+
154161
<h2 align="center">Selecting Which Chunks to Display</h2>
155162

156163
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.

client/components/ModulesTreemap.jsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,18 @@ import {store} from '../store';
1919
import ModulesList from './ModulesList';
2020
import Dropdown from './Dropdown';
2121

22-
const SIZE_SWITCH_ITEMS = [
23-
{label: 'Stat', prop: 'statSize'},
24-
{label: 'Parsed', prop: 'parsedSize'},
25-
{label: 'Gzipped', prop: 'gzipSize'}
26-
];
22+
function getSizeSwitchItems() {
23+
const items = [
24+
{label: 'Stat', prop: 'statSize'},
25+
{label: 'Parsed', prop: 'parsedSize'}
26+
];
27+
28+
if (window.compressionAlgorithm === 'gzip') items.push({label: 'Gzipped', prop: 'gzipSize'});
29+
30+
if (window.compressionAlgorithm === 'brotli') items.push({label: 'Brotli', prop: 'brotliSize'});
31+
32+
return items;
33+
};
2734

2835
@observer
2936
export default class ModulesTreemap extends Component {
@@ -144,7 +151,7 @@ export default class ModulesTreemap extends Component {
144151
renderModuleSize(module, sizeType) {
145152
const sizeProp = `${sizeType}Size`;
146153
const size = module[sizeProp];
147-
const sizeLabel = SIZE_SWITCH_ITEMS.find(item => item.prop === sizeProp).label;
154+
const sizeLabel = getSizeSwitchItems().find(item => item.prop === sizeProp).label;
148155
const isActive = (store.activeSize === sizeProp);
149156

150157
return (typeof size === 'number') ?
@@ -168,7 +175,7 @@ export default class ModulesTreemap extends Component {
168175
};
169176

170177
@computed get sizeSwitchItems() {
171-
return store.hasParsedSizes ? SIZE_SWITCH_ITEMS : SIZE_SWITCH_ITEMS.slice(0, 1);
178+
return store.hasParsedSizes ? getSizeSwitchItems() : getSizeSwitchItems().slice(0, 1);
172179
}
173180

174181
@computed get activeSizeItem() {
@@ -331,7 +338,7 @@ export default class ModulesTreemap extends Component {
331338
<br/>
332339
{this.renderModuleSize(module, 'stat')}
333340
{!module.inaccurateSizes && this.renderModuleSize(module, 'parsed')}
334-
{!module.inaccurateSizes && this.renderModuleSize(module, 'gzip')}
341+
{!module.inaccurateSizes && this.renderModuleSize(module, window.compressionAlgorithm)}
335342
{module.path &&
336343
<div>Path: <strong>{module.path}</strong></div>
337344
}

client/store.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import localStorage from './localStorage';
44

55
export class Store {
66
cid = 0;
7-
sizes = new Set(['statSize', 'parsedSize', 'gzipSize']);
7+
sizes = new Set(['statSize', 'parsedSize', 'gzipSize', 'brotliSize']);
88

99
@observable.ref allChunks;
1010
@observable.shallow selectedChunks;

src/BundleAnalyzerPlugin.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class BundleAnalyzerPlugin {
1212
this.opts = {
1313
analyzerMode: 'server',
1414
analyzerHost: '127.0.0.1',
15+
compressionAlgorithm: 'gzip',
1516
reportFilename: null,
1617
reportTitle: utils.defaultTitle,
1718
defaultSizes: 'parsed',
@@ -105,6 +106,7 @@ class BundleAnalyzerPlugin {
105106
host: this.opts.analyzerHost,
106107
port: this.opts.analyzerPort,
107108
reportTitle: this.opts.reportTitle,
109+
compressionAlgorithm: this.opts.compressionAlgorithm,
108110
bundleDir: this.getBundleDirFromCompiler(),
109111
logger: this.logger,
110112
defaultSizes: this.opts.defaultSizes,
@@ -117,6 +119,7 @@ class BundleAnalyzerPlugin {
117119
async generateJSONReport(stats) {
118120
await viewer.generateJSONReport(stats, {
119121
reportFilename: path.resolve(this.compiler.outputPath, this.opts.reportFilename || 'report.json'),
122+
compressionAlgorithm: this.opts.compressionAlgorithm,
120123
bundleDir: this.getBundleDirFromCompiler(),
121124
logger: this.logger,
122125
excludeAssets: this.opts.excludeAssets
@@ -128,6 +131,7 @@ class BundleAnalyzerPlugin {
128131
openBrowser: this.opts.openAnalyzer,
129132
reportFilename: path.resolve(this.compiler.outputPath, this.opts.reportFilename || 'report.html'),
130133
reportTitle: this.opts.reportTitle,
134+
compressionAlgorithm: this.opts.compressionAlgorithm,
131135
bundleDir: this.getBundleDirFromCompiler(),
132136
logger: this.logger,
133137
defaultSizes: this.opts.defaultSizes,

src/analyzer.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
const fs = require('fs');
22
const path = require('path');
33

4-
const gzipSize = require('gzip-size');
54
const {parseChunked} = require('@discoveryjs/json-ext');
65

76
const Logger = require('./Logger');
87
const Folder = require('./tree/Folder').default;
98
const {parseBundle} = require('./parseUtils');
109
const {createAssetsFilter} = require('./utils');
10+
const {getCompressedSize} = require('./sizeUtils');
1111

1212
const FILENAME_QUERY_REGEXP = /\?.*$/u;
1313
const FILENAME_EXTENSIONS = /\.(js|mjs|cjs)$/iu;
@@ -20,6 +20,7 @@ module.exports = {
2020
function getViewerData(bundleStats, bundleDir, opts) {
2121
const {
2222
logger = new Logger(),
23+
compressionAlgorithm,
2324
excludeAssets = null
2425
} = opts || {};
2526

@@ -110,7 +111,8 @@ function getViewerData(bundleStats, bundleDir, opts) {
110111

111112
if (assetSources) {
112113
asset.parsedSize = Buffer.byteLength(assetSources.src);
113-
asset.gzipSize = gzipSize.sync(assetSources.src);
114+
if (compressionAlgorithm === 'gzip') asset.gzipSize = getCompressedSize('gzip', assetSources.src);
115+
if (compressionAlgorithm === 'brotli') asset.brotliSize = getCompressedSize('brotli', assetSources.src);
114116
}
115117

116118
// Picking modules from current bundle script
@@ -151,7 +153,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
151153
}
152154

153155
asset.modules = assetModules;
154-
asset.tree = createModulesTree(asset.modules);
156+
asset.tree = createModulesTree(asset.modules, {compressionAlgorithm});
155157
return result;
156158
}, {});
157159

@@ -166,6 +168,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
166168
statSize: asset.tree.size || asset.size,
167169
parsedSize: asset.parsedSize,
168170
gzipSize: asset.gzipSize,
171+
brotliSize: asset.brotliSize,
169172
groups: Object.values(asset.tree.children).map(i => i.toChartData()),
170173
isInitialByEntrypoint: chunkToInitialByEntrypoint[filename] ?? {}
171174
}));
@@ -220,8 +223,8 @@ function isRuntimeModule(statModule) {
220223
return statModule.moduleType === 'runtime';
221224
}
222225

223-
function createModulesTree(modules) {
224-
const root = new Folder('.');
226+
function createModulesTree(modules, opts) {
227+
const root = new Folder('.', opts);
225228

226229
modules.forEach(module => root.addModule(module));
227230
root.mergeNestedFolders();

src/bin/analyzer.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const Logger = require('../Logger');
1111
const utils = require('../utils');
1212

1313
const SIZES = new Set(['stat', 'parsed', 'gzip']);
14+
const COMPRESSION_ALGORITHMS = new Set(['gzip', 'brotli']);
1415

1516
const program = commander
1617
.version(require('../../package.json').version)
@@ -58,6 +59,12 @@ const program = commander
5859
br(`Possible values: ${[...SIZES].join(', ')}`),
5960
'parsed'
6061
)
62+
.option(
63+
'--compression-algorithm <type>',
64+
'Compression algorithm that will be used to calculate the compressed module sizes.' +
65+
br(`Possible values: ${[...COMPRESSION_ALGORITHMS].join(', ')}`),
66+
'gzip'
67+
)
6168
.option(
6269
'-O, --no-open',
6370
"Don't open report in default browser automatically."
@@ -84,6 +91,7 @@ let {
8491
report: reportFilename,
8592
title: reportTitle,
8693
defaultSizes,
94+
compressionAlgorithm,
8795
logLevel,
8896
open: openBrowser,
8997
exclude: excludeAssets
@@ -104,6 +112,9 @@ if (mode === 'server') {
104112
port = port === 'auto' ? 0 : Number(port);
105113
if (isNaN(port)) showHelp('Invalid port. Should be a number or `auto`');
106114
}
115+
if (!COMPRESSION_ALGORITHMS.has(compressionAlgorithm)) {
116+
showHelp(`Invalid compression algorithm option. Possible values are: ${[...COMPRESSION_ALGORITHMS].join(', ')}`);
117+
}
107118
if (!SIZES.has(defaultSizes)) showHelp(`Invalid default sizes option. Possible values are: ${[...SIZES].join(', ')}`);
108119

109120
bundleStatsFile = resolve(bundleStatsFile);
@@ -121,6 +132,7 @@ async function parseAndAnalyse(bundleStatsFile) {
121132
port,
122133
host,
123134
defaultSizes,
135+
compressionAlgorithm,
124136
reportTitle,
125137
bundleDir,
126138
excludeAssets,
@@ -133,13 +145,15 @@ async function parseAndAnalyse(bundleStatsFile) {
133145
reportFilename: resolve(reportFilename || 'report.html'),
134146
reportTitle,
135147
defaultSizes,
148+
compressionAlgorithm,
136149
bundleDir,
137150
excludeAssets,
138151
logger: new Logger(logLevel)
139152
});
140153
} else if (mode === 'json') {
141154
viewer.generateJSONReport(bundleStats, {
142155
reportFilename: resolve(reportFilename || 'report.json'),
156+
compressionAlgorithm,
143157
bundleDir,
144158
excludeAssets,
145159
logger: new Logger(logLevel)
@@ -159,7 +173,7 @@ function showHelp(error) {
159173
}
160174

161175
function br(str) {
162-
return `\n${' '.repeat(28)}${str}`;
176+
return `\n${' '.repeat(32)}${str}`;
163177
}
164178

165179
function array() {

0 commit comments

Comments
 (0)