diff --git a/.changeset/angry-students-pay.md b/.changeset/angry-students-pay.md
new file mode 100644
index 0000000000..82db27596d
--- /dev/null
+++ b/.changeset/angry-students-pay.md
@@ -0,0 +1,13 @@
+---
+"@react-router/dev": minor
+"react-router": minor
+---
+
+Added a new `react-router.config.ts` `routeDiscovery` option to configure Lazy Route Discovery behavior.
+
+- By default, Lazy Route Discovery is enabled and makes manifest requests to the `/__manifest` path:
+  - `routeDiscovery: { mode: "lazy", manifestPath: "/__manifest" }`
+- You can modify the manifest path used:
+  - `routeDiscovery: { mode: "lazy", manifestPath: "/custom-manifest" }`
+- Or you can disable this feature entirely and include all routes in the manifest on initial document load:
+  - `routeDiscovery: { mode: "initial" }`
diff --git a/integration/fog-of-war-test.ts b/integration/fog-of-war-test.ts
index 9dda94a4c1..e5e9bc8fe1 100644
--- a/integration/fog-of-war-test.ts
+++ b/integration/fog-of-war-test.ts
@@ -1,4 +1,5 @@
 import { test, expect } from "@playwright/test";
+import { PassThrough } from "node:stream";
 
 import {
   createAppFixture,
@@ -6,6 +7,7 @@ import {
   js,
 } from "./helpers/create-fixture.js";
 import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+import { reactRouterConfig } from "./helpers/vite.js";
 
 function getFiles() {
   return {
@@ -118,6 +120,10 @@ test.describe("Fog of War", () => {
     let res = await fixture.requestDocument("/");
     let html = await res.text();
 
+    expect(html).toContain("window.__reactRouterManifest = {");
+    expect(html).not.toContain(
+      ' {
     await app.clickLink("/a");
     await page.waitForSelector("#a-index");
   });
+
+  test("allows configuration of the manifest path", async ({ page }) => {
+    let fixture = await createFixture({
+      files: {
+        ...getFiles(),
+        "react-router.config.ts": reactRouterConfig({
+          routeDiscovery: { mode: "lazy", manifestPath: "/custom-manifest" },
+        }),
+      },
+    });
+    let appFixture = await createAppFixture(fixture);
+    let app = new PlaywrightFixture(appFixture, page);
+
+    let wrongManifestRequests: string[] = [];
+    let manifestRequests: string[] = [];
+    page.on("request", (req) => {
+      if (req.url().includes("/__manifest")) {
+        wrongManifestRequests.push(req.url());
+      }
+      if (req.url().includes("/custom-manifest")) {
+        manifestRequests.push(req.url());
+      }
+    });
+
+    await app.goto("/", true);
+    expect(
+      await page.evaluate(() =>
+        Object.keys((window as any).__reactRouterManifest.routes)
+      )
+    ).toEqual(["root", "routes/_index", "routes/a"]);
+    expect(manifestRequests).toEqual([
+      expect.stringMatching(/\/custom-manifest\?p=%2F&p=%2Fa&version=/),
+    ]);
+    manifestRequests = [];
+
+    await app.clickLink("/a");
+    await page.waitForSelector("#a");
+    expect(await app.getHtml("#a")).toBe(`
A: A LOADER
`);
+    // Wait for eager discovery to kick off
+    await new Promise((r) => setTimeout(r, 500));
+    expect(manifestRequests).toEqual([
+      expect.stringMatching(/\/custom-manifest\?p=%2Fa%2Fb&version=/),
+    ]);
+
+    expect(wrongManifestRequests).toEqual([]);
+  });
+
+  test.describe("routeDiscovery=initial", () => {
+    test("loads full manifest on initial load", async ({ page }) => {
+      let fixture = await createFixture({
+        files: {
+          ...getFiles(),
+          "react-router.config.ts": reactRouterConfig({
+            routeDiscovery: { mode: "initial" },
+          }),
+          "app/entry.client.tsx": js`
+            import { HydratedRouter } from "react-router/dom";
+            import { startTransition, StrictMode } from "react";
+            import { hydrateRoot } from "react-dom/client";
+            startTransition(() => {
+              hydrateRoot(
+                document,
+                
+                  
+                
+              );
+            });
+          `,
+        },
+      });
+      let appFixture = await createAppFixture(fixture);
+
+      let manifestRequests: string[] = [];
+      page.on("request", (req) => {
+        if (req.url().includes("/__manifest")) {
+          manifestRequests.push(req.url());
+        }
+      });
+
+      let app = new PlaywrightFixture(appFixture, page);
+      let res = await fixture.requestDocument("/");
+      let html = await res.text();
+
+      expect(html).not.toContain("window.__reactRouterManifest = {");
+      expect(html).toContain(
+        '
+          Object.keys((window as any).__reactRouterManifest.routes)
+        )
+      ).toEqual([
+        "root",
+        "routes/_index",
+        "routes/a",
+        "routes/a.b",
+        "routes/a.b.c",
+      ]);
+
+      await app.clickLink("/a");
+      await page.waitForSelector("#a");
+      expect(await app.getHtml("#a")).toBe(`A: A LOADER
`);
+      expect(manifestRequests).toEqual([]);
+    });
+
+    test("defaults to `routeDiscovery=initial` when `ssr:false` is set", async ({
+      page,
+    }) => {
+      let fixture = await createFixture({
+        spaMode: true,
+        files: {
+          "react-router.config.ts": reactRouterConfig({
+            ssr: false,
+          }),
+          "app/root.tsx": js`
+            import * as React from "react";
+            import { Link, Links, Meta, Outlet, Scripts } from "react-router";
+            export default function Root() {
+              let [showLink, setShowLink] = React.useState(false);
+              return (
+                
+                  
+                    
+                    
+                  
+                  
+                    Home
+                    /a
+                    
+                    
+                  
+                
+              );
+            }
+          `,
+          "app/routes/_index.tsx": js`
+            export default function Index() {
+              return Index
+            }
+          `,
+
+          "app/routes/a.tsx": js`
+            export function clientLoader({ request }) {
+              return { message: "A LOADER" };
+            }
+            export default function Index({ loaderData }) {
+              return A: {loaderData.message}
+            }
+          `,
+        },
+      });
+      let appFixture = await createAppFixture(fixture);
+
+      let manifestRequests: string[] = [];
+      page.on("request", (req) => {
+        if (req.url().includes("/__manifest")) {
+          manifestRequests.push(req.url());
+        }
+      });
+
+      let app = new PlaywrightFixture(appFixture, page);
+      let res = await fixture.requestDocument("/");
+      let html = await res.text();
+
+      expect(html).toContain('"routeDiscovery":{"mode":"initial"}');
+
+      await app.goto("/", true);
+      await page.waitForSelector("#index");
+      await app.clickLink("/a");
+      await page.waitForSelector("#a");
+      expect(await app.getHtml("#a")).toBe(`A: A LOADER
`);
+      expect(manifestRequests).toEqual([]);
+    });
+
+    test("Errors if you try to set routeDiscovery=lazy and ssr:false", async () => {
+      let ogConsole = console.error;
+      console.error = () => {};
+      let buildStdio = new PassThrough();
+      let err;
+      try {
+        await createFixture({
+          buildStdio,
+          spaMode: true,
+          files: {
+            ...getFiles(),
+            "react-router.config.ts": reactRouterConfig({
+              ssr: false,
+              routeDiscovery: { mode: "lazy" },
+            }),
+          },
+        });
+      } catch (e) {
+        err = e;
+      }
+
+      let chunks: Buffer[] = [];
+      let buildOutput = await new Promise((resolve, reject) => {
+        buildStdio.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
+        buildStdio.on("error", (err) => reject(err));
+        buildStdio.on("end", () =>
+          resolve(Buffer.concat(chunks).toString("utf8"))
+        );
+      });
+
+      expect(err).toEqual(new Error("Build failed, check the output above"));
+      expect(buildOutput).toContain(
+        'Error: The `routeDiscovery.mode` config cannot be set to "lazy" when setting `ssr:false`'
+      );
+      console.error = ogConsole;
+    });
+  });
 });
diff --git a/integration/helpers/vite.ts b/integration/helpers/vite.ts
index 1330c291e1..d590d5cad7 100644
--- a/integration/helpers/vite.ts
+++ b/integration/helpers/vite.ts
@@ -31,6 +31,7 @@ export const reactRouterConfig = ({
   splitRouteModules,
   viteEnvironmentApi,
   middleware,
+  routeDiscovery,
 }: {
   ssr?: boolean;
   basename?: string;
@@ -41,12 +42,14 @@ export const reactRouterConfig = ({
   >["unstable_splitRouteModules"];
   viteEnvironmentApi?: boolean;
   middleware?: boolean;
+  routeDiscovery?: Config["routeDiscovery"];
 }) => {
   let config: Config = {
     ssr,
     basename,
     prerender,
     appDirectory,
+    routeDiscovery,
     future: {
       unstable_splitRouteModules: splitRouteModules,
       unstable_viteEnvironmentApi: viteEnvironmentApi,
diff --git a/integration/vite-presets-test.ts b/integration/vite-presets-test.ts
index 6128229996..bc3b9cfab9 100644
--- a/integration/vite-presets-test.ts
+++ b/integration/vite-presets-test.ts
@@ -29,7 +29,7 @@ const files = {
     export default {
       // Ensure user config takes precedence over preset config
       appDirectory: "app",
-      
+
       presets: [
         // Ensure user config is passed to reactRouterConfig hook
         {
@@ -221,6 +221,7 @@ test.describe("Vite / presets", async () => {
         "future",
         "prerender",
         "routes",
+        "routeDiscovery",
         "serverBuildFile",
         "serverBundles",
         "serverModuleFormat",
diff --git a/packages/react-router-dev/config/config.ts b/packages/react-router-dev/config/config.ts
index 999e19eec3..68e0e810c9 100644
--- a/packages/react-router-dev/config/config.ts
+++ b/packages/react-router-dev/config/config.ts
@@ -158,6 +158,24 @@ export type ReactRouterConfig = {
    * other platforms and tools.
    */
   presets?: Array;
+  /**
+   * Control the "Lazy Route Discovery" behavior
+   *
+   * - `routeDiscovery.mode`: By default, this resolves to `lazy` which will
+   *   lazily discover routes as the user navigates around your application.
+   *   You can set this to `initial` to opt-out of this behavior and load all
+   *   routes with the initial HTML document load.
+   * - `routeDiscovery.manifestPath`: The path to serve the manifest file from.
+   *    Only applies to `mode: "lazy"` and defaults to `/__manifest`.
+   */
+  routeDiscovery?:
+    | {
+        mode: "lazy";
+        manifestPath?: string;
+      }
+    | {
+        mode: "initial";
+      };
   /**
    * The file name of the server build output. This file
    * should end in a `.js` extension and should be deployed to your server.
@@ -205,6 +223,17 @@ export type ResolvedReactRouterConfig = Readonly<{
    * function returning an array to dynamically generate URLs.
    */
   prerender: ReactRouterConfig["prerender"];
+  /**
+   * Control the "Lazy Route Discovery" behavior
+   *
+   * - `routeDiscovery.mode`: By default, this resolves to `lazy` which will
+   *   lazily discover routes as the user navigates around your application.
+   *   You can set this to `initial` to opt-out of this behavior and load all
+   *   routes with the initial HTML document load.
+   * - `routeDiscovery.manifestPath`: The path to serve the manifest file from.
+   *    Only applies to `mode: "lazy"` and defaults to `/__manifest`.
+   */
+  routeDiscovery: ReactRouterConfig["routeDiscovery"];
   /**
    * An object of all available routes, keyed by route id.
    */
@@ -388,19 +417,25 @@ async function resolveConfig({
     ssr: true,
   } as const satisfies Partial;
 
+  let userAndPresetConfigs = mergeReactRouterConfig(
+    ...presets,
+    reactRouterUserConfig
+  );
+
   let {
     appDirectory: userAppDirectory,
     basename,
     buildDirectory: userBuildDirectory,
     buildEnd,
     prerender,
+    routeDiscovery: userRouteDiscovery,
     serverBuildFile,
     serverBundles,
     serverModuleFormat,
     ssr,
   } = {
     ...defaults, // Default values should be completely overridden by user/preset config, not merged
-    ...mergeReactRouterConfig(...presets, reactRouterUserConfig),
+    ...userAndPresetConfigs,
   };
 
   if (!ssr && serverBundles) {
@@ -420,6 +455,36 @@ async function resolveConfig({
     );
   }
 
+  let routeDiscovery: ResolvedReactRouterConfig["routeDiscovery"];
+  if (userRouteDiscovery == null) {
+    if (ssr) {
+      routeDiscovery = {
+        mode: "lazy",
+        manifestPath: "/__manifest",
+      };
+    } else {
+      routeDiscovery = { mode: "initial" };
+    }
+  } else if (userRouteDiscovery.mode === "initial") {
+    routeDiscovery = userRouteDiscovery;
+  } else if (userRouteDiscovery.mode === "lazy") {
+    if (!ssr) {
+      return err(
+        'The `routeDiscovery.mode` config cannot be set to "lazy" when setting `ssr:false`'
+      );
+    }
+
+    let { manifestPath } = userRouteDiscovery;
+    if (manifestPath != null && !manifestPath.startsWith("/")) {
+      return err(
+        "The `routeDiscovery.manifestPath` config must be a root-relative " +
+          'pathname beginning with a slash (i.e., "/__manifest")'
+      );
+    }
+
+    routeDiscovery = userRouteDiscovery;
+  }
+
   let appDirectory = path.resolve(root, userAppDirectory || "app");
   let buildDirectory = path.resolve(root, userBuildDirectory);
 
@@ -512,11 +577,12 @@ async function resolveConfig({
     future,
     prerender,
     routes,
+    routeDiscovery,
     serverBuildFile,
     serverBundles,
     serverModuleFormat,
     ssr,
-  });
+  } satisfies ResolvedReactRouterConfig);
 
   for (let preset of reactRouterUserConfig.presets ?? []) {
     await preset.reactRouterConfigResolved?.({ reactRouterConfig });
diff --git a/packages/react-router-dev/typegen/index.ts b/packages/react-router-dev/typegen/index.ts
index 342f27f808..a046345d31 100644
--- a/packages/react-router-dev/typegen/index.ts
+++ b/packages/react-router-dev/typegen/index.ts
@@ -161,6 +161,7 @@ const virtual = ts`
     export const isSpaMode: ServerBuild["isSpaMode"];
     export const prerender: ServerBuild["prerender"];
     export const publicPath: ServerBuild["publicPath"];
+    export const routeDiscovery: ServerBuild["routeDiscovery"];
     export const routes: ServerBuild["routes"];
     export const ssr: ServerBuild["ssr"];
     export const unstable_getCriticalCss: ServerBuild["unstable_getCriticalCss"];
diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts
index 66c24acfd9..30d026efd5 100644
--- a/packages/react-router-dev/vite/plugin.ts
+++ b/packages/react-router-dev/vite/plugin.ts
@@ -742,6 +742,9 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
       export const ssr = ${ctx.reactRouterConfig.ssr};
       export const isSpaMode = ${isSpaMode};
       export const prerender = ${JSON.stringify(prerenderPaths)};
+      export const routeDiscovery = ${JSON.stringify(
+        ctx.reactRouterConfig.routeDiscovery
+      )};
       export const publicPath = ${JSON.stringify(ctx.publicPath)};
       export const entry = { module: entryServer };
       export const routes = {
diff --git a/packages/react-router/__tests__/dom/scroll-restoration-test.tsx b/packages/react-router/__tests__/dom/scroll-restoration-test.tsx
index 8576f535e1..9686a49b25 100644
--- a/packages/react-router/__tests__/dom/scroll-restoration-test.tsx
+++ b/packages/react-router/__tests__/dom/scroll-restoration-test.tsx
@@ -11,10 +11,10 @@ import {
   ScrollRestoration,
   createBrowserRouter,
 } from "../../index";
-import type { FrameworkContextObject } from "../../lib/dom/ssr/entry";
 import { createMemoryRouter, redirect } from "react-router";
 import { FrameworkContext, Scripts } from "../../lib/dom/ssr/components";
 import "@testing-library/jest-dom/extend-expect";
+import { mockFrameworkContext } from "../utils/framework";
 
 describe(`ScrollRestoration`, () => {
   it("restores the scroll position for a page when re-visited", () => {
@@ -207,23 +207,7 @@ describe(`ScrollRestoration`, () => {
       window.scrollTo = scrollTo;
     });
 
-    let context: FrameworkContextObject = {
-      routeModules: { root: { default: () => null } },
-      manifest: {
-        routes: {
-          root: {
-            hasLoader: false,
-            hasAction: false,
-            hasErrorBoundary: false,
-            id: "root",
-            module: "root.js",
-          },
-        },
-        entry: { imports: [], module: "" },
-        url: "",
-        version: "",
-      },
-    };
+    let context = mockFrameworkContext();
 
     it("should render a