Skip to content

Commit e6d5c7e

Browse files
committed
refactor(@angular/build): improve BuildOutputFile property access
The `BuildOutputFile` type's helper functions have been adjusted to cache commonly accessed property values to avoid potentially expensive repeat processing. This includes encoding/decoding UTF-8 content and calculating hash values for the output file content. A size property has also been added to allow consumers to more directly determine the byte size of the output file. The size property is currently unused but will be leveraged in forthcoming updates to bundle budgets and console info logging.
1 parent ab6ccee commit e6d5c7e

File tree

6 files changed

+102
-63
lines changed

6 files changed

+102
-63
lines changed

goldens/public-api/angular/build/index.api.md

+2
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ export interface BuildOutputFile extends OutputFile {
9191
// (undocumented)
9292
clone: () => BuildOutputFile;
9393
// (undocumented)
94+
readonly size: number;
95+
// (undocumented)
9496
type: BuildOutputFileType;
9597
}
9698

packages/angular/build/src/builders/application/execute-post-bundle.ts

+5-9
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
} from '../../tools/esbuild/bundler-context';
1515
import { BuildOutputAsset } from '../../tools/esbuild/bundler-execution-result';
1616
import { generateIndexHtml } from '../../tools/esbuild/index-html-generator';
17-
import { createOutputFileFromText } from '../../tools/esbuild/utils';
17+
import { createOutputFile } from '../../tools/esbuild/utils';
1818
import { maxWorkers } from '../../utils/environment-options';
1919
import { prerenderPages } from '../../utils/server-rendering/prerender';
2020
import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker';
@@ -84,14 +84,14 @@ export async function executePostBundleSteps(
8484

8585
additionalHtmlOutputFiles.set(
8686
indexHtmlOptions.output,
87-
createOutputFileFromText(indexHtmlOptions.output, csrContent, BuildOutputFileType.Browser),
87+
createOutputFile(indexHtmlOptions.output, csrContent, BuildOutputFileType.Browser),
8888
);
8989

9090
if (ssrContent) {
9191
const serverIndexHtmlFilename = 'index.server.html';
9292
additionalHtmlOutputFiles.set(
9393
serverIndexHtmlFilename,
94-
createOutputFileFromText(serverIndexHtmlFilename, ssrContent, BuildOutputFileType.Server),
94+
createOutputFile(serverIndexHtmlFilename, ssrContent, BuildOutputFileType.Server),
9595
);
9696

9797
ssrIndexContent = ssrContent;
@@ -131,7 +131,7 @@ export async function executePostBundleSteps(
131131
for (const [path, content] of Object.entries(output)) {
132132
additionalHtmlOutputFiles.set(
133133
path,
134-
createOutputFileFromText(path, content, BuildOutputFileType.Browser),
134+
createOutputFile(path, content, BuildOutputFileType.Browser),
135135
);
136136
}
137137
}
@@ -153,11 +153,7 @@ export async function executePostBundleSteps(
153153
);
154154

155155
additionalOutputFiles.push(
156-
createOutputFileFromText(
157-
'ngsw.json',
158-
serviceWorkerResult.manifest,
159-
BuildOutputFileType.Browser,
160-
),
156+
createOutputFile('ngsw.json', serviceWorkerResult.manifest, BuildOutputFileType.Browser),
161157
);
162158
additionalAssets.push(...serviceWorkerResult.assetFiles);
163159
} catch (error) {

packages/angular/build/src/tools/esbuild/bundler-context.ts

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export enum BuildOutputFileType {
5555

5656
export interface BuildOutputFile extends OutputFile {
5757
type: BuildOutputFileType;
58+
readonly size: number;
5859
clone: () => BuildOutputFile;
5960
}
6061

packages/angular/build/src/tools/esbuild/bundler-execution-result.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { normalize } from 'node:path';
1111
import type { ChangedFiles } from '../../tools/esbuild/watcher';
1212
import type { SourceFileCache } from './angular/source-file-cache';
1313
import type { BuildOutputFile, BuildOutputFileType, BundlerContext } from './bundler-context';
14-
import { createOutputFileFromText } from './utils';
14+
import { createOutputFile } from './utils';
1515

1616
export interface BuildOutputAsset {
1717
source: string;
@@ -49,8 +49,8 @@ export class ExecutionResult {
4949
private codeBundleCache?: SourceFileCache,
5050
) {}
5151

52-
addOutputFile(path: string, content: string, type: BuildOutputFileType): void {
53-
this.outputFiles.push(createOutputFileFromText(path, content, type));
52+
addOutputFile(path: string, content: string | Uint8Array, type: BuildOutputFileType): void {
53+
this.outputFiles.push(createOutputFile(path, content, type));
5454
}
5555

5656
addAssets(assets: BuildOutputAsset[]): void {

packages/angular/build/src/tools/esbuild/i18n-inliner.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import assert from 'node:assert';
1010
import Piscina from 'piscina';
1111
import { BuildOutputFile, BuildOutputFileType } from './bundler-context';
12-
import { createOutputFileFromText } from './utils';
12+
import { createOutputFile } from './utils';
1313

1414
/**
1515
* A keyword used to indicate if a JavaScript file may require inlining of translations.
@@ -139,9 +139,9 @@ export class I18nInliner {
139139
const type = this.#fileToType.get(file);
140140
assert(type !== undefined, 'localized file should always have a type' + file);
141141

142-
const resultFiles = [createOutputFileFromText(file, code, type)];
142+
const resultFiles = [createOutputFile(file, code, type)];
143143
if (map) {
144-
resultFiles.push(createOutputFileFromText(file + '.map', map, type));
144+
resultFiles.push(createOutputFile(file + '.map', map, type));
145145
}
146146

147147
for (const message of messages) {

packages/angular/build/src/tools/esbuild/utils.ts

+88-48
Original file line numberDiff line numberDiff line change
@@ -294,64 +294,104 @@ export async function emitFilesToDisk<T = BuildOutputAsset | BuildOutputFile>(
294294
}
295295
}
296296

297-
export function createOutputFileFromText(
297+
export function createOutputFile(
298298
path: string,
299-
text: string,
299+
data: string | Uint8Array,
300300
type: BuildOutputFileType,
301301
): BuildOutputFile {
302-
return {
303-
path,
304-
text,
305-
type,
306-
get hash() {
307-
return createHash('sha256').update(this.text).digest('hex');
308-
},
309-
get contents() {
310-
return Buffer.from(this.text, 'utf-8');
311-
},
312-
clone(): BuildOutputFile {
313-
return createOutputFileFromText(this.path, this.text, this.type);
314-
},
315-
};
302+
if (typeof data === 'string') {
303+
let cachedContents: Uint8Array | null = null;
304+
let cachedText: string | null = data;
305+
let cachedHash: string | null = null;
306+
307+
return {
308+
path,
309+
type,
310+
get contents(): Uint8Array {
311+
cachedContents ??= new TextEncoder().encode(data);
312+
313+
return cachedContents;
314+
},
315+
set contents(value: Uint8Array) {
316+
cachedContents = value;
317+
cachedText = null;
318+
},
319+
get text(): string {
320+
cachedText ??= new TextDecoder('utf-8').decode(this.contents);
321+
322+
return cachedText;
323+
},
324+
get size(): number {
325+
return this.contents.byteLength;
326+
},
327+
get hash(): string {
328+
cachedHash ??= createHash('sha256')
329+
.update(cachedText ?? this.contents)
330+
.digest('hex');
331+
332+
return cachedHash;
333+
},
334+
clone(): BuildOutputFile {
335+
return createOutputFile(this.path, cachedText ?? this.contents, this.type);
336+
},
337+
};
338+
} else {
339+
let cachedContents = data;
340+
let cachedText: string | null = null;
341+
let cachedHash: string | null = null;
342+
343+
return {
344+
get contents(): Uint8Array {
345+
return cachedContents;
346+
},
347+
set contents(value: Uint8Array) {
348+
cachedContents = value;
349+
cachedText = null;
350+
},
351+
path,
352+
type,
353+
get size(): number {
354+
return this.contents.byteLength;
355+
},
356+
get text(): string {
357+
cachedText ??= new TextDecoder('utf-8').decode(this.contents);
358+
359+
return cachedText;
360+
},
361+
get hash(): string {
362+
cachedHash ??= createHash('sha256').update(this.contents).digest('hex');
363+
364+
return cachedHash;
365+
},
366+
clone(): BuildOutputFile {
367+
return createOutputFile(this.path, this.contents, this.type);
368+
},
369+
};
370+
}
316371
}
317372

318-
export function createOutputFileFromData(
319-
path: string,
320-
data: Uint8Array,
321-
type: BuildOutputFileType,
322-
): BuildOutputFile {
373+
export function convertOutputFile(file: OutputFile, type: BuildOutputFileType): BuildOutputFile {
374+
let { contents: cachedContents } = file;
375+
let cachedText: string | null = null;
376+
323377
return {
324-
path,
325-
type,
326-
get text() {
327-
return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('utf-8');
378+
get contents(): Uint8Array {
379+
return cachedContents;
328380
},
329-
get hash() {
330-
return createHash('sha256').update(this.text).digest('hex');
381+
set contents(value: Uint8Array) {
382+
cachedContents = value;
383+
cachedText = null;
331384
},
332-
get contents() {
333-
return data;
334-
},
335-
clone(): BuildOutputFile {
336-
return createOutputFileFromData(this.path, this.contents, this.type);
385+
hash: file.hash,
386+
path: file.path,
387+
type,
388+
get size(): number {
389+
return this.contents.byteLength;
337390
},
338-
};
339-
}
340-
341-
export function convertOutputFile(file: OutputFile, type: BuildOutputFileType): BuildOutputFile {
342-
const { path, contents, hash } = file;
391+
get text(): string {
392+
cachedText ??= new TextDecoder('utf-8').decode(this.contents);
343393

344-
return {
345-
contents,
346-
hash,
347-
path,
348-
type,
349-
get text() {
350-
return Buffer.from(
351-
this.contents.buffer,
352-
this.contents.byteOffset,
353-
this.contents.byteLength,
354-
).toString('utf-8');
394+
return cachedText;
355395
},
356396
clone(): BuildOutputFile {
357397
return convertOutputFile(this, this.type);

0 commit comments

Comments
 (0)