Skip to content

Commit 8d7a51d

Browse files
committedDec 10, 2024·
feat(@angular/ssr): add modulepreload for lazy-loaded routes
Enhance performance when using SSR by adding `modulepreload` links to lazy-loaded routes. This ensures that the required modules are preloaded in the background, improving the user experience and reducing the time to interactive. Closes #26484
1 parent faa710e commit 8d7a51d

20 files changed

+921
-20
lines changed
 

‎packages/angular/build/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ ts_library(
127127
"@npm//@angular/compiler-cli",
128128
"@npm//@babel/core",
129129
"@npm//prettier",
130+
"@npm//typescript",
130131
],
131132
)
132133

‎packages/angular/build/src/builders/application/execute-build.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -247,12 +247,13 @@ export async function executeBuild(
247247

248248
// Perform i18n translation inlining if enabled
249249
if (i18nOptions.shouldInline) {
250-
const result = await inlineI18n(options, executionResult, initialFiles);
250+
const result = await inlineI18n(metafile, options, executionResult, initialFiles);
251251
executionResult.addErrors(result.errors);
252252
executionResult.addWarnings(result.warnings);
253253
executionResult.addPrerenderedRoutes(result.prerenderedRoutes);
254254
} else {
255255
const result = await executePostBundleSteps(
256+
metafile,
256257
options,
257258
executionResult.outputFiles,
258259
executionResult.assetFiles,

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

+11
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import type { Metafile } from 'esbuild';
910
import assert from 'node:assert';
1011
import {
1112
BuildOutputFile,
@@ -34,6 +35,7 @@ import { OutputMode } from './schema';
3435

3536
/**
3637
* Run additional builds steps including SSG, AppShell, Index HTML file and Service worker generation.
38+
* @param metafile An esbuild metafile object.
3739
* @param options The normalized application builder options used to create the build.
3840
* @param outputFiles The output files of an executed build.
3941
* @param assetFiles The assets of an executed build.
@@ -42,6 +44,7 @@ import { OutputMode } from './schema';
4244
*/
4345
// eslint-disable-next-line max-lines-per-function
4446
export async function executePostBundleSteps(
47+
metafile: Metafile,
4548
options: NormalizedApplicationBuildOptions,
4649
outputFiles: BuildOutputFile[],
4750
assetFiles: BuildOutputAsset[],
@@ -71,6 +74,7 @@ export async function executePostBundleSteps(
7174
serverEntryPoint,
7275
prerenderOptions,
7376
appShellOptions,
77+
publicPath,
7478
workspaceRoot,
7579
partialSSRBuild,
7680
} = options;
@@ -108,6 +112,7 @@ export async function executePostBundleSteps(
108112
}
109113

110114
// Create server manifest
115+
const initialFilesPaths = new Set(initialFiles.keys());
111116
if (serverEntryPoint) {
112117
const { manifestContent, serverAssetsChunks } = generateAngularServerAppManifest(
113118
additionalHtmlOutputFiles,
@@ -116,6 +121,9 @@ export async function executePostBundleSteps(
116121
undefined,
117122
locale,
118123
baseHref,
124+
initialFilesPaths,
125+
metafile,
126+
publicPath,
119127
);
120128

121129
additionalOutputFiles.push(
@@ -197,6 +205,9 @@ export async function executePostBundleSteps(
197205
serializableRouteTreeNodeForManifest,
198206
locale,
199207
baseHref,
208+
initialFilesPaths,
209+
metafile,
210+
publicPath,
200211
);
201212

202213
for (const chunk of serverAssetsChunks) {

‎packages/angular/build/src/builders/application/i18n.ts

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import { BuilderContext } from '@angular-devkit/architect';
10+
import type { Metafile } from 'esbuild';
1011
import { join } from 'node:path';
1112
import { BuildOutputFileType, InitialFileRecord } from '../../tools/esbuild/bundler-context';
1213
import {
@@ -23,11 +24,13 @@ import { NormalizedApplicationBuildOptions, getLocaleBaseHref } from './options'
2324
/**
2425
* Inlines all active locales as specified by the application build options into all
2526
* application JavaScript files created during the build.
27+
* @param metafile An esbuild metafile object.
2628
* @param options The normalized application builder options used to create the build.
2729
* @param executionResult The result of an executed build.
2830
* @param initialFiles A map containing initial file information for the executed build.
2931
*/
3032
export async function inlineI18n(
33+
metafile: Metafile,
3134
options: NormalizedApplicationBuildOptions,
3235
executionResult: ExecutionResult,
3336
initialFiles: Map<string, InitialFileRecord>,
@@ -79,6 +82,7 @@ export async function inlineI18n(
7982
additionalOutputFiles,
8083
prerenderedRoutes: generatedRoutes,
8184
} = await executePostBundleSteps(
85+
metafile,
8286
{
8387
...options,
8488
baseHref: getLocaleBaseHref(baseHref, i18nOptions, locale) ?? baseHref,

‎packages/angular/build/src/tools/angular/compilation/aot-compilation.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
ensureSourceFileVersions,
1818
} from '../angular-host';
1919
import { replaceBootstrap } from '../transformers/jit-bootstrap-transformer';
20+
import { lazyRoutesTransformer } from '../transformers/lazy-routes-transformer';
2021
import { createWorkerTransformer } from '../transformers/web-worker-transformer';
2122
import { AngularCompilation, DiagnosticModes, EmitFileResult } from './angular-compilation';
2223
import { collectHmrCandidates } from './hmr-candidates';
@@ -47,6 +48,10 @@ class AngularCompilationState {
4748
export class AotCompilation extends AngularCompilation {
4849
#state?: AngularCompilationState;
4950

51+
constructor(private readonly browserOnlyBuild: boolean) {
52+
super();
53+
}
54+
5055
async initialize(
5156
tsconfig: string,
5257
hostOptions: AngularHostOptions,
@@ -314,8 +319,12 @@ export class AotCompilation extends AngularCompilation {
314319
transformers.before ??= [];
315320
transformers.before.push(
316321
replaceBootstrap(() => typeScriptProgram.getProgram().getTypeChecker()),
322+
webWorkerTransform,
317323
);
318-
transformers.before.push(webWorkerTransform);
324+
325+
if (!this.browserOnlyBuild) {
326+
transformers.before.push(lazyRoutesTransformer(compilerOptions, compilerHost));
327+
}
319328

320329
// Emit is handled in write file callback when using TypeScript
321330
if (useTypeScriptTranspilation) {

‎packages/angular/build/src/tools/angular/compilation/factory.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,26 @@ import type { AngularCompilation } from './angular-compilation';
1414
* compilation either for AOT or JIT mode. By default a parallel compilation is created
1515
* that uses a Node.js worker thread.
1616
* @param jit True, for Angular JIT compilation; False, for Angular AOT compilation.
17+
* @param browserOnlyBuild True, for browser only builds; False, for browser and server builds.
1718
* @returns An instance of an Angular compilation object.
1819
*/
19-
export async function createAngularCompilation(jit: boolean): Promise<AngularCompilation> {
20+
export async function createAngularCompilation(
21+
jit: boolean,
22+
browserOnlyBuild: boolean,
23+
): Promise<AngularCompilation> {
2024
if (useParallelTs) {
2125
const { ParallelCompilation } = await import('./parallel-compilation');
2226

23-
return new ParallelCompilation(jit);
27+
return new ParallelCompilation(jit, browserOnlyBuild);
2428
}
2529

2630
if (jit) {
2731
const { JitCompilation } = await import('./jit-compilation');
2832

29-
return new JitCompilation();
33+
return new JitCompilation(browserOnlyBuild);
3034
} else {
3135
const { AotCompilation } = await import('./aot-compilation');
3236

33-
return new AotCompilation();
37+
return new AotCompilation(browserOnlyBuild);
3438
}
3539
}

‎packages/angular/build/src/tools/angular/compilation/jit-compilation.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { loadEsmModule } from '../../../utils/load-esm';
1313
import { profileSync } from '../../esbuild/profiling';
1414
import { AngularHostOptions, createAngularCompilerHost } from '../angular-host';
1515
import { createJitResourceTransformer } from '../transformers/jit-resource-transformer';
16+
import { lazyRoutesTransformer } from '../transformers/lazy-routes-transformer';
1617
import { createWorkerTransformer } from '../transformers/web-worker-transformer';
1718
import { AngularCompilation, DiagnosticModes, EmitFileResult } from './angular-compilation';
1819

@@ -29,6 +30,10 @@ class JitCompilationState {
2930
export class JitCompilation extends AngularCompilation {
3031
#state?: JitCompilationState;
3132

33+
constructor(private readonly browserOnlyBuild: boolean) {
34+
super();
35+
}
36+
3237
async initialize(
3338
tsconfig: string,
3439
hostOptions: AngularHostOptions,
@@ -116,8 +121,8 @@ export class JitCompilation extends AngularCompilation {
116121
replaceResourcesTransform,
117122
webWorkerTransform,
118123
} = this.#state;
119-
const buildInfoFilename =
120-
typeScriptProgram.getCompilerOptions().tsBuildInfoFile ?? '.tsbuildinfo';
124+
const compilerOptions = typeScriptProgram.getCompilerOptions();
125+
const buildInfoFilename = compilerOptions.tsBuildInfoFile ?? '.tsbuildinfo';
121126

122127
const emittedFiles: EmitFileResult[] = [];
123128
const writeFileCallback: ts.WriteFileCallback = (filename, contents, _a, _b, sourceFiles) => {
@@ -140,6 +145,10 @@ export class JitCompilation extends AngularCompilation {
140145
],
141146
};
142147

148+
if (!this.browserOnlyBuild) {
149+
transformers.before.push(lazyRoutesTransformer(compilerOptions, compilerHost));
150+
}
151+
143152
// TypeScript will loop until there are no more affected files in the program
144153
while (
145154
typeScriptProgram.emitNextAffectedFile(writeFileCallback, undefined, undefined, transformers)

‎packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ import { AngularCompilation, DiagnosticModes, EmitFileResult } from './angular-c
2626
export class ParallelCompilation extends AngularCompilation {
2727
readonly #worker: WorkerPool;
2828

29-
constructor(readonly jit: boolean) {
29+
constructor(
30+
private readonly jit: boolean,
31+
private readonly browserOnlyBuild: boolean,
32+
) {
3033
super();
3134

3235
// TODO: Convert to import.meta usage during ESM transition
@@ -99,6 +102,7 @@ export class ParallelCompilation extends AngularCompilation {
99102
fileReplacements: hostOptions.fileReplacements,
100103
tsconfig,
101104
jit: this.jit,
105+
browserOnlyBuild: this.browserOnlyBuild,
102106
stylesheetPort: stylesheetChannel.port2,
103107
optionsPort: optionsChannel.port2,
104108
optionsSignal,

‎packages/angular/build/src/tools/angular/compilation/parallel-worker.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { JitCompilation } from './jit-compilation';
1717

1818
export interface InitRequest {
1919
jit: boolean;
20+
browserOnlyBuild: boolean;
2021
tsconfig: string;
2122
fileReplacements?: Record<string, string>;
2223
stylesheetPort: MessagePort;
@@ -31,7 +32,9 @@ let compilation: AngularCompilation | undefined;
3132
const sourceFileCache = new SourceFileCache();
3233

3334
export async function initialize(request: InitRequest) {
34-
compilation ??= request.jit ? new JitCompilation() : new AotCompilation();
35+
compilation ??= request.jit
36+
? new JitCompilation(request.browserOnlyBuild)
37+
: new AotCompilation(request.browserOnlyBuild);
3538

3639
const stylesheetRequests = new Map<string, [(value: string) => void, (reason: Error) => void]>();
3740
request.stylesheetPort.on('message', ({ requestId, value, error }) => {

0 commit comments

Comments
 (0)
Please sign in to comment.