Skip to content

Commit

Permalink
feat(@angular/ssr): add modulepreload for lazy-loaded routes
Browse files Browse the repository at this point in the history
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 angular#26484
  • Loading branch information
alan-agius4 committed Dec 6, 2024
1 parent 6647247 commit 4281f58
Show file tree
Hide file tree
Showing 20 changed files with 827 additions and 19 deletions.
1 change: 1 addition & 0 deletions packages/angular/build/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ ts_library(
"@npm//@angular/compiler-cli",
"@npm//@babel/core",
"@npm//prettier",
"@npm//typescript",
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,13 @@ export async function executeBuild(

// Perform i18n translation inlining if enabled
if (i18nOptions.shouldInline) {
const result = await inlineI18n(options, executionResult, initialFiles);
const result = await inlineI18n(metafile, options, executionResult, initialFiles);
executionResult.addErrors(result.errors);
executionResult.addWarnings(result.warnings);
executionResult.addPrerenderedRoutes(result.prerenderedRoutes);
} else {
const result = await executePostBundleSteps(
metafile,
options,
executionResult.outputFiles,
executionResult.assetFiles,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/

import type { Metafile } from 'esbuild';
import assert from 'node:assert';
import {
BuildOutputFile,
Expand Down Expand Up @@ -34,6 +35,7 @@ import { OutputMode } from './schema';

/**
* Run additional builds steps including SSG, AppShell, Index HTML file and Service worker generation.
* @param metafile An esbuild metafile object.
* @param options The normalized application builder options used to create the build.
* @param outputFiles The output files of an executed build.
* @param assetFiles The assets of an executed build.
Expand All @@ -42,6 +44,7 @@ import { OutputMode } from './schema';
*/
// eslint-disable-next-line max-lines-per-function
export async function executePostBundleSteps(
metafile: Metafile,
options: NormalizedApplicationBuildOptions,
outputFiles: BuildOutputFile[],
assetFiles: BuildOutputAsset[],
Expand Down Expand Up @@ -71,6 +74,7 @@ export async function executePostBundleSteps(
serverEntryPoint,
prerenderOptions,
appShellOptions,
publicPath,
workspaceRoot,
partialSSRBuild,
} = options;
Expand Down Expand Up @@ -108,6 +112,7 @@ export async function executePostBundleSteps(
}

// Create server manifest
const initialFilesPaths = new Set(initialFiles.keys());
if (serverEntryPoint) {
const { manifestContent, serverAssetsChunks } = generateAngularServerAppManifest(
additionalHtmlOutputFiles,
Expand All @@ -116,6 +121,9 @@ export async function executePostBundleSteps(
undefined,
locale,
baseHref,
initialFilesPaths,
metafile,
publicPath,
);

additionalOutputFiles.push(
Expand Down Expand Up @@ -197,6 +205,9 @@ export async function executePostBundleSteps(
serializableRouteTreeNodeForManifest,
locale,
baseHref,
initialFilesPaths,
metafile,
publicPath,
);

for (const chunk of serverAssetsChunks) {
Expand Down
4 changes: 4 additions & 0 deletions packages/angular/build/src/builders/application/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { BuilderContext } from '@angular-devkit/architect';
import type { Metafile } from 'esbuild';
import { join } from 'node:path';
import { BuildOutputFileType, InitialFileRecord } from '../../tools/esbuild/bundler-context';
import {
Expand All @@ -23,11 +24,13 @@ import { NormalizedApplicationBuildOptions, getLocaleBaseHref } from './options'
/**
* Inlines all active locales as specified by the application build options into all
* application JavaScript files created during the build.
* @param metafile An esbuild metafile object.
* @param options The normalized application builder options used to create the build.
* @param executionResult The result of an executed build.
* @param initialFiles A map containing initial file information for the executed build.
*/
export async function inlineI18n(
metafile: Metafile,
options: NormalizedApplicationBuildOptions,
executionResult: ExecutionResult,
initialFiles: Map<string, InitialFileRecord>,
Expand Down Expand Up @@ -80,6 +83,7 @@ export async function inlineI18n(
additionalOutputFiles,
prerenderedRoutes: generatedRoutes,
} = await executePostBundleSteps(
metafile,
{
...options,
baseHref,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
ensureSourceFileVersions,
} from '../angular-host';
import { replaceBootstrap } from '../transformers/jit-bootstrap-transformer';
import { lazyRoutesTransformer } from '../transformers/lazy-routes-transformer';
import { createWorkerTransformer } from '../transformers/web-worker-transformer';
import { AngularCompilation, DiagnosticModes, EmitFileResult } from './angular-compilation';
import { collectHmrCandidates } from './hmr-candidates';
Expand Down Expand Up @@ -47,6 +48,10 @@ class AngularCompilationState {
export class AotCompilation extends AngularCompilation {
#state?: AngularCompilationState;

constructor(private readonly browserOnlyBuild: boolean) {
super();
}

async initialize(
tsconfig: string,
hostOptions: AngularHostOptions,
Expand Down Expand Up @@ -314,8 +319,12 @@ export class AotCompilation extends AngularCompilation {
transformers.before ??= [];
transformers.before.push(
replaceBootstrap(() => typeScriptProgram.getProgram().getTypeChecker()),
webWorkerTransform,
);
transformers.before.push(webWorkerTransform);

if (!this.browserOnlyBuild) {
transformers.before.push(lazyRoutesTransformer(compilerOptions, compilerHost));
}

// Emit is handled in write file callback when using TypeScript
if (useTypeScriptTranspilation) {
Expand Down
12 changes: 8 additions & 4 deletions packages/angular/build/src/tools/angular/compilation/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,26 @@ import type { AngularCompilation } from './angular-compilation';
* compilation either for AOT or JIT mode. By default a parallel compilation is created
* that uses a Node.js worker thread.
* @param jit True, for Angular JIT compilation; False, for Angular AOT compilation.
* @param browserOnlyBuild True, for browser only builds; False, for browser and server builds.
* @returns An instance of an Angular compilation object.
*/
export async function createAngularCompilation(jit: boolean): Promise<AngularCompilation> {
export async function createAngularCompilation(
jit: boolean,
browserOnlyBuild: boolean,
): Promise<AngularCompilation> {
if (useParallelTs) {
const { ParallelCompilation } = await import('./parallel-compilation');

return new ParallelCompilation(jit);
return new ParallelCompilation(jit, browserOnlyBuild);
}

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

return new JitCompilation();
return new JitCompilation(browserOnlyBuild);
} else {
const { AotCompilation } = await import('./aot-compilation');

return new AotCompilation();
return new AotCompilation(browserOnlyBuild);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { loadEsmModule } from '../../../utils/load-esm';
import { profileSync } from '../../esbuild/profiling';
import { AngularHostOptions, createAngularCompilerHost } from '../angular-host';
import { createJitResourceTransformer } from '../transformers/jit-resource-transformer';
import { lazyRoutesTransformer } from '../transformers/lazy-routes-transformer';
import { createWorkerTransformer } from '../transformers/web-worker-transformer';
import { AngularCompilation, DiagnosticModes, EmitFileResult } from './angular-compilation';

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

constructor(private readonly browserOnlyBuild: boolean) {
super();
}

async initialize(
tsconfig: string,
hostOptions: AngularHostOptions,
Expand Down Expand Up @@ -116,8 +121,8 @@ export class JitCompilation extends AngularCompilation {
replaceResourcesTransform,
webWorkerTransform,
} = this.#state;
const buildInfoFilename =
typeScriptProgram.getCompilerOptions().tsBuildInfoFile ?? '.tsbuildinfo';
const compilerOptions = typeScriptProgram.getCompilerOptions();
const buildInfoFilename = compilerOptions.tsBuildInfoFile ?? '.tsbuildinfo';

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

if (!this.browserOnlyBuild) {
transformers.before.push(lazyRoutesTransformer(compilerOptions, compilerHost));
}

// TypeScript will loop until there are no more affected files in the program
while (
typeScriptProgram.emitNextAffectedFile(writeFileCallback, undefined, undefined, transformers)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ import { AngularCompilation, DiagnosticModes, EmitFileResult } from './angular-c
export class ParallelCompilation extends AngularCompilation {
readonly #worker: WorkerPool;

constructor(readonly jit: boolean) {
constructor(
private readonly jit: boolean,
private readonly browserOnlyBuild: boolean,
) {
super();

// TODO: Convert to import.meta usage during ESM transition
Expand Down Expand Up @@ -99,6 +102,7 @@ export class ParallelCompilation extends AngularCompilation {
fileReplacements: hostOptions.fileReplacements,
tsconfig,
jit: this.jit,
browserOnlyBuild: this.browserOnlyBuild,
stylesheetPort: stylesheetChannel.port2,
optionsPort: optionsChannel.port2,
optionsSignal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { JitCompilation } from './jit-compilation';

export interface InitRequest {
jit: boolean;
browserOnlyBuild: boolean;
tsconfig: string;
fileReplacements?: Record<string, string>;
stylesheetPort: MessagePort;
Expand All @@ -31,7 +32,9 @@ let compilation: AngularCompilation | undefined;
const sourceFileCache = new SourceFileCache();

export async function initialize(request: InitRequest) {
compilation ??= request.jit ? new JitCompilation() : new AotCompilation();
compilation ??= request.jit
? new JitCompilation(request.browserOnlyBuild)
: new AotCompilation(request.browserOnlyBuild);

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

0 comments on commit 4281f58

Please sign in to comment.