Repository: remix-run/remix
Pull request #6818 (2023-07-12 17:49:26)

chore(dev): warn if not using `v2_dev`
## TODO

- [x] `--manual` + `v2_dev.manual` (undoc/warn for `--no-restart` and `v2_dev.restart`)
- [x] warn if not using `v2_dev`
- [x] changeset

## Follow-on work
- [ ] `remix-serve` compat with CJS `--manual`?
- [ ] update templates
- [ ] update tutorials


changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: ff8b6632ec1588ce6970b78a8b342a9bfeb51e7c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/remix-run/remix/new/pedro/v2-dev?filename=.changeset/chilly-coins-draw.md&value=---%0A%0A---%0A%0Adocs(guides%2Fdev-perf)%3A%20add%20dx%20guide%20for%20perf%0A)


mjackson: I don't think we need to undoc or warn if they use `--no-restart`. It's just an alias for the same thing, and could help some people understand what it does from a different angle. I like the fact that we have 2 flags for it.

Patch:
diff --git a/.changeset/hot-ducks-march.md b/.changeset/hot-ducks-march.md
new file mode 100644
index 000000000..881d758c2
--- /dev/null
+++ b/.changeset/hot-ducks-march.md
@@ -0,0 +1,8 @@
+---
+"@remix-run/dev": patch
+---
+
+Warn if not using v2_dev
+
+Also, rename `--no-restart` to `--manual` to match intention and documentation.
+`--no-restart` remains an alias for `--manual` in v1 for backwards compatibility.
diff --git a/docs/guides/dev-perf.md b/docs/guides/dev-perf.md
new file mode 100644
index 000000000..1957d27e0
--- /dev/null
+++ b/docs/guides/dev-perf.md
@@ -0,0 +1,310 @@
+---
+title: Development Performance
+---
+
+## Bundle analysis
+
+Remix outputs metafiles to the server build directory (`build/` by default) so you can analyze your bundle size and composition.
+
+- `metafile.css.json` : Metafile for the CSS bundle
+- `metafile.js.json` : Metafile for the browser JS bundle
+- `metafile.server.json` : Metafile for the serve JS bundle
+
+Remix uses esbuild's metafile format so you can directly upload those files to https://esbuild.github.io/analyze/ to visualize your bundle.
+
+## Path imports
+
+Currently, when Remix rebuilds your app, the compiler has to process your app code along with any of its dependencies.
+The compiler treeshakes unused code from app so that you don't ship any unused code to browser and so that you keep your server as slim as possible.
+But the compiler still needs to _crawl_ all the code to know what to keep and what to treeshake away.
+
+In short, this means that the way you do imports and exports can have a big impact on how long it takes to rebuild your app.
+For example, if you are using a library like Material UI or AntD you can likely speed up your builds by using [path imports][path-imports]:
+
+```diff
+- import { Button, TextField } from '@mui/material';
++ import Button from '@mui/material/Button';
++ import TextField from '@mui/material/TextField';
+```
+
+In the future, Remix could pre-bundle dependencies in development to avoid this problem entirely.
+But today, you can help the compiler out by using path imports.
+
+## Manual mode
+
+By default, `remix dev` drives like an automatic.
+It keeps your app server up-to-date with the latest code changes by automatically restarting the app server whenever file changes are detected in your app code.
+This is a simple approach that stays out of your way and we think will work well for most apps.
+
+But if app server restarts are slowing you down, you can take the wheel and drive `remix dev` like a manual:
+
+```sh
+remix dev --manual -c "node ./server.js"
+```
+
+That means learning how to use the clutch to shift gears.
+It also means you might stall while your getting your bearings.
+It takes a bit more time to learn and its more code for you to maintain.
+
+> With great power comes great responsibility.
+
+We don't think its worth it unless you're feeling some pain with the default automatic mode.
+But if you are, Remix has got you covered.
+
+### Mental model for `remix dev`
+
+Before you start drag racing, it helps to understand how Remix works under the hood.
+It's especially important to understand that `remix dev` spins up _not one, but two servers_: the dev server and the app server.
+
+The dev server is a glorified compiler running in watch mode.
+**The browser _never_ sends requests to the dev server.**
+You probably shouldn't care what port it runs on.
+
+Check out our video ["Mental model for the new dev server 🧠"][mental-model] for more details.
+
+### Learning to drive stick
+
+When you switch on manual mode with `--manual`, you take on some new responsibilities:
+
+1. Detect when server code changes are available
+2. Re-import code changes while keeping the app server running
+3. Send "ready" message to dev server _after_ those changes are picked up
+
+Re-importing code changes turns out to be tricky because JS imports are cached.
+
+```js
+import fs from "node:fs";
+
+const original = await import("./build/index.js");
+fs.writeFileSync("./build/index.js", someCode);
+const changed = await import("./build/index.js");
+//  ^^^^^^^ this will return the original module from the import cache without the code changes
+```
+
+You need some way to bust the import cache when you want to re-import modules with code changes.
+Also importing modules is different between CommonJS (`require`) and ESM (`import`) which makes things even more complicated.
+
+#### 1.a CJS: `require` cache busting
+
+CommonJS uses `require` for imports, giving you direct access to the `require` cache.
+That lets you bust the cache for _just_ the server code when rebuilds occur.
+
+For example, here's how to bust the `require` cache for the Remix server build:
+
+```js
+const path = require("node:path");
+
+/**
+ * @typedef {import('@remix-run/node').ServerBuild} ServerBuild
+ */
+
+const BUILD_PATH = path.resolve("./build/index.js");
+
+/**
+ * Initial build
+ * @type {ServerBuild}
+ */
+let build = require(BUILD_PATH);
+
+/**
+ * @type {() => ServerBuild}
+ */
+const reimportServer = () => {
+  // 1. manually remove the server build from the require cache
+  Object.keys(require.cache).forEach((key) => {
+    if (key.startsWith(BUILD_PATH)) {
+      delete require.cache[key];
+    }
+  });
+
+  // 2. re-import the server build
+  return require(BUILD_PATH);
+};
+
+// to update your app server with new code changes:
+build = reimportServer();
+```
+
+<docs-info>
+
+The `require` cache keys are _absolute paths_ so make sure you resolve your server build path to an absolute path!
+
+</docs-info>
+
+#### 1.b ESM: `import` cache busting
+
+Unlike CJS, ESM doesn't give you direct access to the import cache.
+To workaround this, you can use a timestamp query parameter to force ESM to treat the import as a new module.
+
+```js
+import * as fs from "node:fs";
+import * as path from "node:path";
+
+/**
+ * @typedef {import('@remix-run/node').ServerBuild} ServerBuild
+ */
+
+const BUILD_PATH = "./build/index.js";
+
+/**
+ * Initial build
+ * @type {ServerBuild}
+ */
+let build = await import(BUILD_PATH);
+
+/**
+ * @type {() => Promise<ServerBuild>}
+ */
+const reimportServer = async () => {
+  const stat = fs.statSync(BUILD_PATH);
+
+  // use a timestamp query parameter to bust the import cache
+  return import(BUILD_PATH + "?t=" + stat.mtimeMs);
+};
+
+// to update your app server with new code changes:
+build = await reimportServer();
+```
+
+<docs-warning>
+
+In ESM, there's no way to remove entries from the `import` cache.
+While our timestamp workaround works, it means that the `import` cache will grow over time which can eventually cause Out of Memory errors.
+
+If this happens, you can restart the dev server to restart with a fresh import cache.
+In the future, Remix may pre-bundle your dependencies to keep the import cache small.
+
+</docs-warning>
+
+#### 2. Detecting server code changes
+
+Now that you have a way to bust the import cache for CJS or ESM, its time to put that to use by dynamically updating the server build within your app server.
+To detect when the server code changes, you can use a file watcher like [chokidar][chokidar]:
+
+```js
+import chokidar from "chokidar";
+
+async function handleServerUpdate() {
+  build = await reimportServer();
+}
+
+chokidar
+  .watch(BUILD_PATH, { ignoreInitial: true })
+  .on("add", handleServerUpdate)
+  .on("change", handleServerUpdate);
+```
+
+#### 3. Sending "ready" message to dev server
+
+Now's a good time to double-check that your app server is sending "ready" messages to the dev server when it initially spins up:
+
+```js filename=server.js lines=[5-7]
+const port = 3000;
+app.listen(port, async () => {
+  console.log(`Express server listening on port ${port}`);
+
+  if (process.env.NODE_ENV === "development") {
+    broadcastDevReady(build);
+  }
+});
+```
+
+In manual mode, you also need to send "ready" messages whenever you re-import the server build:
+
+```js lines=[4-5]
+async function handleServerUpdate() {
+  // 1. re-import the server build
+  build = await reimportServer();
+  // 2. tell dev server that this app server is now up-to-date and ready
+  broadcastDevReady(build);
+}
+```
+
+#### 4. Dev-aware request handler
+
+Last step is to wrap all of this up in a development mode request handler:
+
+```js
+function createDevRequestHandler() {
+  async function handleServerUpdate() {
+    // 1. re-import the server build
+    build = await reimportServer();
+    // 2. tell dev server that this app server is now up-to-date and ready
+    broadcastDevReady(build);
+  }
+
+  chokidar
+    .watch(BUILD_PATH, { ignoreInitial: true })
+    .on("add", handleServerUpdate)
+    .on("change", handleServerUpdate);
+
+  // wrap request handler to make sure its recreated with the latest build for every request
+  return async (req, res, next) => {
+    try {
+      return createRequestHandler({
+        build,
+        mode: "development",
+      })(req, res, next);
+    } catch (error) {
+      next(error);
+    }
+  };
+}
+```
+
+Awesome!
+Now let's plug in our new manual transmission when running in development mode:
+
+```js filename=server.js
+app.all(
+  "*",
+  process.env.NODE_ENV === "development"
+    ? createDevRequestHandler()
+    : createRequestHandler({
+        build,
+        mode: process.env.NODE_ENV,
+      })
+);
+```
+
+For complete app server code examples, check out the [express template][express-template] or [community examples][community-examples].
+
+### Keeping in-memory server state across rebuilds
+
+When server code is re-imported, any server-side in-memory state is lost.
+That includes things like database connections, caches, in-memory data structures, etc.
+
+Here's a utility that remembers any in-memory values you want to keep around across rebuilds:
+
+```ts filename=app/utils/remember.ts
+// adapted from https://github.com/jenseng/abuse-the-platform/blob/main/app/utils/singleton.ts
+// thanks @jenseng!
+
+export function remember<T>(
+  key: string,
+  getValue: () => T
+) {
+  const g = global as any;
+  g.__remember ??= {};
+  g.__remember[key] ??= getValue();
+  return g.__remember[key];
+}
+```
+
+For example, to reuse a Prisma client across rebuilds:
+
+```ts filename=app/db.server.ts
+import { PrismaClient } from "@prisma/client";
+
+import { remember } from "~/utils/remember";
+
+// hard-code a unique key so we can look up the client when this module gets re-imported
+export const db = remember("db", () => new PrismaClient());
+```
+
+[path-imports]: https://mui.com/material-ui/guides/minimizing-bundle-size/#option-one-use-path-imports
+[mental-model]: https://www.youtube.com/watch?v=zTrjaUt9hLo
+[express]: https://expressjs.com/
+[chokidar]: https://github.com/paulmillr/chokidar
+[express-template]: https://github.com/remix-run/remix/blob/main/templates/express/server.js
+[community-examples]: https://github.com/xHomu/remix-v2-server
diff --git a/docs/guides/manual-mode.md b/docs/guides/manual-mode.md
new file mode 100644
index 000000000..1855753ec
--- /dev/null
+++ b/docs/guides/manual-mode.md
@@ -0,0 +1,282 @@
+---
+title: Manual mode
+toc: false
+---
+
+# Manual mode
+
+By default, `remix dev` drives like an automatic.
+It keeps your app server up-to-date with the latest code changes by automatically restarting the app server whenever file changes are detected in your app code.
+This is a simple approach that stays out of your way and we think will work well for most apps.
+
+But if app server restarts are slowing you down, you can take the wheel and drive `remix dev` like a manual:
+
+```sh
+remix dev --manual -c "node ./server.js"
+```
+
+That means learning how to use the clutch to shift gears.
+It also means you might stall while your getting your bearings.
+It takes a bit more time to learn and its more code for you to maintain.
+
+> With great power comes great responsibility.
+
+We don't think its worth it unless you're feeling some pain with the default automatic mode.
+But if you are, Remix has got you covered.
+
+## Mental model for `remix dev`
+
+Before you start drag racing, it helps to understand how Remix works under the hood.
+It's especially important to understand that `remix dev` spins up _not one, but two servers_: the dev server and the app server.
+
+The dev server is a glorified compiler running in watch mode.
+**The browser _never_ sends requests to the dev server.**
+You probably shouldn't care what port it runs on.
+
+Check out our video ["Mental model for the new dev server 🧠"][mental-model] for more details.
+
+## Learning to drive stick
+
+When you switch on manual mode with `--manual`, you take on some new responsibilities:
+
+1. Detect when server code changes are available
+2. Re-import code changes while keeping the app server running
+3. Send "ready" message to dev server _after_ those changes are picked up
+
+Re-importing code changes turns out to be tricky because JS imports are cached.
+
+```js
+import fs from "node:fs";
+
+const original = await import("./build/index.js");
+fs.writeFileSync("./build/index.js", someCode);
+const changed = await import("./build/index.js");
+//    ^^^^^^^ this will return the original module from the import cache without the code changes
+```
+
+You need some way to bust the import cache when you want to re-import modules with code changes.
+Also importing modules is different between CommonJS (`require`) and ESM (`import`) which makes things even more complicated.
+
+### 1.a CJS: `require` cache busting
+
+CommonJS uses `require` for imports, giving you direct access to the `require` cache.
+That lets you bust the cache for _just_ the server code when rebuilds occur.
+
+For example, here's how to bust the `require` cache for the Remix server build:
+
+```js
+const path = require("node:path");
+
+/**
+ * @typedef {import('@remix-run/node').ServerBuild} ServerBuild
+ */
+
+const BUILD_PATH = path.resolve("./build/index.js");
+
+/**
+ * Initial build
+ * @type {ServerBuild}
+ */
+let build = require(BUILD_PATH);
+
+/**
+ * @type {() => ServerBuild}
+ */
+const reimportServer = () => {
+  // 1. manually remove the server build from the require cache
+  Object.keys(require.cache).forEach((key) => {
+    if (key.startsWith(BUILD_PATH)) {
+      delete require.cache[key];
+    }
+  });
+
+  // 2. re-import the server build
+  return require(BUILD_PATH);
+};
+
+// to update your app server with new code changes:
+build = reimportServer();
+```
+
+<docs-info>
+
+The `require` cache keys are _absolute paths_ so make sure you resolve your server build path to an absolute path!
+
+</docs-info>
+
+### 1.b ESM: `import` cache busting
+
+Unlike CJS, ESM doesn't give you direct access to the import cache.
+To workaround this, you can use a timestamp query parameter to force ESM to treat the import as a new module.
+
+```js
+import * as fs from "node:fs";
+import * as path from "node:path";
+
+/**
+ * @typedef {import('@remix-run/node').ServerBuild} ServerBuild
+ */
+
+const BUILD_PATH = "./build/index.js";
+
+/**
+ * Initial build
+ * @type {ServerBuild}
+ */
+let build = await import(BUILD_PATH);
+
+/**
+ * @type {() => Promise<ServerBuild>}
+ */
+const reimportServer = async () => {
+  const stat = fs.statSync(BUILD_PATH);
+
+  // use a timestamp query parameter to bust the import cache
+  return import(BUILD_PATH + "?t=" + stat.mtimeMs);
+};
+
+// to update your app server with new code changes:
+build = await reimportServer();
+```
+
+<docs-warning>
+
+In ESM, there's no way to remove entries from the `import` cache.
+While our timestamp workaround works, it means that the `import` cache will grow over time which can eventually cause Out of Memory errors.
+
+If this happens, you can restart the dev server to restart with a fresh import cache.
+In the future, Remix may pre-bundle your dependencies to keep the import cache small.
+
+</docs-warning>
+
+### 2. Detecting server code changes
+
+Now that you have a way to bust the import cache for CJS or ESM, its time to put that to use by dynamically updating the server build within your app server.
+To detect when the server code changes, you can use a file watcher like [chokidar][chokidar]:
+
+```js
+import chokidar from "chokidar";
+
+async function handleServerUpdate() {
+  build = await reimportServer();
+}
+
+chokidar
+  .watch(BUILD_PATH, { ignoreInitial: true })
+  .on("add", handleServerUpdate)
+  .on("change", handleServerUpdate);
+```
+
+### 3. Sending "ready" message to dev server
+
+Now's a good time to double-check that your app server is sending "ready" messages to the dev server when it initially spins up:
+
+```js filename=server.js lines=[5-7]
+const port = 3000;
+app.listen(port, async () => {
+  console.log(`Express server listening on port ${port}`);
+
+  if (process.env.NODE_ENV === "development") {
+    broadcastDevReady(build);
+  }
+});
+```
+
+In manual mode, you also need to send "ready" messages whenever you re-import the server build:
+
+```js lines=[4-5]
+async function handleServerUpdate() {
+  // 1. re-import the server build
+  build = await reimportServer();
+  // 2. tell dev server that this app server is now up-to-date and ready
+  broadcastDevReady(build);
+}
+```
+
+### 4. Dev-aware request handler
+
+Last step is to wrap all of this up in a development mode request handler:
+
+```js
+function createDevRequestHandler() {
+  async function handleServerUpdate() {
+    // 1. re-import the server build
+    build = await reimportServer();
+    // 2. tell dev server that this app server is now up-to-date and ready
+    broadcastDevReady(build);
+  }
+
+  chokidar
+    .watch(BUILD_PATH, { ignoreInitial: true })
+    .on("add", handleServerUpdate)
+    .on("change", handleServerUpdate);
+
+  // wrap request handler to make sure its recreated with the latest build for every request
+  return async (req, res, next) => {
+    try {
+      return createRequestHandler({
+        build,
+        mode: "development",
+      })(req, res, next);
+    } catch (error) {
+      next(error);
+    }
+  };
+}
+```
+
+Awesome!
+Now let's plug in our new manual transmission when running in development mode:
+
+```js filename=server.js
+app.all(
+  "*",
+  process.env.NODE_ENV === "development"
+    ? createDevRequestHandler()
+    : createRequestHandler({
+        build,
+        mode: process.env.NODE_ENV,
+      })
+);
+```
+
+For complete app server code examples, check out the [express template][express-template] or [community examples][community-examples].
+
+## Keeping in-memory server state across rebuilds
+
+When server code is re-imported, any server-side in-memory state is lost.
+That includes things like database connections, caches, in-memory data structures, etc.
+
+Here's a utility that remembers any in-memory values you want to keep around across rebuilds:
+
+```ts filename=app/utils/remember.ts
+// adapted from https://github.com/jenseng/abuse-the-platform/blob/main/app/utils/singleton.ts
+// thanks @jenseng!
+
+export function remember<T>(
+  key: string,
+  getValue: () => T
+) {
+  const g = global as any;
+  g.__remember ??= {};
+  g.__remember[key] ??= getValue();
+  return g.__remember[key];
+}
+```
+
+For example, to reuse a Prisma client across rebuilds:
+
+```ts filename=app/db.server.ts
+import { PrismaClient } from "@prisma/client";
+
+import { remember } from "~/utils/remember";
+
+// hard-code a unique key so we can look up the client when this module gets re-imported
+export const db = remember("db", () => new PrismaClient());
+```
+
+[mental-model]: https://www.youtube.com/watch?v=zTrjaUt9hLo
+[express]: https://expressjs.com/
+[chokidar]: https://github.com/paulmillr/chokidar
+[express-template]: https://github.com/remix-run/remix/blob/main/templates/express/server.js
+[community-examples]: https://github.com/xHomu/remix-v2-server
diff --git a/docs/guides/performance.md b/docs/guides/performance.md
index 79c55732c..63563c237 100644
--- a/docs/guides/performance.md
+++ b/docs/guides/performance.md
@@ -10,6 +10,16 @@ Instead of prescribing a precise architecture with all of its constraints like S
 
 The fastest thing to send to a user is, of course, a static document on a CDN that's close to the user. Until recently, servers pretty much only ran in one region of the world, which made for slow responses everywhere else. This is perhaps one reason SSG gained so much popularity, it allowed developers to essentially "cache" their data into HTML documents and then distribute them across the world. It comes with a lot of tradeoffs too: build times, build complexity, duplicate websites for translations, can't use it for authenticated user experiences, can't use it for very large and dynamic data sources (like our project [unpkg.com][unpkg-com]!) to name a few.
 
+## Bundle analysis
+
+Remix outputs metafiles to the server build directory (`build/` by default) so you can analyze your bundle size and composition.
+
+- `metafile.css.json` : Metafile for the CSS bundle
+- `metafile.js.json` : Metafile for the browser JS bundle
+- `metafile.server.json` : Metafile for the serve JS bundle
+
+Remix uses esbuild's metafile format so you can directly upload those files to https://esbuild.github.io/analyze/ to visualize your bundle.
+
 ## The Edge
 
 (No, not the U2 guy.)
diff --git a/docs/other-api/dev-v2.md b/docs/other-api/dev-v2.md
index 2b5fc18fb..6854db29e 100644
--- a/docs/other-api/dev-v2.md
+++ b/docs/other-api/dev-v2.md
@@ -4,6 +4,8 @@ order: 2
 new: true
 ---
 
+# Remix CLI (v2)
+
 The Remix CLI comes from the `@remix-run/dev` package. It also includes the compiler. Make sure it is in your `package.json` `devDependencies` so it doesn't get deployed to your server.
 
 To get a full list of available commands and flags, run:
@@ -136,13 +138,13 @@ but CloudFlare does not support async I/O like `fetch` outside of request handli
 
 Options priority order is: 1. flags, 2. config, 3. defaults.
 
-| Option          | flag               | config           | default                           |
-| --------------- | ------------------ | ---------------- | --------------------------------- |
-| Command         | `-c` / `--command` | `command`        | `remix-serve <server build path>` |
-| No restart      | `--no-restart`     | `restart: false` | `restart: true`                   |
-| Port            | `--port`           | `port`           | Dynamically chosen open port      |
-| TLS key         | `--tls-key`        | `tlsKey`         | N/A                               |
-| TLS certificate | `--tls-cert`       | `tlsCert`        | N/A                               |
+| Option          | flag               | config    | default                           | description                                                |
+| --------------- | ------------------ | --------- | --------------------------------- | ---------------------------------------------------------- |
+| Command         | `-c` / `--command` | `command` | `remix-serve <server build path>` | Command the dev server will run to spin up your app server |
+| Manual          | `--manual`         | `manual`  | `false`                           | See [guide for manual mode](../guides/manual-mode)         |
+| Port            | `--port`           | `port`    | Dynamically chosen open port      | Internal port used for hot updates                         |
+| TLS key         | `--tls-key`        | `tlsKey`  | N/A                               | TLS key for configuring local HTTPS                        |
+| TLS certificate | `--tls-cert`       | `tlsCert` | N/A                               | TLS certificate for configuring local HTTPS                |
 
 <docs-info>
 
@@ -156,7 +158,7 @@ for example to setup Docker networking or use a specific open port for security
 
 </docs-info>
 
-For example, to override the port used by the dev server via config:
+For example, to override the internal port used by the dev server via config:
 
 ```js filename=remix.config.js
 module.exports = {
@@ -168,74 +170,13 @@ module.exports = {
 };
 ```
 
-### Keep app server running across rebuilds
-
-By default, the Remix dev server restarts your app server when rebuilds occur.
-This is a simple way to ensure that your app server is up-to-date with the latest code changes.
-
-If you'd like to opt-out of this behavior use the `--no-restart` flag:
-
-```sh
-remix dev --no-restart -c 'node ./server.js'
-```
-
-🚨 BUT that means you are now on the hook for applying changes to your running app server _and_ telling the dev server when those changes have been applied.
-
-> With great power comes great responsibility.
-
-Check out our [templates][templates] for examples on how to use `import` cache busting to apply code changes to your app server while it keeps running.
-
-If you're using CJS but looking at an ESM template, you'll need to swap out `import` cache busting with `require` cache busting:
-
-```diff
-- const stat = fs.statSync(BUILD_DIR);
-- build = import(BUILD_DIR + "?t=" + stat.mtimeMs);
-+ for (const key in require.cache) {
-+   if (key.startsWith(BUILD_DIR)) {
-+     delete require.cache[key];
-+   }
-+ }
-+ build = require(BUILD_DIR)
-```
-
-#### Pick up changes from other packages
+### Pick up changes from other packages
 
 If you are using a monorepo, you might want Remix to perform hot updates not only when your app code changes, but whenever you change code in any of your apps dependencies.
 
 For example, you could have a UI library package (`packages/ui`) that is used within your Remix app (`packages/app`).
 To pick up changes in `packages/ui`, you can configure [watchPaths][watch-paths] to include your packages.
 
-#### Keep in-memory data and connections across rebuilds
-
-Every time you re-import code to apply changes to your app server, that code will be run.
-Rerunning each changed module works great in most cases, but sometimes you want to want to keep stuff around.
-
-For example, it'd be nice if your app only connected to your database once and kept that connection around across rebuilds.
-But since the connection is held in-memory, re-imports will wipe those out and cause your app to reconnect.
-
-Luckily, there's a trick to get around this: use `global` as a cache for keeping things in-memory across rebuilds!
-Here's a nifty utility adapted from [Jon Jensen's code][jenseng-code] for [his Remix Conf 2023 talk][jenseng-talk]:
-
-```ts filename=app/utils/remember.ts
-export function remember<T>(key: string, value: T) {
-  const g = global as any;
-  g.__singletons ??= {};
-  g.__singletons[key] ??= value;
-  return g.__singletons[key];
-}
-```
-
-And here's how to use it to keep stuff around across rebuilds:
-
-```ts filename=app/utils/db.server.ts
-import { PrismaClient } from "@prisma/client";
-
-import { remember } from "~/utils/remember";
-
-// hard-code a unique key so we can look up the client when this module gets re-imported
-export const db = remember("db", new PrismaClient());
-```
-
 ### How to set up MSW
 
 To use [Mock Service Worker][msw] in development, you'll need to:
@@ -366,6 +307,31 @@ Now, hot updates will be sent correctly to the proxy:
 
 - Hot updates 👉 `https://myhost` / `wss://myhost` ✅
 
+### Performance tuning and debugging
+
+#### Path imports
+
+Currently, when Remix rebuilds your app, the compiler has to process your app code along with any of its dependencies.
+The compiler treeshakes unused code from app so that you don't ship any unused code to browser and so that you keep your server as slim as possible.
+But the compiler still needs to _crawl_ all the code to know what to keep and what to treeshake away.
+
+In short, this means that the way you do imports and exports can have a big impact on how long it takes to rebuild your app.
+For example, if you are using a library like Material UI or AntD you can likely speed up your builds by using [path imports][path-imports]:
+
+```diff
+- import { Button, TextField } from '@mui/material';
++ import Button from '@mui/material/Button';
++ import TextField from '@mui/material/TextField';
+```
+
+In the future, Remix could pre-bundle dependencies in development to avoid this problem entirely.
+But today, you can help the compiler out by using path imports.
+
+#### Debugging bundles
+
+Dependending on your app and dependencies, you might be processing much more code than your app needs.
+Check out our [bundle analysis guide][bundle-analysis] for more details.
+
 ### Troubleshooting
 
 #### HMR: hot updates losing app state
@@ -411,12 +377,11 @@ That way the dev server can detect loader changes on rebuilds.
 While the initial build slowdown is inherently a cost for HDR, we plan to optimize rebuilds so that there is no perceivable slowdown for HDR rebuilds.
 
 [legendary-dx]: https://www.youtube.com/watch?v=79M4vYZi-po
-[templates]: https://github.com/remix-run/remix/tree/main/templates
 [watch-paths]: https://remix.run/docs/en/1.17.1/file-conventions/remix-config#watchpaths
-[jenseng-code]: https://github.com/jenseng/abuse-the-platform/blob/main/app/utils/singleton.ts
-[jenseng-talk]: https://www.youtube.com/watch?v=lbzNnN0F67Y
 [react-keys]: https://react.dev/learn/rendering-lists#why-does-react-need-keys
 [react-refresh]: https://github.com/facebook/react/tree/main/packages/react-refresh
 [binode]: https://github.com/kentcdodds/binode
 [msw]: https://mswjs.io/
 [mkcert]: https://github.com/FiloSottile/mkcert
+[path-imports]: https://mui.com/material-ui/guides/minimizing-bundle-size/#option-one-use-path-imports
+[bundle-analysis]: ../guides/performance
diff --git a/docs/pages/v2.md b/docs/pages/v2.md
index 47ef4988e..f941b1d54 100644
--- a/docs/pages/v2.md
+++ b/docs/pages/v2.md
@@ -778,22 +778,15 @@ app.listen(port, async () => {
 });
 ```
 
-4. (Optional) `--no-restart`
+4. (Optional) `--manual`
 
-If you were relying on `require` cache purging, you can keep doing so by using the `--no-restart` flag:
+If you were relying on `require` cache purging, you can keep doing so by using the `--manual` flag:
 
 ```sh
 remix dev --no-restart -c 'node ./server.js'
 ```
 
-<docs-info>
-
-If you are purging your `require` cache for every request, you can instead switch to only busting the cache when your app server rebuilds.
-To do so, use a file watcher like `chokidar`.
-
-</docs-info>
-
-See our [templates][templates] and the [`remix dev` docs][v2-dev-docs] for examples on how to do this.
+Check out the [manual mode guide][manual-mode] for more details.
 
 [future-flags]: ./api-development-strategy
 [remix-config]: ../file-conventions/remix-config
@@ -805,3 +798,4 @@ See our [templates][templates] and the [`remix dev` docs][v2-dev-docs] for examp
 [v2-dev-config]: ../other-api/dev-v2
 [templates]: https://github.com/remix-run/remix/tree/main/templates
 [v2-dev-docs]: ../other-api/dev-v2.md
+[manual-mode]: ../guides/manual-mode.md
diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts
index 0280942ad..e766158a8 100644
--- a/packages/remix-dev/cli/commands.ts
+++ b/packages/remix-dev/cli/commands.ts
@@ -215,12 +215,13 @@ export async function dev(
 
     // v2_dev
     command?: string;
-    scheme?: string;
-    host?: string;
+    manual?: boolean;
     port?: number;
-    restart?: boolean;
     tlsKey?: string;
     tlsCert?: string;
+    scheme?: string; // TODO: remove in v2
+    host?: string; // TODO: remove in v2
+    restart?: boolean; // TODO: remove in v2
   } = {}
 ) {
   console.log(`\n 💿  remix dev\n`);
@@ -234,6 +235,12 @@ export async function dev(
   let config = await readConfig(remixRoot);
 
   if (config.future.v2_dev === false) {
+    logger.warn("The `remix dev` changing in v2", {
+      details: [
+        "You can use the `v2_dev` future flag to opt-in early.",
+        "-> https://remix.run/docs/en/main/pages/v2#dev-server",
+      ],
+    });
     await devServer.serve(config, flags.port);
     return await new Promise(() => {});
   }
@@ -521,13 +528,14 @@ let resolveDev = async (
 let resolveDevServe = async (
   config: RemixConfig,
   flags: {
+    command?: string;
+    manual?: boolean;
     port?: number;
     tlsKey?: string;
     tlsCert?: string;
     scheme?: string; // TODO: remove in v2
     host?: string; // TODO: remove in v2
-    command?: string;
-    restart?: boolean;
+    restart?: boolean; // TODO: remove in v2
   } = {}
 ) => {
   let dev = config.future.v2_dev;
@@ -540,12 +548,27 @@ let resolveDevServe = async (
     flags.command ??
     (dev === true ? undefined : dev.command)
 
-  let restart =
-    flags.restart ?? (dev === true ? undefined : dev.restart) ?? true;
+  // TODO: remove in v2
+  let restart = dev === true ? undefined : dev.restart;
+  if (restart !== undefined) {
+    logger.warn("The `v2_dev.restart` option is deprecated", {
+      details: [
+        "Use `v2_dev.manual` instead.",
+        "-> https://remix.run/docs/en/main/guides/development-performance#manual-mode",
+      ],
+    });
+  }
+
+  // prettier-ignore
+  let manual =
+    flags.manual ??
+    (dev === true ? undefined : dev.manual) ??
+    restart !== undefined ? !restart : // TODO: remove in v2
+    false;
 
   return {
     ...resolved,
     command,
-    restart,
+    manual,
   };
 };
diff --git a/packages/remix-dev/cli/run.ts b/packages/remix-dev/cli/run.ts
index 7f14c1f5b..af5bdc546 100644
--- a/packages/remix-dev/cli/run.ts
+++ b/packages/remix-dev/cli/run.ts
@@ -45,8 +45,8 @@ ${colors.logoBlue("R")} ${colors.logoGreen("E")} ${colors.logoYellow(
 
     [v2_dev]
     --command, -c       Command used to run your app server
+    --manual            Enable manual mode
     --port              Port for the dev server. Default: any open port
-    --no-restart        Do not restart the app server when rebuilds occur.
     --tls-key           Path to TLS key (key.pem)
     --tls-cert          Path to TLS certificate (cert.pem)
   \`init\` Options:
@@ -182,13 +182,14 @@ export async function run(argv: string[] = process.argv.slice(2)) {
       // dev server
       "--command": String,
       "-c": "--command",
+      "--manual": Boolean,
       "--port": Number,
       "-p": "--port",
-      "--no-restart": Boolean,
       "--tls-key": String,
       "--tls-cert": String,
 
       // deprecated, remove in v2
+      "--no-restart": Boolean,
       "--scheme": String,
       "--host": String,
     },
@@ -254,7 +255,13 @@ export async function run(argv: string[] = process.argv.slice(2)) {
   }
   flags.interactive = flags.interactive ?? require.main === module;
   if (args["--no-restart"]) {
-    flags.restart = false;
+    logger.warn("`--no-restart` flag is deprecated", {
+      details: [
+        "Use `--manual` instead.",
+        "-> https://remix.run/docs/en/main/guides/development-performance#manual-mode",
+      ],
+    });
+    flags.manual = true;
     delete flags["no-restart"];
   }
   if (args["--no-typescript"]) {
diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts
index ca834c9e8..647858e46 100644
--- a/packages/remix-dev/config.ts
+++ b/packages/remix-dev/config.ts
@@ -38,11 +38,13 @@ export type ServerPlatform = "node" | "neutral";
 
 type Dev = {
   command?: string;
+  manual?: boolean;
   port?: number;
-  restart?: boolean;
   tlsKey?: string;
   tlsCert?: string;
 
+  /** @deprecated remove in v2 */
+  restart?: boolean;
   /** @deprecated remove in v2 */
   scheme?: string;
   /** @deprecated remove in v2 */
diff --git a/packages/remix-dev/devServer_unstable/index.ts b/packages/remix-dev/devServer_unstable/index.ts
index 1abeb3641..ef2c5151f 100644
--- a/packages/remix-dev/devServer_unstable/index.ts
+++ b/packages/remix-dev/devServer_unstable/index.ts
@@ -39,11 +39,11 @@ export let serve = async (
   initialConfig: RemixConfig,
   options: {
     command?: string;
+    manual: boolean;
     port: number;
     tlsKey?: string;
     tlsCert?: string;
     REMIX_DEV_ORIGIN: URL;
-    restart: boolean;
   }
 ) => {
   await loadEnv(initialConfig.rootDirectory);
@@ -200,7 +200,7 @@ export let serve = async (
         let newState: typeof state = { prevManifest: state.manifest };
         try {
           let start = Date.now();
-          if (state.appServer === undefined || options.restart) {
+          if (state.appServer === undefined || !options.manual) {
             if (state.appServer?.pid) {
               await killtree(state.appServer.pid);
             }


Test patch:
diff --git a/packages/remix-dev/__tests__/cli-test.ts b/packages/remix-dev/__tests__/cli-test.ts
index 75160fd01..cd744cffd 100644
--- a/packages/remix-dev/__tests__/cli-test.ts
+++ b/packages/remix-dev/__tests__/cli-test.ts
@@ -118,8 +118,8 @@ describe("remix CLI", () => {
 
             [v2_dev]
             --command, -c       Command used to run your app server
+            --manual            Enable manual mode
             --port              Port for the dev server. Default: any open port
-            --no-restart        Do not restart the app server when rebuilds occur.
             --tls-key           Path to TLS key (key.pem)
             --tls-cert          Path to TLS certificate (cert.pem)
           \`init\` Options: