Skip to content

Commit bc85637

Browse files
clydindgp1130
authored andcommitted
feat(@angular-devkit/build-angular): add estimated transfer size to build output report
When optimizations are enabled (either scripts or styles), an additional column will now be present in the output report shown in the console for an application build. This additonal column will display the estimated transfer size for each file as well as the total initial estimated transfer size for the initial files. The estimated transfer size is determined by calculating the compressed size of the file using brotli's default settings. In a development configuration (a configuration with optimizations disabled), the calculations are not performed to avoid any potential increase in rebuild speed due to the large size of unoptimized files. Closes: #21394
1 parent 3aa3bfc commit bc85637

File tree

4 files changed

+161
-23
lines changed

4 files changed

+161
-23
lines changed

packages/angular_devkit/build_angular/src/webpack/configs/common.ts

+5
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
ScriptsWebpackPlugin,
3232
} from '../plugins';
3333
import { ProgressPlugin } from '../plugins/progress-plugin';
34+
import { TransferSizePlugin } from '../plugins/transfer-size-plugin';
3435
import { createIvyPlugin } from '../plugins/typescript';
3536
import {
3637
assetPatterns,
@@ -287,6 +288,10 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Config
287288
);
288289
}
289290

291+
if (platform === 'browser' && (scriptsOptimization || stylesOptimization.minify)) {
292+
extraMinimizers.push(new TransferSizePlugin());
293+
}
294+
290295
const externals: Configuration['externals'] = [...externalDependencies];
291296
if (isPlatformServer && !bundleDependencies) {
292297
externals.push(({ context, request }, callback) =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { promisify } from 'util';
10+
import { Compiler } from 'webpack';
11+
import { brotliCompress } from 'zlib';
12+
13+
const brotliCompressAsync = promisify(brotliCompress);
14+
15+
const PLUGIN_NAME = 'angular-transfer-size-estimator';
16+
17+
export class TransferSizePlugin {
18+
constructor() {}
19+
20+
apply(compiler: Compiler) {
21+
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
22+
compilation.hooks.processAssets.tapPromise(
23+
{
24+
name: PLUGIN_NAME,
25+
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ANALYSE,
26+
},
27+
async (compilationAssets) => {
28+
const actions = [];
29+
for (const assetName of Object.keys(compilationAssets)) {
30+
if (!assetName.endsWith('.js') && !assetName.endsWith('.css')) {
31+
continue;
32+
}
33+
34+
const scriptAsset = compilation.getAsset(assetName);
35+
if (!scriptAsset || scriptAsset.source.size() <= 0) {
36+
continue;
37+
}
38+
39+
actions.push(
40+
brotliCompressAsync(scriptAsset.source.source())
41+
.then((result) => {
42+
compilation.updateAsset(assetName, (s) => s, {
43+
estimatedTransferSize: result.length,
44+
});
45+
})
46+
.catch((error) => {
47+
compilation.warnings.push(
48+
new compilation.compiler.webpack.WebpackError(
49+
`Unable to calculate estimated transfer size for '${assetName}'. Reason: ${error.message}`,
50+
),
51+
);
52+
}),
53+
);
54+
}
55+
56+
await Promise.all(actions);
57+
},
58+
);
59+
});
60+
}
61+
}

packages/angular_devkit/build_angular/src/webpack/utils/stats.ts

+83-22
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,28 @@ export function formatSize(size: number): string {
3030
return `${roundedSize.toFixed(fractionDigits)} ${abbreviations[index]}`;
3131
}
3232

33-
export type BundleStatsData = [files: string, names: string, size: number | string];
33+
export type BundleStatsData = [
34+
files: string,
35+
names: string,
36+
rawSize: number | string,
37+
estimatedTransferSize: number | string,
38+
];
3439
export interface BundleStats {
3540
initial: boolean;
3641
stats: BundleStatsData;
3742
}
3843

3944
export function generateBundleStats(info: {
40-
size?: number;
45+
rawSize?: number;
46+
estimatedTransferSize?: number;
4147
files?: string[];
4248
names?: string[];
4349
initial?: boolean;
4450
rendered?: boolean;
4551
}): BundleStats {
46-
const size = typeof info.size === 'number' ? info.size : '-';
52+
const rawSize = typeof info.rawSize === 'number' ? info.rawSize : '-';
53+
const estimatedTransferSize =
54+
typeof info.estimatedTransferSize === 'number' ? info.estimatedTransferSize : '-';
4755
const files =
4856
info.files
4957
?.filter((f) => !f.endsWith('.map'))
@@ -54,14 +62,15 @@ export function generateBundleStats(info: {
5462

5563
return {
5664
initial,
57-
stats: [files, names, size],
65+
stats: [files, names, rawSize, estimatedTransferSize],
5866
};
5967
}
6068

6169
function generateBuildStatsTable(
6270
data: BundleStats[],
6371
colors: boolean,
6472
showTotalSize: boolean,
73+
showEstimatedTransferSize: boolean,
6574
): string {
6675
const g = (x: string) => (colors ? ansiColors.greenBright(x) : x);
6776
const c = (x: string) => (colors ? ansiColors.cyanBright(x) : x);
@@ -71,36 +80,70 @@ function generateBuildStatsTable(
7180
const changedEntryChunksStats: BundleStatsData[] = [];
7281
const changedLazyChunksStats: BundleStatsData[] = [];
7382

74-
let initialTotalSize = 0;
83+
let initialTotalRawSize = 0;
84+
let initialTotalEstimatedTransferSize;
7585

7686
for (const { initial, stats } of data) {
77-
const [files, names, size] = stats;
78-
79-
const data: BundleStatsData = [
80-
g(files),
81-
names,
82-
c(typeof size === 'number' ? formatSize(size) : size),
83-
];
87+
const [files, names, rawSize, estimatedTransferSize] = stats;
88+
89+
let data: BundleStatsData;
90+
91+
if (showEstimatedTransferSize) {
92+
data = [
93+
g(files),
94+
names,
95+
c(typeof rawSize === 'number' ? formatSize(rawSize) : rawSize),
96+
c(
97+
typeof estimatedTransferSize === 'number'
98+
? formatSize(estimatedTransferSize)
99+
: estimatedTransferSize,
100+
),
101+
];
102+
} else {
103+
data = [g(files), names, c(typeof rawSize === 'number' ? formatSize(rawSize) : rawSize), ''];
104+
}
84105

85106
if (initial) {
86107
changedEntryChunksStats.push(data);
87-
if (typeof size === 'number') {
88-
initialTotalSize += size;
108+
if (typeof rawSize === 'number') {
109+
initialTotalRawSize += rawSize;
110+
}
111+
if (showEstimatedTransferSize && typeof estimatedTransferSize === 'number') {
112+
if (initialTotalEstimatedTransferSize === undefined) {
113+
initialTotalEstimatedTransferSize = 0;
114+
}
115+
initialTotalEstimatedTransferSize += estimatedTransferSize;
89116
}
90117
} else {
91118
changedLazyChunksStats.push(data);
92119
}
93120
}
94121

95122
const bundleInfo: (string | number)[][] = [];
123+
const baseTitles = ['Names', 'Raw Size'];
124+
const tableAlign: ('l' | 'r')[] = ['l', 'l', 'r'];
125+
126+
if (showEstimatedTransferSize) {
127+
baseTitles.push('Estimated Transfer Size');
128+
tableAlign.push('r');
129+
}
96130

97131
// Entry chunks
98132
if (changedEntryChunksStats.length) {
99-
bundleInfo.push(['Initial Chunk Files', 'Names', 'Size'].map(bold), ...changedEntryChunksStats);
133+
bundleInfo.push(['Initial Chunk Files', ...baseTitles].map(bold), ...changedEntryChunksStats);
100134

101135
if (showTotalSize) {
102136
bundleInfo.push([]);
103-
bundleInfo.push([' ', 'Initial Total', formatSize(initialTotalSize)].map(bold));
137+
138+
const totalSizeElements = [' ', 'Initial Total', formatSize(initialTotalRawSize)];
139+
if (showEstimatedTransferSize) {
140+
totalSizeElements.push(
141+
typeof initialTotalEstimatedTransferSize === 'number'
142+
? formatSize(initialTotalEstimatedTransferSize)
143+
: '-',
144+
);
145+
}
146+
bundleInfo.push(totalSizeElements.map(bold));
104147
}
105148
}
106149

@@ -111,13 +154,13 @@ function generateBuildStatsTable(
111154

112155
// Lazy chunks
113156
if (changedLazyChunksStats.length) {
114-
bundleInfo.push(['Lazy Chunk Files', 'Names', 'Size'].map(bold), ...changedLazyChunksStats);
157+
bundleInfo.push(['Lazy Chunk Files', ...baseTitles].map(bold), ...changedLazyChunksStats);
115158
}
116159

117160
return textTable(bundleInfo, {
118161
hsep: dim(' | '),
119162
stringLength: (s) => removeColor(s).length,
120-
align: ['l', 'l', 'r'],
163+
align: tableAlign,
121164
});
122165
}
123166

@@ -148,6 +191,7 @@ function statsToString(
148191

149192
const changedChunksStats: BundleStats[] = bundleState ?? [];
150193
let unchangedChunkNumber = 0;
194+
let hasEstimatedTransferSizes = false;
151195
if (!bundleState?.length) {
152196
const isFirstRun = !runsCache.has(json.outputPath || '');
153197

@@ -159,10 +203,26 @@ function statsToString(
159203
}
160204

161205
const assets = json.assets?.filter((asset) => chunk.files?.includes(asset.name));
162-
const summedSize = assets
163-
?.filter((asset) => !asset.name.endsWith('.map'))
164-
.reduce((total, asset) => total + asset.size, 0);
165-
changedChunksStats.push(generateBundleStats({ ...chunk, size: summedSize }));
206+
let rawSize = 0;
207+
let estimatedTransferSize;
208+
if (assets) {
209+
for (const asset of assets) {
210+
if (asset.name.endsWith('.map')) {
211+
continue;
212+
}
213+
214+
rawSize += asset.size;
215+
216+
if (typeof asset.info.estimatedTransferSize === 'number') {
217+
if (estimatedTransferSize === undefined) {
218+
estimatedTransferSize = 0;
219+
hasEstimatedTransferSizes = true;
220+
}
221+
estimatedTransferSize += asset.info.estimatedTransferSize;
222+
}
223+
}
224+
}
225+
changedChunksStats.push(generateBundleStats({ ...chunk, rawSize, estimatedTransferSize }));
166226
}
167227
unchangedChunkNumber = json.chunks.length - changedChunksStats.length;
168228

@@ -186,6 +246,7 @@ function statsToString(
186246
changedChunksStats,
187247
colors,
188248
unchangedChunkNumber === 0,
249+
hasEstimatedTransferSizes,
189250
);
190251

191252
// In some cases we do things outside of webpack context

tests/legacy-cli/e2e/tests/basic/build.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ import { ng } from '../../utils/process';
33

44
export default async function () {
55
// Development build
6-
await ng('build', '--configuration=development');
6+
const { stdout: stdoutDev } = await ng('build', '--configuration=development');
77
await expectFileToMatch('dist/test-project/index.html', 'main.js');
8+
if (stdoutDev.includes('Estimated Transfer Size')) {
9+
throw new Error(
10+
`Expected stdout not to contain 'Estimated Transfer Size' but it did.\n${stdoutDev}`,
11+
);
12+
}
813

914
// Named Development build
1015
await ng('build', 'test-project', '--configuration=development');
@@ -19,6 +24,12 @@ export default async function () {
1924
throw new Error(`Expected stdout to contain 'Initial Total' but it did not.\n${stdout}`);
2025
}
2126

27+
if (!stdout.includes('Estimated Transfer Size')) {
28+
throw new Error(
29+
`Expected stdout to contain 'Estimated Transfer Size' but it did not.\n${stdout}`,
30+
);
31+
}
32+
2233
const logs: string[] = [
2334
'Browser application bundle generation complete',
2435
'Copying assets complete',

0 commit comments

Comments
 (0)