diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel
index 5f8cb394d6f6..1aa140f21887 100644
--- a/packages/angular/build/BUILD.bazel
+++ b/packages/angular/build/BUILD.bazel
@@ -127,6 +127,7 @@ ts_library(
         "@npm//@angular/compiler-cli",
         "@npm//@babel/core",
         "@npm//prettier",
+        "@npm//typescript",
     ],
 )
 
diff --git a/packages/angular/build/src/builders/application/execute-build.ts b/packages/angular/build/src/builders/application/execute-build.ts
index b5ca83a76405..3da1b3721f11 100644
--- a/packages/angular/build/src/builders/application/execute-build.ts
+++ b/packages/angular/build/src/builders/application/execute-build.ts
@@ -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,
diff --git a/packages/angular/build/src/builders/application/execute-post-bundle.ts b/packages/angular/build/src/builders/application/execute-post-bundle.ts
index 31b4a9d2e97c..5066d5aaca01 100644
--- a/packages/angular/build/src/builders/application/execute-post-bundle.ts
+++ b/packages/angular/build/src/builders/application/execute-post-bundle.ts
@@ -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,
@@ -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.
@@ -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[],
@@ -71,6 +74,7 @@ export async function executePostBundleSteps(
     serverEntryPoint,
     prerenderOptions,
     appShellOptions,
+    publicPath,
     workspaceRoot,
     partialSSRBuild,
   } = options;
@@ -108,6 +112,7 @@ export async function executePostBundleSteps(
   }
 
   // Create server manifest
+  const initialFilesPaths = new Set(initialFiles.keys());
   if (serverEntryPoint) {
     const { manifestContent, serverAssetsChunks } = generateAngularServerAppManifest(
       additionalHtmlOutputFiles,
@@ -116,6 +121,9 @@ export async function executePostBundleSteps(
       undefined,
       locale,
       baseHref,
+      initialFilesPaths,
+      metafile,
+      publicPath,
     );
 
     additionalOutputFiles.push(
@@ -197,6 +205,9 @@ export async function executePostBundleSteps(
         serializableRouteTreeNodeForManifest,
         locale,
         baseHref,
+        initialFilesPaths,
+        metafile,
+        publicPath,
       );
 
       for (const chunk of serverAssetsChunks) {
diff --git a/packages/angular/build/src/builders/application/i18n.ts b/packages/angular/build/src/builders/application/i18n.ts
index 101956f6319a..f526286e35a7 100644
--- a/packages/angular/build/src/builders/application/i18n.ts
+++ b/packages/angular/build/src/builders/application/i18n.ts
@@ -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 {
@@ -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>,
@@ -79,6 +82,7 @@ export async function inlineI18n(
         additionalOutputFiles,
         prerenderedRoutes: generatedRoutes,
       } = await executePostBundleSteps(
+        metafile,
         {
           ...options,
           baseHref: getLocaleBaseHref(baseHref, i18nOptions, locale) ?? baseHref,
diff --git a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts
index f111224c36d8..a807e0ea791d 100644
--- a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts
+++ b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts
@@ -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';
@@ -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,
@@ -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) {
diff --git a/packages/angular/build/src/tools/angular/compilation/factory.ts b/packages/angular/build/src/tools/angular/compilation/factory.ts
index 5984b4815f6a..91447dea24cf 100644
--- a/packages/angular/build/src/tools/angular/compilation/factory.ts
+++ b/packages/angular/build/src/tools/angular/compilation/factory.ts
@@ -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);
   }
 }
diff --git a/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts b/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts
index db2de81b4ae7..a811cb50ec0a 100644
--- a/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts
+++ b/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts
@@ -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';
 
@@ -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,
@@ -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) => {
@@ -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)
diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts b/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts
index f3b3503f6988..be612cbfcad4 100644
--- a/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts
+++ b/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts
@@ -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
@@ -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,
diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts
index 2669951c12e4..d67fbb9bd06c 100644
--- a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts
+++ b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts
@@ -17,6 +17,7 @@ import { JitCompilation } from './jit-compilation';
 
 export interface InitRequest {
   jit: boolean;
+  browserOnlyBuild: boolean;
   tsconfig: string;
   fileReplacements?: Record<string, string>;
   stylesheetPort: MessagePort;
@@ -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 }) => {
diff --git a/packages/angular/build/src/tools/angular/transformers/lazy-routes-transformer.ts b/packages/angular/build/src/tools/angular/transformers/lazy-routes-transformer.ts
new file mode 100644
index 000000000000..10d45d00d714
--- /dev/null
+++ b/packages/angular/build/src/tools/angular/transformers/lazy-routes-transformer.ts
@@ -0,0 +1,225 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.dev/license
+ */
+
+import assert from 'node:assert';
+import { relative } from 'node:path/posix';
+import ts from 'typescript';
+
+/**
+ * A transformer factory that adds a property to the lazy-loaded route object.
+ * This property is used to allow for the retrieval of the module path during SSR.
+ *
+ * @param compilerOptions The compiler options.
+ * @param compilerHost The compiler host.
+ * @returns A transformer factory.
+ *
+ * @example
+ * **Before:**
+ * ```ts
+ * const routes: Routes = [
+ *   {
+ *     path: 'lazy',
+ *     loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule)
+ *   }
+ * ];
+ * ```
+ *
+ * **After:**
+ * ```ts
+ * const routes: Routes = [
+ *   {
+ *     path: 'lazy',
+ *     loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule),
+ *     ...(typeof ngServerMode !== "undefined" && ngServerMode ? { ɵentryName: "./lazy/lazy.module.ts" }: {})
+ *   }
+ * ];
+ * ```
+ */
+export function lazyRoutesTransformer(
+  compilerOptions: ts.CompilerOptions,
+  compilerHost: ts.CompilerHost,
+): ts.TransformerFactory<ts.SourceFile> {
+  const moduleResolutionCache = compilerHost.getModuleResolutionCache?.();
+  assert(
+    typeof compilerOptions.basePath === 'string',
+    'compilerOptions.basePath should be a string.',
+  );
+  const basePath = compilerOptions.basePath;
+
+  return (context: ts.TransformationContext) => {
+    const factory = context.factory;
+
+    const visitor = (node: ts.Node): ts.Node => {
+      if (!ts.isObjectLiteralExpression(node)) {
+        // Not an object literal, so skip it.
+        return ts.visitEachChild(node, visitor, context);
+      }
+
+      const loadFunction = getLoadComponentOrChildrenProperty(node)?.initializer;
+      // Check if the initializer is an arrow function or a function expression
+      if (
+        !loadFunction ||
+        (!ts.isArrowFunction(loadFunction) && !ts.isFunctionExpression(loadFunction))
+      ) {
+        return ts.visitEachChild(node, visitor, context);
+      }
+
+      let callExpression: ts.CallExpression | undefined;
+
+      if (ts.isArrowFunction(loadFunction)) {
+        // Handle arrow functions: body can either be a block or a direct call expression
+        const body = loadFunction.body;
+
+        if (ts.isBlock(body)) {
+          // Arrow function with a block: check the first statement for a return call expression
+          const firstStatement = body.statements[0];
+
+          if (
+            firstStatement &&
+            ts.isReturnStatement(firstStatement) &&
+            firstStatement.expression &&
+            ts.isCallExpression(firstStatement.expression)
+          ) {
+            callExpression = firstStatement.expression;
+          }
+        } else if (ts.isCallExpression(body)) {
+          // Arrow function with a direct call expression as its body
+          callExpression = body;
+        }
+      } else if (ts.isFunctionExpression(loadFunction)) {
+        // Handle function expressions: check for a return statement with a call expression
+        const returnExpression = loadFunction.body.statements.find(
+          ts.isReturnStatement,
+        )?.expression;
+
+        if (returnExpression && ts.isCallExpression(returnExpression)) {
+          callExpression = returnExpression;
+        }
+      }
+
+      if (!callExpression) {
+        return ts.visitEachChild(node, visitor, context);
+      }
+
+      // Optionally check for the 'then' property access expression
+      const expression = callExpression.expression;
+      if (
+        !ts.isCallExpression(expression) &&
+        ts.isPropertyAccessExpression(expression) &&
+        expression.name.text !== 'then'
+      ) {
+        return ts.visitEachChild(node, visitor, context);
+      }
+
+      const importExpression = ts.isPropertyAccessExpression(expression)
+        ? expression.expression // Navigate to the underlying expression for 'then'
+        : callExpression;
+
+      // Ensure the underlying expression is an import call
+      if (
+        !ts.isCallExpression(importExpression) ||
+        importExpression.expression.kind !== ts.SyntaxKind.ImportKeyword
+      ) {
+        return ts.visitEachChild(node, visitor, context);
+      }
+
+      // Check if the argument to the import call is a string literal
+      const callExpressionArgument = importExpression.arguments[0];
+      if (!ts.isStringLiteralLike(callExpressionArgument)) {
+        // Not a string literal, so skip it.
+        return ts.visitEachChild(node, visitor, context);
+      }
+
+      const resolvedPath = ts.resolveModuleName(
+        callExpressionArgument.text,
+        node.getSourceFile().fileName,
+        compilerOptions,
+        compilerHost,
+        moduleResolutionCache,
+      )?.resolvedModule?.resolvedFileName;
+
+      if (!resolvedPath) {
+        // Could not resolve the module, so skip it.
+        return ts.visitEachChild(node, visitor, context);
+      }
+
+      const resolvedRelativePath = relative(basePath, resolvedPath);
+
+      // Create the new property
+      // Example: `...(typeof ngServerMode !== "undefined" && ngServerMode ? { ɵentryName: "src/home.ts" }: {})`
+      const newProperty = factory.createSpreadAssignment(
+        factory.createParenthesizedExpression(
+          factory.createConditionalExpression(
+            factory.createBinaryExpression(
+              factory.createBinaryExpression(
+                factory.createTypeOfExpression(factory.createIdentifier('ngServerMode')),
+                factory.createToken(ts.SyntaxKind.ExclamationEqualsEqualsToken),
+                factory.createStringLiteral('undefined'),
+              ),
+              factory.createToken(ts.SyntaxKind.AmpersandAmpersandToken),
+              factory.createIdentifier('ngServerMode'),
+            ),
+            factory.createToken(ts.SyntaxKind.QuestionToken),
+            factory.createObjectLiteralExpression([
+              factory.createPropertyAssignment(
+                factory.createIdentifier('ɵentryName'),
+                factory.createStringLiteral(resolvedRelativePath),
+              ),
+            ]),
+            factory.createToken(ts.SyntaxKind.ColonToken),
+            factory.createObjectLiteralExpression([]),
+          ),
+        ),
+      );
+
+      // Add the new property to the object literal.
+      return factory.updateObjectLiteralExpression(node, [...node.properties, newProperty]);
+    };
+
+    return (sourceFile) => {
+      const text = sourceFile.text;
+      if (!text.includes('loadC')) {
+        // Fast check for 'loadComponent' and 'loadChildren'.
+        return sourceFile;
+      }
+
+      return ts.visitEachChild(sourceFile, visitor, context);
+    };
+  };
+}
+
+/**
+ * Retrieves the property assignment for the `loadComponent` or `loadChildren` property of a route object.
+ *
+ * @param node The object literal expression to search.
+ * @returns The property assignment if found, otherwise `undefined`.
+ */
+function getLoadComponentOrChildrenProperty(
+  node: ts.ObjectLiteralExpression,
+): ts.PropertyAssignment | undefined {
+  let hasPathProperty = false;
+  let loadComponentOrChildrenProperty: ts.PropertyAssignment | undefined;
+  for (const prop of node.properties) {
+    if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
+      continue;
+    }
+
+    const propertyNameText = prop.name.text;
+    if (propertyNameText === 'path') {
+      hasPathProperty = true;
+    } else if (propertyNameText === 'loadComponent' || propertyNameText === 'loadChildren') {
+      loadComponentOrChildrenProperty = prop;
+    }
+
+    if (hasPathProperty && loadComponentOrChildrenProperty) {
+      break;
+    }
+  }
+
+  return loadComponentOrChildrenProperty;
+}
diff --git a/packages/angular/build/src/tools/angular/transformers/lazy-routes-transformer_spec.ts b/packages/angular/build/src/tools/angular/transformers/lazy-routes-transformer_spec.ts
new file mode 100644
index 000000000000..4dd388f28eb1
--- /dev/null
+++ b/packages/angular/build/src/tools/angular/transformers/lazy-routes-transformer_spec.ts
@@ -0,0 +1,208 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.dev/license
+ */
+
+import ts from 'typescript';
+import { lazyRoutesTransformer } from './lazy-routes-transformer';
+
+describe('lazyRoutesTransformer', () => {
+  let program: ts.Program;
+  let compilerHost: ts.CompilerHost;
+
+  beforeEach(() => {
+    // Mock a basic TypeScript program and compilerHost
+    program = ts.createProgram(['/project/src/dummy.ts'], { basePath: '/project/' });
+    compilerHost = {
+      getNewLine: () => '\n',
+      fileExists: () => true,
+      readFile: () => '',
+      writeFile: () => undefined,
+      getCanonicalFileName: (fileName: string) => fileName,
+      getCurrentDirectory: () => '/project',
+      getDefaultLibFileName: () => 'lib.d.ts',
+      getSourceFile: () => undefined,
+      useCaseSensitiveFileNames: () => true,
+      resolveModuleNames: (moduleNames, containingFile) =>
+        moduleNames.map(
+          (name) =>
+            ({
+              resolvedFileName: `/project/src/${name}.ts`,
+            }) as ts.ResolvedModule,
+        ),
+    };
+  });
+
+  const transformSourceFile = (sourceCode: string): ts.SourceFile => {
+    const sourceFile = ts.createSourceFile(
+      '/project/src/dummy.ts',
+      sourceCode,
+      ts.ScriptTarget.ESNext,
+      true,
+      ts.ScriptKind.TS,
+    );
+
+    const transformer = lazyRoutesTransformer(program.getCompilerOptions(), compilerHost);
+    const result = ts.transform(sourceFile, [transformer]);
+
+    return result.transformed[0];
+  };
+
+  it('should return the same object when the routes array contains an empty object', () => {
+    const source = `
+      const routes = [{}];
+    `;
+
+    const transformedSourceFile = transformSourceFile(source);
+    const transformedCode = ts.createPrinter().printFile(transformedSourceFile);
+
+    expect(transformedCode).toContain(`const routes = [{}]`);
+  });
+
+  it('should add ɵentryName property to object with loadComponent and path (Arrow function)', () => {
+    const source = `
+      const routes = [
+        {
+          path: 'home',
+          loadComponent: () => import('./home').then(m => m.HomeComponent)
+        }
+      ];
+    `;
+
+    const transformedSourceFile = transformSourceFile(source);
+    const transformedCode = ts.createPrinter().printFile(transformedSourceFile);
+
+    expect(transformedCode).toContain(
+      `...(typeof ngServerMode !== "undefined" && ngServerMode ? { ɵentryName: "src/home.ts" } : {})`,
+    );
+  });
+
+  it('should add ɵentryName property to object with loadComponent and path (Arrow function with return)', () => {
+    const source = `
+      const routes = [
+        {
+          path: 'home',
+          loadComponent: () => {
+            return import('./home').then(m => m.HomeComponent);
+          }
+        }
+      ];
+    `;
+
+    const transformedSourceFile = transformSourceFile(source);
+    const transformedCode = ts.createPrinter().printFile(transformedSourceFile);
+
+    expect(transformedCode).toContain(
+      `...(typeof ngServerMode !== "undefined" && ngServerMode ? { ɵentryName: "src/home.ts" } : {})`,
+    );
+  });
+
+  it('should add ɵentryName property to object with loadComponent and path (Arrow function without .then)', () => {
+    const source = `
+      const routes = [
+        {
+          path: 'about',
+          loadComponent: () => import('./about')
+        }
+      ];
+    `;
+
+    const transformedSourceFile = transformSourceFile(source);
+    const transformedCode = ts.createPrinter().printFile(transformedSourceFile);
+
+    expect(transformedCode).toContain(
+      `...(typeof ngServerMode !== "undefined" && ngServerMode ? { ɵentryName: "src/about.ts" } : {})`,
+    );
+  });
+
+  it('should add ɵentryName property to object with loadComponent using return and .then', () => {
+    const source = `
+      const routes = [
+        {
+          path: '',
+          loadComponent: () => {
+            return import('./home').then((m) => m.HomeComponent);
+          }
+        }
+      ];
+    `;
+
+    const transformedSourceFile = transformSourceFile(source);
+    const transformedCode = ts.createPrinter().printFile(transformedSourceFile);
+
+    expect(transformedCode).toContain(
+      `...(typeof ngServerMode !== "undefined" && ngServerMode ? { ɵentryName: "src/home.ts" } : {})`,
+    );
+  });
+
+  it('should add ɵentryName property to object with loadComponent and path (Function expression)', () => {
+    const source = `
+      const routes = [
+        {
+          path: 'home',
+          loadComponent: function () { return import('./home').then(m => m.HomeComponent) }
+        }
+      ];
+    `;
+
+    const transformedSourceFile = transformSourceFile(source);
+    const transformedCode = ts.createPrinter().printFile(transformedSourceFile);
+
+    expect(transformedCode).toContain(
+      `...(typeof ngServerMode !== "undefined" && ngServerMode ? { ɵentryName: "src/home.ts" } : {})`,
+    );
+  });
+
+  it('should not modify unrelated object literals', () => {
+    const source = `
+      const routes = [
+        {
+          path: 'home',
+          component: HomeComponent
+        }
+      ];
+    `;
+
+    const transformedSourceFile = transformSourceFile(source);
+    const transformedCode = ts.createPrinter().printFile(transformedSourceFile);
+
+    expect(transformedCode).not.toContain(`ɵentryName`);
+  });
+
+  it('should ignore loadComponent without a valid import call', () => {
+    const source = `
+      const routes = [
+        {
+          path: 'home',
+          loadComponent: () => someFunction()
+        }
+      ];
+    `;
+
+    const transformedSourceFile = transformSourceFile(source);
+    const transformedCode = ts.createPrinter().printFile(transformedSourceFile);
+
+    expect(transformedCode).not.toContain(`ɵentryName`);
+  });
+
+  it('should resolve paths relative to basePath', () => {
+    const source = `
+      const routes = [
+        {
+          path: 'about',
+          loadChildren: () => import('./features/about').then(m => m.AboutModule)
+        }
+      ];
+    `;
+
+    const transformedSourceFile = transformSourceFile(source);
+    const transformedCode = ts.createPrinter().printFile(transformedSourceFile);
+
+    expect(transformedCode).toContain(
+      `...(typeof ngServerMode !== "undefined" && ngServerMode ? { ɵentryName: "src/features/about.ts" } : {})`,
+    );
+  });
+});
diff --git a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts
index 0e3e3fa8e0fb..740c2d119b5a 100644
--- a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts
+++ b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts
@@ -40,6 +40,7 @@ export interface CompilerPluginOptions {
   sourcemap: boolean | 'external';
   tsconfig: string;
   jit?: boolean;
+  browserOnlyBuild?: boolean;
 
   /** Skip TypeScript compilation setup. This is useful to re-use the TypeScript compilation from another plugin. */
   noopTypeScriptCompilation?: boolean;
@@ -119,7 +120,7 @@ export function createCompilerPlugin(
       // Create new reusable compilation for the appropriate mode based on the `jit` plugin option
       const compilation: AngularCompilation = pluginOptions.noopTypeScriptCompilation
         ? new NoopCompilation()
-        : await createAngularCompilation(!!pluginOptions.jit);
+        : await createAngularCompilation(!!pluginOptions.jit, !!pluginOptions.browserOnlyBuild);
       // Compilation is initially assumed to have errors until emitted
       let hasCompilationErrors = true;
 
diff --git a/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts b/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts
index 355bbad228ff..93a92fa7e9a1 100644
--- a/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts
+++ b/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts
@@ -31,6 +31,7 @@ export function createCompilerPluginOptions(
   const incremental = !!options.watch;
 
   return {
+    browserOnlyBuild: !options.serverEntryPoint,
     sourcemap: !!sourcemapOptions.scripts && (sourcemapOptions.hidden ? 'external' : true),
     thirdPartySourcemaps: sourcemapOptions.vendor,
     tsconfig,
diff --git a/packages/angular/build/src/utils/server-rendering/manifest.ts b/packages/angular/build/src/utils/server-rendering/manifest.ts
index ca2886f11d89..bdd7fdd28df9 100644
--- a/packages/angular/build/src/utils/server-rendering/manifest.ts
+++ b/packages/angular/build/src/utils/server-rendering/manifest.ts
@@ -6,14 +6,21 @@
  * found in the LICENSE file at https://angular.dev/license
  */
 
+import type { Metafile } from 'esbuild';
 import { extname } from 'node:path';
 import { NormalizedApplicationBuildOptions } from '../../builders/application/options';
 import { type BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context';
 import { createOutputFile } from '../../tools/esbuild/utils';
+import { shouldOptimizeChunks } from '../environment-options';
 
 export const SERVER_APP_MANIFEST_FILENAME = 'angular-app-manifest.mjs';
 export const SERVER_APP_ENGINE_MANIFEST_FILENAME = 'angular-app-engine-manifest.mjs';
 
+interface FilesMapping {
+  path: string;
+  dynamicImport: boolean;
+}
+
 const MAIN_SERVER_OUTPUT_FILENAME = 'main.server.mjs';
 
 /**
@@ -97,6 +104,9 @@ export default {
  * the application, helping with localization and rendering content specific to the locale.
  * @param baseHref - The base HREF for the application. This is used to set the base URL
  * for all relative URLs in the application.
+ * @param initialFiles - A list of initial files that preload tags have already been added for.
+ * @param metafile - An esbuild metafile object.
+ * @param publicPath - The configured public path.
  *
  * @returns An object containing:
  * - `manifestContent`: A string of the SSR manifest content.
@@ -109,6 +119,9 @@ export function generateAngularServerAppManifest(
   routes: readonly unknown[] | undefined,
   locale: string | undefined,
   baseHref: string,
+  initialFiles: Set<string>,
+  metafile: Metafile,
+  publicPath: string | undefined,
 ): {
   manifestContent: string;
   serverAssetsChunks: BuildOutputFile[];
@@ -132,6 +145,13 @@ export function generateAngularServerAppManifest(
     }
   }
 
+  // When routes have been extracted, mappings are no longer needed, as preloads will be included in the metadata.
+  // When shouldOptimizeChunks is enabled the metadata is no longer correct and thus we cannot generate the mappings.
+  const entryPointToBrowserMapping =
+    routes?.length || shouldOptimizeChunks
+      ? undefined
+      : generateLazyLoadedFilesMappings(metafile, initialFiles, publicPath);
+
   const manifestContent = `
 export default {
   bootstrap: () => import('./main.server.mjs').then(m => m.default),
@@ -139,6 +159,7 @@ export default {
   baseHref: '${baseHref}',
   locale: ${JSON.stringify(locale)},
   routes: ${JSON.stringify(routes, undefined, 2)},
+  entryPointToBrowserMapping: ${JSON.stringify(entryPointToBrowserMapping, undefined, 2)},
   assets: {
     ${Object.entries(serverAssets)
       .map(([key, value]) => `'${key}': ${value}`)
@@ -149,3 +170,50 @@ export default {
 
   return { manifestContent, serverAssetsChunks };
 }
+
+/**
+ * Maps entry points to their corresponding browser bundles for lazy loading.
+ *
+ * This function processes a metafile's outputs to generate a mapping between browser-side entry points
+ * and the associated JavaScript files that should be loaded in the browser. It includes the entry-point's
+ * own path and any valid imports while excluding initial files or external resources.
+ */
+function generateLazyLoadedFilesMappings(
+  metafile: Metafile,
+  initialFiles: Set<string>,
+  publicPath = '',
+): Record<string, FilesMapping[]> {
+  const entryPointToBundles: Record<string, FilesMapping[]> = {};
+  for (const [fileName, { entryPoint, exports, imports }] of Object.entries(metafile.outputs)) {
+    // Skip files that don't have an entryPoint, no exports, or are not .js
+    if (!entryPoint || exports?.length < 1 || !fileName.endsWith('.js')) {
+      continue;
+    }
+
+    const importedPaths: FilesMapping[] = [
+      {
+        path: `${publicPath}${fileName}`,
+        dynamicImport: false,
+      },
+    ];
+
+    for (const { kind, external, path } of imports) {
+      if (
+        external ||
+        initialFiles.has(path) ||
+        (kind !== 'dynamic-import' && kind !== 'import-statement')
+      ) {
+        continue;
+      }
+
+      importedPaths.push({
+        path: `${publicPath}${path}`,
+        dynamicImport: kind === 'dynamic-import',
+      });
+    }
+
+    entryPointToBundles[entryPoint] = importedPaths;
+  }
+
+  return entryPointToBundles;
+}
diff --git a/packages/angular/ssr/src/app.ts b/packages/angular/ssr/src/app.ts
index 08d5ca13a180..85f1ca9818ad 100644
--- a/packages/angular/ssr/src/app.ts
+++ b/packages/angular/ssr/src/app.ts
@@ -251,7 +251,7 @@ export class AngularServerApp {
     matchedRoute: RouteTreeNodeMetadata,
     requestContext?: unknown,
   ): Promise<Response | null> {
-    const { renderMode, headers, status } = matchedRoute;
+    const { renderMode, headers, status, preload } = matchedRoute;
 
     if (!this.allowStaticRouteRender && renderMode === RenderMode.Prerender) {
       return null;
@@ -293,8 +293,8 @@ export class AngularServerApp {
       );
     } else if (renderMode === RenderMode.Client) {
       // Serve the client-side rendered version if the route is configured for CSR.
-      let html = await assets.getServerAsset('index.csr.html').text();
-      html = await this.runTransformsOnHtml(html, url);
+      let html = await this.assets.getServerAsset('index.csr.html').text();
+      html = await this.runTransformsOnHtml(html, url, preload);
 
       return new Response(html, responseInit);
     }
@@ -308,7 +308,7 @@ export class AngularServerApp {
 
     this.boostrap ??= await bootstrap();
     let html = await assets.getIndexServerHtml().text();
-    html = await this.runTransformsOnHtml(html, url);
+    html = await this.runTransformsOnHtml(html, url, preload);
     html = await renderAngular(
       html,
       this.boostrap,
@@ -385,13 +385,22 @@ export class AngularServerApp {
    *
    * @param html - The raw HTML content to be transformed.
    * @param url - The URL associated with the HTML content, used for context during transformations.
+   * @param preload - An array of URLs representing the JavaScript resources to preload.
    * @returns A promise that resolves to the transformed HTML string.
    */
-  private async runTransformsOnHtml(html: string, url: URL): Promise<string> {
+  private async runTransformsOnHtml(
+    html: string,
+    url: URL,
+    preload: readonly string[] | undefined,
+  ): Promise<string> {
     if (this.hooks.has('html:transform:pre')) {
       html = await this.hooks.run('html:transform:pre', { html, url });
     }
 
+    if (preload?.length) {
+      html = appendPreloadHintsToHtml(html, preload);
+    }
+
     return html;
   }
 }
@@ -430,3 +439,30 @@ export function destroyAngularServerApp(): void {
 
   angularServerApp = undefined;
 }
+
+/**
+ * Appends module preload hints to an HTML string for specified JavaScript resources.
+ * This function enhances the HTML by injecting `<link rel="modulepreload">` elements
+ * for each provided resource, allowing browsers to preload the specified JavaScript
+ * modules for better performance.
+ *
+ * @param html - The original HTML string to which preload hints will be added.
+ * @param preload - An array of URLs representing the JavaScript resources to preload.
+ * @returns The modified HTML string with the preload hints injected before the closing `</body>` tag.
+ *          If `</body>` is not found, the links are not added.
+ */
+function appendPreloadHintsToHtml(html: string, preload: readonly string[]): string {
+  const bodyCloseIdx = html.lastIndexOf('</body>');
+  if (bodyCloseIdx === -1) {
+    return html;
+  }
+
+  // Note: Module preloads should be placed at the end before the closing body tag to avoid a performance penalty.
+  // Placing them earlier can cause the browser to prioritize downloading these modules
+  // over other critical page resources like images, CSS, and fonts.
+  return [
+    html.slice(0, bodyCloseIdx),
+    ...preload.map((val) => `<link rel="modulepreload" href="${val}">`),
+    html.slice(bodyCloseIdx),
+  ].join('\n');
+}
diff --git a/packages/angular/ssr/src/manifest.ts b/packages/angular/ssr/src/manifest.ts
index 2c0d642ec2ae..f37ca613314b 100644
--- a/packages/angular/ssr/src/manifest.ts
+++ b/packages/angular/ssr/src/manifest.ts
@@ -110,6 +110,26 @@ export interface AngularAppManifest {
    * the application, aiding with localization and rendering content specific to the locale.
    */
   readonly locale?: string;
+
+  /**
+   * Maps entry-point names to their corresponding browser bundles and loading strategies.
+   *
+   * - **Key**: The entry-point name, typically the value of `ɵentryName`.
+   * - **Value**: An array of objects, each representing a browser bundle with:
+   *   - `path`: The filename or URL of the associated JavaScript bundle to preload.
+   *   - `dynamicImport`: A boolean indicating whether the bundle is loaded via a dynamic `import()`.
+   *     If `true`, the bundle is lazily loaded, impacting its preloading behavior.
+   *
+   * ### Example
+   * ```ts
+   * {
+   *   'src/app/lazy/lazy.ts': [{ path: 'src/app/lazy/lazy.js', dynamicImport: true }]
+   * }
+   * ```
+   */
+  readonly entryPointToBrowserMapping?: Readonly<
+    Record<string, ReadonlyArray<{ path: string; dynamicImport: boolean }> | undefined>
+  >;
 }
 
 /**
diff --git a/packages/angular/ssr/src/routes/ng-routes.ts b/packages/angular/ssr/src/routes/ng-routes.ts
index 1f4f7c7d5613..be91636ccd25 100644
--- a/packages/angular/ssr/src/routes/ng-routes.ts
+++ b/packages/angular/ssr/src/routes/ng-routes.ts
@@ -9,7 +9,11 @@
 import { APP_BASE_HREF, PlatformLocation } from '@angular/common';
 import { ApplicationRef, Compiler, Injector, runInInjectionContext, ɵConsole } from '@angular/core';
 import { INITIAL_CONFIG, platformServer } from '@angular/platform-server';
-import { Route, Router, ɵloadChildren as loadChildrenHelper } from '@angular/router';
+import {
+  Route as AngularRoute,
+  Router,
+  ɵloadChildren as loadChildrenHelper,
+} from '@angular/router';
 import { ServerAssets } from '../assets';
 import { Console } from '../console';
 import { AngularAppManifest, getAngularAppManifest } from '../manifest';
@@ -25,6 +29,16 @@ import {
 } from './route-config';
 import { RouteTree, RouteTreeNodeMetadata } from './route-tree';
 
+interface Route extends AngularRoute {
+  ɵentryName?: string;
+}
+
+/**
+ * The maximum number of module preload link elements that should be added for
+ * initial scripts.
+ */
+const MODULE_PRELOAD_MAX = 10;
+
 /**
  * Regular expression to match segments preceded by a colon in a string.
  */
@@ -87,6 +101,8 @@ interface AngularRouterConfigResult {
   appShellRoute?: string;
 }
 
+type EntryPointToBrowserMapping = AngularAppManifest['entryPointToBrowserMapping'];
+
 /**
  * Traverses an array of route configurations to generate route tree node metadata.
  *
@@ -104,6 +120,8 @@ async function* traverseRoutesConfig(options: {
   serverConfigRouteTree: RouteTree<ServerConfigRouteTreeAdditionalMetadata> | undefined;
   invokeGetPrerenderParams: boolean;
   includePrerenderFallbackRoutes: boolean;
+  entryPointToBrowserMapping: EntryPointToBrowserMapping | undefined;
+  parentPreloads?: readonly string[];
 }): AsyncIterableIterator<RouteTreeNodeMetadata | { error: string }> {
   const {
     routes,
@@ -111,13 +129,15 @@ async function* traverseRoutesConfig(options: {
     parentInjector,
     parentRoute,
     serverConfigRouteTree,
+    entryPointToBrowserMapping,
+    parentPreloads,
     invokeGetPrerenderParams,
     includePrerenderFallbackRoutes,
   } = options;
 
   for (const route of routes) {
     try {
-      const { path = '', redirectTo, loadChildren, children } = route;
+      const { path = '', redirectTo, loadChildren, loadComponent, children, ɵentryName } = route;
       const currentRoutePath = joinUrlParts(parentRoute, path);
 
       // Get route metadata from the server config route tree, if available
@@ -140,13 +160,17 @@ async function* traverseRoutesConfig(options: {
       const metadata: ServerConfigRouteTreeNodeMetadata = {
         renderMode: RenderMode.Prerender,
         ...matchedMetaData,
+        preload: parentPreloads,
         // Match Angular router behavior
         // ['one', 'two', ''] -> 'one/two/'
         // ['one', 'two', 'three'] -> 'one/two/three'
         route: path === '' ? addTrailingSlash(currentRoutePath) : currentRoutePath,
+        presentInClientRouter: undefined,
       };
 
-      delete metadata.presentInClientRouter;
+      if (ɵentryName && loadComponent) {
+        appendPreloadToMetadata(ɵentryName, entryPointToBrowserMapping, metadata, true);
+      }
 
       if (metadata.renderMode === RenderMode.Prerender) {
         // Handle SSG routes
@@ -180,11 +204,20 @@ async function* traverseRoutesConfig(options: {
           ...options,
           routes: children,
           parentRoute: currentRoutePath,
+          parentPreloads: metadata.preload,
         });
       }
 
       // Load and process lazy-loaded child routes
       if (loadChildren) {
+        if (ɵentryName) {
+          // When using `loadChildren`, the entire feature area (including multiple routes) is loaded.
+          // As a result, we do not want all dynamic-import dependencies to be preload, because it involves multiple dependencies
+          // across different child routes. In contrast, `loadComponent` only loads a single component, which allows
+          // for precise control over preloading, ensuring that the files preloaded are exactly those required for that specific route.
+          appendPreloadToMetadata(ɵentryName, entryPointToBrowserMapping, metadata, false);
+        }
+
         const loadedChildRoutes = await loadChildrenHelper(
           route,
           compiler,
@@ -198,6 +231,7 @@ async function* traverseRoutesConfig(options: {
             routes: childRoutes,
             parentInjector: injector,
             parentRoute: currentRoutePath,
+            parentPreloads: metadata.preload,
           });
         }
       }
@@ -209,6 +243,36 @@ async function* traverseRoutesConfig(options: {
   }
 }
 
+/**
+ * Appends preload information to the metadata object based on the specified entry-point and chunk mappings.
+ *
+ * This function extracts preload data for a given entry-point from the provided chunk mappings. It adds the
+ * corresponding browser bundles to the metadata's preload list, ensuring no duplicates and limiting the total
+ * preloads to a predefined maximum.
+ */
+function appendPreloadToMetadata(
+  entryName: string,
+  entryPointToBrowserMapping: EntryPointToBrowserMapping,
+  metadata: ServerConfigRouteTreeNodeMetadata,
+  includeDynamicImports: boolean,
+): void {
+  if (!entryPointToBrowserMapping) {
+    return;
+  }
+
+  const preload = entryPointToBrowserMapping[entryName];
+
+  if (preload?.length) {
+    // Merge existing preloads with new ones, ensuring uniqueness and limiting the total to the maximum allowed.
+    const preloadPaths =
+      preload
+        .filter(({ dynamicImport }) => includeDynamicImports || !dynamicImport)
+        .map(({ path }) => path) ?? [];
+    const combinedPreloads = [...(metadata.preload ?? []), ...preloadPaths];
+    metadata.preload = Array.from(new Set(combinedPreloads)).slice(0, MODULE_PRELOAD_MAX);
+  }
+}
+
 /**
  * Handles SSG (Static Site Generation) routes by invoking `getPrerenderParams` and yielding
  * all parameterized paths, returning any errors encountered.
@@ -391,6 +455,7 @@ function buildServerConfigRouteTree({ routes, appShellRoute }: ServerRoutesConfi
  * @param invokeGetPrerenderParams - A boolean flag indicating whether to invoke `getPrerenderParams` for parameterized SSG routes
  * to handle prerendering paths. Defaults to `false`.
  * @param includePrerenderFallbackRoutes - A flag indicating whether to include fallback routes in the result. Defaults to `true`.
+ * @param entryPointToBrowserMapping - Maps the entry-point name to the associated JavaScript browser bundles.
  *
  * @returns A promise that resolves to an object of type `AngularRouterConfigResult` or errors.
  */
@@ -400,6 +465,7 @@ export async function getRoutesFromAngularRouterConfig(
   url: URL,
   invokeGetPrerenderParams = false,
   includePrerenderFallbackRoutes = true,
+  entryPointToBrowserMapping: EntryPointToBrowserMapping | undefined = undefined,
 ): Promise<AngularRouterConfigResult> {
   const { protocol, host } = url;
 
@@ -469,6 +535,7 @@ export async function getRoutesFromAngularRouterConfig(
         serverConfigRouteTree,
         invokeGetPrerenderParams,
         includePrerenderFallbackRoutes,
+        entryPointToBrowserMapping,
       });
 
       for await (const result of traverseRoutes) {
@@ -569,6 +636,7 @@ export function extractRoutesAndCreateRouteTree(options: {
       url,
       invokeGetPrerenderParams,
       includePrerenderFallbackRoutes,
+      manifest.entryPointToBrowserMapping,
     );
 
     for (const { route, ...metadata } of routes) {
diff --git a/packages/angular/ssr/src/routes/route-tree.ts b/packages/angular/ssr/src/routes/route-tree.ts
index 85b1279aaee5..ba79688aa3c6 100644
--- a/packages/angular/ssr/src/routes/route-tree.ts
+++ b/packages/angular/ssr/src/routes/route-tree.ts
@@ -65,6 +65,11 @@ export interface RouteTreeNodeMetadata {
    * Specifies the rendering mode used for this route.
    */
   renderMode: RenderMode;
+
+  /**
+   * A list of resource that should be preloaded by the browser.
+   */
+  preload?: readonly string[];
 }
 
 /**
diff --git a/packages/angular/ssr/test/testing-utils.ts b/packages/angular/ssr/test/testing-utils.ts
index dd18ea29b516..f872487b06d8 100644
--- a/packages/angular/ssr/test/testing-utils.ts
+++ b/packages/angular/ssr/test/testing-utils.ts
@@ -22,7 +22,7 @@ import { ServerRoute, provideServerRoutesConfig } from '../src/routes/route-conf
  *
  * @param routes - An array of route definitions to be used by the Angular Router.
  * @param serverRoutes - An array of server route definitions for server-side rendering.
- * @param [baseHref='/'] - An optional base href for the HTML template (default is `/`).
+ * @param baseHref - An optional base href to be used in the HTML template.
  * @param additionalServerAssets - A record of additional server assets to include,
  *                                  where the keys are asset paths and the values are asset details.
  * @param locale - An optional locale to configure for the application during testing.
diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-preload-links.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-preload-links.ts
new file mode 100644
index 000000000000..77670e5eb64d
--- /dev/null
+++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-preload-links.ts
@@ -0,0 +1,223 @@
+import assert from 'node:assert';
+import { replaceInFile, writeMultipleFiles } from '../../../utils/fs';
+import { execAndWaitForOutputToMatch, ng, noSilentNg, silentNg } from '../../../utils/process';
+import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages';
+import { ngServe, updateJsonFile, useSha } from '../../../utils/project';
+import { getGlobalVariable } from '../../../utils/env';
+import { findFreePort } from '../../../utils/network';
+
+export default async function () {
+  assert(
+    getGlobalVariable('argv')['esbuild'],
+    'This test should not be called in the Webpack suite.',
+  );
+
+  // Forcibly remove in case another test doesn't clean itself up.
+  await uninstallPackage('@angular/ssr');
+  await ng('add', '@angular/ssr', '--server-routing', '--skip-confirmation', '--skip-install');
+  await useSha();
+  await installWorkspacePackages();
+
+  await updateJsonFile('angular.json', (workspaceJson) => {
+    const appProject = workspaceJson.projects['test-project'];
+    appProject.architect['build'].options.namedChunks = true;
+  });
+
+  // Add routes
+  await writeMultipleFiles({
+    'src/app/app.routes.ts': `
+  import { Routes } from '@angular/router';
+
+  export const routes: Routes = [
+    {
+      path: '',
+      loadComponent: () => import('./home/home.component').then(c => c.HomeComponent),
+    },
+    {
+      path: 'ssg',
+      loadChildren: () => import('./ssg.routes').then(m => m.routes),
+    },
+    {
+      path: 'ssr',
+      loadComponent: () => import('./ssr/ssr.component').then(c => c.SsrComponent),
+    },
+    {
+      path: 'csr',
+      loadComponent: () => import('./csr/csr.component').then(c => c.CsrComponent),
+    },
+  ];
+  `,
+    'src/app/app.routes.server.ts': `
+  import { RenderMode, ServerRoute } from '@angular/ssr';
+
+  export const serverRoutes: ServerRoute[] = [
+    {
+      path: 'ssr',
+      renderMode: RenderMode.Server,
+    },
+    {
+      path: 'csr',
+      renderMode: RenderMode.Client,
+    },
+    {
+      path: '**',
+      renderMode: RenderMode.Prerender,
+    },
+  ];
+  `,
+    'src/app/cross-dep.ts': `export const foo = 'foo';`,
+    'src/app/ssg.routes.ts': `
+  import { Routes } from '@angular/router';
+
+  export const routes: Routes = [
+    {
+      path: '',
+      loadComponent: () => import('./ssg/ssg.component').then(c => c.SsgComponent),
+    },
+    {
+      path: 'one',
+      loadComponent: () => import('./ssg-one/ssg-one.component').then(c => c.SsgOneComponent),
+    },
+    {
+      path: 'two',
+      loadComponent: () => import('./ssg-two/ssg-two.component').then(c => c.SsgTwoComponent),
+    },
+  ];`,
+  });
+
+  // Generate components for the above routes
+  const componentNames: string[] = ['home', 'ssg', 'csr', 'ssr', 'ssg-one', 'ssg-two'];
+
+  for (const componentName of componentNames) {
+    await silentNg('generate', 'component', componentName);
+  }
+
+  // Add a cross-dependency
+  await Promise.all([
+    replaceInFile(
+      'src/app/ssg-one/ssg-one.component.ts',
+      `OneComponent {`,
+      `OneComponent {
+          async ngOnInit() {
+            await import('../cross-dep');
+          }
+      `,
+    ),
+    replaceInFile(
+      'src/app/ssg-two/ssg-two.component.ts',
+      `TwoComponent {`,
+      `TwoComponent {
+          async ngOnInit() {
+            await import('../cross-dep');
+          }
+      `,
+    ),
+  ]);
+
+  // Test both vite and `ng build`
+  await runTests(await ngServe());
+
+  await noSilentNg('build', '--output-mode=server');
+  await runTests(await spawnServer());
+}
+
+const RESPONSE_EXPECTS: Record<
+  string,
+  {
+    matches: RegExp[];
+    notMatches: RegExp[];
+  }
+> = {
+  '/': {
+    matches: [/<link rel="modulepreload" href="(home\.component-[a-zA-Z0-9]{8}\.js)">/],
+    notMatches: [/ssg\.component/, /ssr\.component/, /csr\.component/, /cross-dep-/],
+  },
+  '/ssg': {
+    matches: [
+      /<link rel="modulepreload" href="(ssg\.routes-[a-zA-Z0-9]{8}\.js)">/,
+      /<link rel="modulepreload" href="(ssg\.component-[a-zA-Z0-9]{8}\.js)">/,
+    ],
+    notMatches: [
+      /home\.component/,
+      /ssr\.component/,
+      /csr\.component/,
+      /ssg-one\.component/,
+      /ssg-two\.component/,
+      /cross-dep-/,
+    ],
+  },
+  '/ssg/one': {
+    matches: [
+      /<link rel="modulepreload" href="(ssg\.routes-[a-zA-Z0-9]{8}\.js)">/,
+      /<link rel="modulepreload" href="(ssg-one\.component-[a-zA-Z0-9]{8}\.js)">/,
+      /<link rel="modulepreload" href="(cross-dep-[a-zA-Z0-9]{8}\.js)">/,
+    ],
+    notMatches: [
+      /home\.component/,
+      /ssr\.component/,
+      /csr\.component/,
+      /ssg-two\.component/,
+      /ssg\.component/,
+    ],
+  },
+  '/ssg/two': {
+    matches: [
+      /<link rel="modulepreload" href="(ssg\.routes-[a-zA-Z0-9]{8}\.js)">/,
+      /<link rel="modulepreload" href="(ssg-two\.component-[a-zA-Z0-9]{8}\.js)">/,
+      /<link rel="modulepreload" href="(cross-dep-[a-zA-Z0-9]{8}\.js)">/,
+    ],
+    notMatches: [
+      /home\.component/,
+      /ssr\.component/,
+      /csr\.component/,
+      /ssg-one\.component/,
+      /ssg\.component/,
+    ],
+  },
+  '/ssr': {
+    matches: [/<link rel="modulepreload" href="(ssr\.component-[a-zA-Z0-9]{8}\.js)">/],
+    notMatches: [/home\.component/, /ssg\.component/, /csr\.component/],
+  },
+  '/csr': {
+    matches: [/<link rel="modulepreload" href="(csr\.component-[a-zA-Z0-9]{8}\.js)">/],
+    notMatches: [/home\.component/, /ssg\.component/, /ssr\.component/, /cross-dep-/],
+  },
+};
+
+async function runTests(port: number): Promise<void> {
+  for (const [pathname, { matches, notMatches }] of Object.entries(RESPONSE_EXPECTS)) {
+    const res = await fetch(`http://localhost:${port}${pathname}`);
+    const text = await res.text();
+
+    for (const match of matches) {
+      assert.match(text, match, `Response for '${pathname}': ${match} was not matched in content.`);
+
+      // Ensure that the url is correct and it's a 200.
+      const link = text.match(match)?.[1];
+      const preloadRes = await fetch(`http://localhost:${port}/${link}`);
+      assert.equal(preloadRes.status, 200);
+    }
+
+    for (const match of notMatches) {
+      assert.doesNotMatch(
+        text,
+        match,
+        `Response for '${pathname}': ${match} was matched in content.`,
+      );
+    }
+  }
+}
+
+async function spawnServer(): Promise<number> {
+  const port = await findFreePort();
+  await execAndWaitForOutputToMatch(
+    'npm',
+    ['run', 'serve:ssr:test-project'],
+    /Node Express server listening on/,
+    {
+      'PORT': String(port),
+    },
+  );
+
+  return port;
+}