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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 26 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ This module will help you:
4. Optimize it!

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

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

Expand All @@ -61,7 +61,8 @@ new BundleAnalyzerPlugin(options?: object)
|**`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.|
|**`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`, `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|
|**`statsFilename`**|`{String}`|Default: `stats.json`. Name of webpack stats JSON file that will be generated if `generateStatsFile` is `true`. It can be either an absolute path or a path relative to a bundle output directory (which is output.path in webpack config).|
Expand Down Expand Up @@ -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, gzip (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, 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.
-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>
Expand All @@ -151,6 +154,10 @@ as Uglify, then this value will reflect the minified size of your code.

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>

When opened, the report displays all of the Webpack chunks for your project. It's possible to filter to a more specific list of chunks by using the sidebar or the chunk context menu.
Expand Down
23 changes: 15 additions & 8 deletions client/components/ModulesTreemap.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,18 @@ import {store} from '../store';
import ModulesList from './ModulesList';
import Dropdown from './Dropdown';

const SIZE_SWITCH_ITEMS = [
{label: 'Stat', prop: 'statSize'},
{label: 'Parsed', prop: 'parsedSize'},
{label: 'Gzipped', prop: 'gzipSize'}
];
function getSizeSwitchItems() {
const items = [
{label: 'Stat', prop: 'statSize'},
{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
export default class ModulesTreemap extends Component {
Expand Down Expand Up @@ -144,7 +151,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 = getSizeSwitchItems().find(item => item.prop === sizeProp).label;
const isActive = (store.activeSize === sizeProp);

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

@computed get sizeSwitchItems() {
return store.hasParsedSizes ? SIZE_SWITCH_ITEMS : SIZE_SWITCH_ITEMS.slice(0, 1);
return store.hasParsedSizes ? getSizeSwitchItems() : getSizeSwitchItems().slice(0, 1);
}

@computed get activeSizeItem() {
Expand Down Expand Up @@ -331,7 +338,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>
}
Expand Down
2 changes: 1 addition & 1 deletion client/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import localStorage from './localStorage';

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

@observable.ref allChunks;
@observable.shallow selectedChunks;
Expand Down
4 changes: 4 additions & 0 deletions src/BundleAnalyzerPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class BundleAnalyzerPlugin {
this.opts = {
analyzerMode: 'server',
analyzerHost: '127.0.0.1',
compressionAlgorithm: 'gzip',
reportFilename: null,
reportTitle: utils.defaultTitle,
defaultSizes: 'parsed',
Expand Down Expand Up @@ -105,6 +106,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,
Expand All @@ -117,6 +119,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
Expand All @@ -128,6 +131,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,
Expand Down
13 changes: 8 additions & 5 deletions src/analyzer.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
const fs = require('fs');
const path = require('path');

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

const Logger = require('./Logger');
const Folder = require('./tree/Folder').default;
const {parseBundle} = require('./parseUtils');
const {createAssetsFilter} = require('./utils');
const {getCompressedSize} = require('./sizeUtils');

const FILENAME_QUERY_REGEXP = /\?.*$/u;
const FILENAME_EXTENSIONS = /\.(js|mjs|cjs)$/iu;
Expand All @@ -20,6 +20,7 @@ module.exports = {
function getViewerData(bundleStats, bundleDir, opts) {
const {
logger = new Logger(),
compressionAlgorithm,
excludeAssets = null
} = opts || {};

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

if (assetSources) {
asset.parsedSize = Buffer.byteLength(assetSources.src);
asset.gzipSize = gzipSize.sync(assetSources.src);
if (compressionAlgorithm === 'gzip') asset.gzipSize = getCompressedSize('gzip', assetSources.src);
if (compressionAlgorithm === 'brotli') asset.brotliSize = getCompressedSize('brotli', assetSources.src);
}

// Picking modules from current bundle script
Expand Down Expand Up @@ -151,7 +153,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
}

asset.modules = assetModules;
asset.tree = createModulesTree(asset.modules);
asset.tree = createModulesTree(asset.modules, {compressionAlgorithm});
return result;
}, {});

Expand All @@ -166,6 +168,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
statSize: asset.tree.size || asset.size,
parsedSize: asset.parsedSize,
gzipSize: asset.gzipSize,
brotliSize: asset.brotliSize,
groups: Object.values(asset.tree.children).map(i => i.toChartData()),
isInitialByEntrypoint: chunkToInitialByEntrypoint[filename] ?? {}
}));
Expand Down Expand Up @@ -220,8 +223,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();
Expand Down
16 changes: 15 additions & 1 deletion src/bin/analyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +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 program = commander
.version(require('../../package.json').version)
Expand Down Expand Up @@ -58,6 +59,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: ${[...COMPRESSION_ALGORITHMS].join(', ')}`),
'gzip'
)
.option(
'-O, --no-open',
"Don't open report in default browser automatically."
Expand All @@ -84,6 +91,7 @@ let {
report: reportFilename,
title: reportTitle,
defaultSizes,
compressionAlgorithm,
logLevel,
open: openBrowser,
exclude: excludeAssets
Expand All @@ -104,6 +112,9 @@ if (mode === 'server') {
port = port === 'auto' ? 0 : Number(port);
if (isNaN(port)) showHelp('Invalid port. Should be a number or `auto`');
}
if (!COMPRESSION_ALGORITHMS.has(compressionAlgorithm)) {
showHelp(`Invalid compression algorithm option. Possible values are: ${[...COMPRESSION_ALGORITHMS].join(', ')}`);
}
if (!SIZES.has(defaultSizes)) showHelp(`Invalid default sizes option. Possible values are: ${[...SIZES].join(', ')}`);

bundleStatsFile = resolve(bundleStatsFile);
Expand All @@ -121,6 +132,7 @@ async function parseAndAnalyse(bundleStatsFile) {
port,
host,
defaultSizes,
compressionAlgorithm,
reportTitle,
bundleDir,
excludeAssets,
Expand All @@ -133,13 +145,15 @@ async function parseAndAnalyse(bundleStatsFile) {
reportFilename: resolve(reportFilename || 'report.html'),
reportTitle,
defaultSizes,
compressionAlgorithm,
bundleDir,
excludeAssets,
logger: new Logger(logLevel)
});
} else if (mode === 'json') {
viewer.generateJSONReport(bundleStats, {
reportFilename: resolve(reportFilename || 'report.json'),
compressionAlgorithm,
bundleDir,
excludeAssets,
logger: new Logger(logLevel)
Expand All @@ -159,7 +173,7 @@ function showHelp(error) {
}

function br(str) {
return `\n${' '.repeat(28)}${str}`;
return `\n${' '.repeat(32)}${str}`;
}

function array() {
Expand Down
8 changes: 8 additions & 0 deletions src/sizeUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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;

throw new Error(`Unsupported compression algorithm: ${compressionAlgorithm}.`);
}
3 changes: 2 additions & 1 deletion src/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function getScript(filename, mode) {
}
}

function renderViewer({title, enableWebSocket, chartData, entrypoints, defaultSizes, mode} = {}) {
function renderViewer({title, enableWebSocket, chartData, entrypoints, defaultSizes, compressionAlgorithm, mode} = {}) {
return html`<!DOCTYPE html>
<html>
<head>
Expand All @@ -60,6 +60,7 @@ function renderViewer({title, enableWebSocket, chartData, entrypoints, defaultSi
window.chartData = ${escapeJson(chartData)};
window.entrypoints = ${escapeJson(entrypoints)};
window.defaultSizes = ${escapeJson(defaultSizes)};
window.compressionAlgorithm = ${escapeJson(compressionAlgorithm)};
</script>
</body>
</html>`;
Expand Down
10 changes: 7 additions & 3 deletions src/tree/ConcatenatedModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,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();
Expand All @@ -20,6 +20,10 @@ export default class ConcatenatedModule extends Module {
return this.getGzipSize() ?? this.getEstimatedSize('gzipSize');
}

get brotliSize() {
return this.getBrotliSize() ?? this.getEstimatedSize('brotliSize');
}

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

Expand Down Expand Up @@ -53,7 +57,7 @@ export default class ConcatenatedModule extends Module {
});

const ModuleConstructor = moduleData.modules ? ConcatenatedModule : ContentModule;
const module = new ModuleConstructor(fileName, moduleData, this);
const module = new ModuleConstructor(fileName, moduleData, this, this.opts);
currentFolder.addChildModule(module);
}

Expand Down
5 changes: 5 additions & 0 deletions src/tree/ContentFolder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand All @@ -28,6 +32,7 @@ export default class ContentFolder extends BaseFolder {
...super.toChartData(),
parsedSize: this.parsedSize,
gzipSize: this.gzipSize,
brotliSize: this.brotliSize,
inaccurateSizes: true
};
}
Expand Down
4 changes: 4 additions & 0 deletions src/tree/ContentModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down
Loading