Viewing docs for an older release. View latest
Vite (Unstable)
On this page

Vite (Unstable)

Vite is a powerful, performant and extensible development environment for JavaScript projects. In order to improve and extend Remix's bundling capabilities, we now support Vite as an alternative compiler. In the future, Vite will become the default compiler for Remix.

Getting started

We've got a few different Vite-based templates to get you started.

# Minimal server:
npx create-remix@latest --template remix-run/remix/templates/unstable-vite

# Express:
npx create-remix@latest --template remix-run/remix/templates/unstable-vite-express

# Cloudflare:
npx create-remix@latest --template remix-run/remix/templates/unstable-vite-cloudflare

These templates include a vite.config.ts file which is where the Remix Vite plugin is configured.

Configuration

The Vite plugin does not use remix.config.js. Instead, the plugin accepts options directly.

For example, to configure ignoredRouteFiles:

import { unstable_vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    remix({
      ignoredRouteFiles: ["**/.*"],
    }),
  ],
});

All other bundling-related options are now configured with Vite. This means you have much greater control over the bundling process.

Supported Remix config options

The following subset of Remix config options are supported:

The Vite plugin also accepts the following additional options:

buildDirectory

The path to the build directory, relative to the project root. Defaults to "build".

buildEnd

A function that is called after the full Remix build is complete.

manifest

Whether to write a .remix/manifest.json file to the build directory. Defaults to false.

presets

An array of presets to ease integration with other tools and hosting providers.

serverBuildFile

The name of the server file generated in the server build directory. Defaults to "index.js".

serverBundles

A function for assigning addressable routes to server bundles.

You may also want to enable the manifest option since, when server bundles are enabled, it contains mappings between routes and server bundles.

Cloudflare

To get started with Cloudflare, you can use the unstable-vite-cloudflare template:

npx create-remix@latest --template remix-run/remix/templates/unstable-vite-cloudflare

Bindings

Bindings for Cloudflare resources can be configured within wrangler.toml for local development or within the Cloudflare dashboard for deployments. Then, you can access your bindings via context.env. For example, with a KV namespace bound as MY_KV:

export async function loader({ context }) {
  const { MY_KV } = context.env;
  const value = await MY_KV.get("my-key");
  return json({ value });
}

Vite & Wrangler

There are two ways to run your Cloudflare app locally:

# Vite
remix vite:dev

# Wrangler
remix vite:build # build app before running wrangler
wrangler pages dev ./build/client

While Vite provides a better development experience, Wrangler provides closer emulation of the Cloudflare environment by running your server code in Cloudflare's workerd runtime instead of Node. To simulate the Cloudflare environment in Vite, Wrangler provides Node proxies for resource bindings which are automatically available when using the Remix Cloudflare preset:

import {
  unstable_vitePlugin as remix,
  unstable_cloudflarePreset as cloudflare,
} from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    remix({
      presets: [cloudflare()],
    }),
  ],
});

The Cloudflare team is working to improve their Node proxies to support:

Vite will not use your Cloudflare Pages Functions (functions/*) in development as those are purely for Wrangler routing.

Augmenting Cloudflare load context in development

The Cloudflare preset accepts a getRemixDevLoadContext function that can be used to augment the load context in development:

export default defineConfig({
  plugins: [
    remix({
      presets: [
        cloudflare({
          getRemixDevLoadContext: (context) => ({
            ...context,
            extra: "add on whatever else you want",
          }),
        }),
      ],
    }),
  ],
});

As the name implies, it only augments the load context within Vite's dev server, not within Wrangler nor in production. This limitation exists because Vite is not yet able to delegate server code execution to other non-Node runtimes like Cloudflare's workerd runtime.

To get a consistent load context across Vite, Wrangler, and production you can define a module like get-load-context.ts that exports shared logic for augmenting the load context. Then you can apply the same logic within getRemixDevLoadContext and within functions/[[page]].ts.

Splitting up client and server code

Remix lets you write code that runs on both the client and the server. Out-of-the-box, Vite doesn't support mixing server-only code with client-safe code in the same module. Remix is able to make an exception for routes because we know which exports are server-only and can remove them from the client.

There are a few ways to isolate server-only code in Remix. The simplest approach is to use .server modules.

.server modules

While not strictly necessary, .server modules are a good way to explicitly mark entire modules as server-only. The build will fail if any code in a .server file or .server directory accidentally ends up in the client module graph.

app
β”œβ”€β”€ .server πŸ‘ˆ marks all files in this directory as server-only
β”‚   β”œβ”€β”€ auth.ts
β”‚   └── db.ts
β”œβ”€β”€ cms.server.ts πŸ‘ˆ marks this file as server-only
β”œβ”€β”€ root.tsx
└── routes
    └── _index.tsx

.server modules must be within your Remix app directory.

vite-env-only

If you want to mix server-only code and client-safe code in the same module, you can use vite-env-only. This Vite plugin allows you to explicitly mark any expression as server-only so that it gets replaced with undefined in the client.

For example, once you've added the plugin to your Vite config, you can wrap any server-only exports with serverOnly$:

import { serverOnly$ } from "vite-env-only";

import { db } from "~/.server/db";

export const getPosts = serverOnly$(async () => {
  return db.posts.findMany();
});

export const PostPreview = ({ title, description }) => {
  return (
    <article>
      <h2>{title}</h2>
      <p>{description}</p>
    </article>
  );
};

This example would be compiled into the following code for the client:

export const getPosts = undefined;

export const PostPreview = ({ title, description }) => {
  return (
    <article>
      <h2>{title}</h2>
      <p>{description}</p>
    </article>
  );
};

New build output paths

There is a notable difference with the way Vite manages the public directory compared to the existing Remix compiler. Vite copies files from the public directory into the client build directory, whereas the Remix compiler left the public directory untouched and used a subdirectory (public/build) as the client build directory.

In order to align the default Remix project structure with the way Vite works, the build output paths have been changed. There is now a single buildDirectory option that defaults to "build", replacing the separate assetsBuildDirectory and serverBuildDirectory options. This means that, by default, the server is now compiled into build/server and the client is now compiled into build/client.

This also means that the following configuration defaults have been changed:

  • publicPath defaults to "/" rather than "/build/"
  • serverBuildPath has been replaced by serverBuildFile which defaults to "index.js". This file will be written into the server directory within your configured buildDirectory.

Additional features & plugins

One of the reasons that Remix is moving to Vite is, so you have less to learn when adopting Remix. This means that, for any additional bundling features you'd like to use, you should reference Vite documentation and the Vite plugin community rather than the Remix documentation.

Vite has many features and plugins that are not built into the existing Remix compiler. The use of any such features will render the existing Remix compiler unable to compile your app, so only use them if you intend to use Vite exclusively from here on out.

Migrating

Setup Vite

πŸ‘‰ Install Vite as a development dependency

npm install -D vite

Remix is now just a Vite plugin, so you'll need to hook it up to Vite.

πŸ‘‰ Replace remix.config.js with vite.config.ts at the root of your Remix app

import { unstable_vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [remix()],
});

The subset of supported Remix config options should be passed directly to the plugin:

export default defineConfig({
  plugins: [
    remix({
      ignoredRouteFiles: ["**/.*"],
    }),
  ],
});

HMR & HDR

Vite provides a robust client-side runtime for development features like HMR, making the <LiveReload /> component obsolete. When using the Remix Vite plugin in development, the <Scripts /> component will automatically include Vite's client-side runtime and other dev-only scripts.

πŸ‘‰ Remove <LiveReload/>, keep <Scripts />

  import {
-   LiveReload,
    Outlet,
    Scripts,
  }

  export default function App() {
    return (
      <html>
        <head>
        </head>
        <body>
          <Outlet />
-         <LiveReload />
          <Scripts />
        </body>
      </html>
    )
  }

TypeScript integration

Vite handles imports for all sorts of different file types, sometimes in ways that differ from the existing Remix compiler, so let's reference Vite's types from vite/client instead of the obsolete types from @remix-run/dev.

Since the module types provided by vite/client are not compatible with the module types implicitly included with @remix-run/dev, you'll also need to enable the skipLibCheck flag in your TypeScript config. Remix won't require this flag in the future once the Vite plugin is the default compiler.

πŸ‘‰ Rename remix.env.d.ts to env.d.ts

-/remix.env.d.ts
+/env.d.ts

πŸ‘‰ Replace @remix-run/dev types with vite/client in env.d.ts

-/// <reference types="@remix-run/dev" />
+/// <reference types="vite/client" />
/// <reference types="@remix-run/node" />

πŸ‘‰ Replace reference to remix.env.d.ts with env.d.ts in tsconfig.json

- "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
+ "include": ["env.d.ts", "**/*.ts", "**/*.tsx"],

πŸ‘‰ Ensure skipLibCheck is enabled in tsconfig.json

"skipLibCheck": true,

πŸ‘‰ Ensure module and moduleResolution fields are set correctly in tsconfig.json

"module": "ESNext",
"moduleResolution": "Bundler",

Migrating from Remix App Server

If you were using remix-serve in development (or remix dev without the -c flag), you'll need to switch to the new minimal dev server. It comes built-in with the Remix Vite plugin and will take over when you run remix vite:dev.

The Remix Vite plugin doesn't install any global Node polyfills so you'll need to install them yourself if you were relying on remix-serve to provide them. The easiest way to do this is by calling installGlobals at the top of your Vite config.

The Vite dev server's default port is different to remix-serve so you'll need to configure this via Vite's server.port option if you'd like to maintain the same port.

You'll also need to update to the new build output paths, which are build/server for the server and build/client for client assets.

πŸ‘‰ Update your dev, build and start scripts

{
  "scripts": {
    "dev": "remix vite:dev",
    "build": "remix vite:build",
    "start": "remix-serve ./build/server/index.js"
  }
}

πŸ‘‰ Install global Node polyfills in your Vite config

import { unstable_vitePlugin as remix } from "@remix-run/dev";
+import { installGlobals } from "@remix-run/node";
import { defineConfig } from "vite";

+installGlobals();

export default defineConfig({
  plugins: [remix()],
});

πŸ‘‰ Configure your Vite dev server port (optional)

export default defineConfig({
  server: {
    port: 3000,
  },
  plugins: [remix()],
});

Migrating a custom server

If you were using a custom server in development, you'll need to edit your custom server to use Vite's connect middleware. This will delegate asset requests and initial render requests to Vite during development, letting you benefit from Vite's excellent DX even with a custom server.

You can then load the virtual module named "virtual:remix/server-build" during development to create a Vite-based request handler.

You'll also need to update your server code to reference the new build output paths, which are build/server for the server build and build/client for client assets.

For example, if you were using Express, here's how you could do it.

πŸ‘‰ Update your server.mjs file

import { createRequestHandler } from "@remix-run/express";
import { installGlobals } from "@remix-run/node";
import express from "express";

installGlobals();

const viteDevServer =
  process.env.NODE_ENV === "production"
    ? undefined
    : await import("vite").then((vite) =>
        vite.createServer({
          server: { middlewareMode: true },
        })
      );

const app = express();

// handle asset requests
if (viteDevServer) {
  app.use(viteDevServer.middlewares);
} else {
  app.use(
    "/assets",
    express.static("build/client/assets", {
      immutable: true,
      maxAge: "1y",
    })
  );
}
app.use(express.static("build/client", { maxAge: "1h" }));

// handle SSR requests
app.all(
  "*",
  createRequestHandler({
    build: viteDevServer
      ? () =>
          viteDevServer.ssrLoadModule(
            "virtual:remix/server-build"
          )
      : await import("./build/server/index.js"),
  })
);

const port = 3000;
app.listen(port, () =>
  console.log("http://localhost:" + port)
);

πŸ‘‰ Update your build, dev, and start scripts

{
  "scripts": {
    "dev": "node ./server.mjs",
    "build": "remix vite:build",
    "start": "cross-env NODE_ENV=production node ./server.mjs"
  }
}

If you prefer, you can instead author your custom server in TypeScript. You could then use tools like tsx or tsm to run your custom server:

tsx ./server.ts
node --loader tsm ./server.ts

Just remember that there might be some noticeable slowdown for initial server startup if you do this.

Migrating Cloudflare Functions

The Remix Vite plugin only officially supports Cloudflare Pages which is specifically designed for fullstack applications, unlike Cloudflare Workers Sites. If you're currently on Cloudflare Workers Sites, refer to the Cloudflare Pages migration guide.

πŸ‘‰ In your Vite config, add "workerd" and "worker" to Vite's ssr.resolve.externalConditions option and add the Cloudflare Remix preset

import {
  unstable_vitePlugin as remix,
  unstable_cloudflarePreset as cloudflare,
} from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  ssr: {
    resolve: {
      externalConditions: ["workerd", "worker"],
    },
  },
  plugins: [
    remix({
      presets: [cloudflare()],
    }),
  ],
});

Your Cloudflare app may be setting the the Remix Config server field to generate a catch-all Cloudflare Function. With Vite, this indirection is no longer necessary. Instead, you can author a catch-all route directly for Cloudflare, just like how you would for Express or any other custom servers.

πŸ‘‰ Create a catch-all route for Remix

import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";

// @ts-ignore - the server build file is generated by `remix vite:build`
import * as build from "../build/server";

export const onRequest = createPagesFunctionHandler({
  build,
  getLoadContext: (context) => ({ env: context.env }),
});

While you'll mostly use Vite during development, you can also use Wrangler to preview and deploy your app. To learn more, see Cloudflare > Vite & Wrangler.

πŸ‘‰ Update your package.json scripts

{
  "scripts": {
    "dev": "remix vite:dev",
    "build": "remix vite:build",
    "preview": "wrangler pages dev ./build/client",
    "deploy": "wrangler pages deploy ./build/client"
  }
}

Migrate references to build output paths

When using the existing Remix compiler's default options, the server was compiled into build and the client was compiled into public/build. Due to differences with the way Vite typically works with its public directory compared to the existing Remix compiler, these output paths have changed.

πŸ‘‰ Update references to build output paths

  • The server is now compiled into build/server by default.
  • The client is now compiled into build/client by default.

For example, to update the Dockerfile from the Blues Stack:

-COPY --from=build /myapp/build /myapp/build
-COPY --from=build /myapp/public /myapp/public
+COPY --from=build /myapp/build/server /myapp/build/server
+COPY --from=build /myapp/build/client /myapp/build/client

Configure path aliases

The Remix compiler leverages the paths option in your tsconfig.json to resolve path aliases. This is commonly used in the Remix community to define ~ as an alias for the app directory.

Vite does not provide any path aliases by default. If you were relying on this feature, you can install the vite-tsconfig-paths plugin to automatically resolve path aliases from your tsconfig.json in Vite, matching the behavior of the Remix compiler:

πŸ‘‰ Install vite-tsconfig-paths

npm install -D vite-tsconfig-paths

πŸ‘‰ Add vite-tsconfig-paths to your Vite config

import { unstable_vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [remix(), tsconfigPaths()],
});

Remove @remix-run/css-bundle

Vite has built-in support for CSS side effect imports, PostCSS and CSS Modules, among other CSS bundling features. The Remix Vite plugin automatically attaches bundled CSS to the relevant routes.

The @remix-run/css-bundle package is redundant when using Vite since its cssBundleHref export will always be undefined.

πŸ‘‰ Uninstall @remix-run/css-bundle

npm uninstall @remix-run/css-bundle

πŸ‘‰ Remove references to cssBundleHref

- import { cssBundleHref } from "@remix-run/css-bundle";
  import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

  export const links: LinksFunction = () => [
-   ...(cssBundleHref
-     ? [{ rel: "stylesheet", href: cssBundleHref }]
-     : []),
    // ...
  ];

If a route's links function is only used to wire up cssBundleHref, you can remove it entirely.

- import { cssBundleHref } from "@remix-run/css-bundle";
- import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

- export const links: LinksFunction = () => [
-   ...(cssBundleHref
-     ? [{ rel: "stylesheet", href: cssBundleHref }]
-     : []),
- ];

Fix up CSS imports

In Vite, CSS files are typically imported as side effects.

During development, Vite injects imported CSS files into the page via JavaScript, and the Remix Vite plugin will inline imported CSS alongside your link tags to avoid a flash of unstyled content. In the production build, the Remix Vite plugin will automatically attach CSS files to the relevant routes.

This also means that in many cases you won't need the links function export anymore.

Since the order of your CSS is determined by its import order, you'll need to ensure that your CSS imports are in the same order as your links function.

πŸ‘‰ Convert CSS imports into side effects β€” in the same order they were in your links function!

- import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

- import dashboardStyles from "./dashboard.css?url";
- import sharedStyles from "./shared.css?url";
+ // ⚠️ NOTE: The import order has been updated
+ //   to match the original `links` function!
+ import "./shared.css";
+ import "./dashboard.css";

- export const links: LinksFunction = () => [
-   { rel: "stylesheet", href: sharedStyles },
-   { rel: "stylesheet", href: dashboardStyles },
- ];

While Vite supports importing static asset URLs via an explicit ?url query string, which in theory would match the behavior of the existing Remix compiler when used for CSS files, there is a known Vite issue with ?url for CSS imports. This may be fixed in the future, but in the meantime you should exclusively use side effect imports for CSS.

Optionally scope regular CSS

If you were using Remix's regular CSS support, one important caveat to be aware of is that these styles will no longer be mounted and unmounted automatically when navigating between routes during development.

As a result, you may be more likely to encounter CSS collisions. If this is a concern, you might want to consider migrating your regular CSS files to CSS Modules or using a naming convention that prefixes class names with the corresponding file name.

Enable Tailwind via PostCSS

If your project is using Tailwind CSS, you'll first need to ensure that you have a PostCSS config file which will get automatically picked up by Vite. This is because the Remix compiler didn't require a PostCSS config file when Remix's tailwind option was enabled.

πŸ‘‰ Add PostCSS config if it's missing, including the tailwindcss plugin

export default {
  plugins: {
    tailwindcss: {},
  },
};

If your project already has a PostCSS config file, you'll need to add the tailwindcss plugin if it's not already present. This is because the Remix compiler included this plugin automatically when Remix's tailwind config option was enabled.

πŸ‘‰ Add the tailwindcss plugin to your PostCSS config if it's missing

export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

πŸ‘‰ Convert Tailwind CSS import to a side effect

If you haven't already, be sure to convert your CSS imports to side effects.

// Don't export as a link descriptor:
- import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

- import tailwind from "./tailwind.css";

- export const links: LinksFunction = () => [
-   { rel: "stylesheet", href: tailwind },
- ];

// Import as a side effect instead:
+ import "./tailwind.css";

Add Vanilla Extract plugin

If you're using Vanilla Extract, you'll need to set up the Vite plugin.

πŸ‘‰ Install the official Vanilla Extract plugin for Vite

npm install -D @vanilla-extract/vite-plugin

πŸ‘‰ Add the Vanilla Extract plugin to your Vite config

import { unstable_vitePlugin as remix } from "@remix-run/dev";
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [remix(), vanillaExtractPlugin()],
});

Add MDX plugin

If you're using MDX, since Vite's plugin API is an extension of the Rollup plugin API, you should use the official MDX Rollup plugin:

πŸ‘‰ Install the MDX Rollup plugin

npm install -D @mdx-js/rollup

The Remix plugin expects to process JavaScript or TypeScript files, so any transpilation from other languages β€” like MDX β€” must be done first. In this case, that means putting the MDX plugin before the Remix plugin.

πŸ‘‰ Add the MDX Rollup plugin to your Vite config

import mdx from "@mdx-js/rollup";
import { unstable_vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [mdx(), remix()],
});
Add MDX frontmatter support

The Remix compiler allowed you to define frontmatter in MDX. If you were using this feature, you can achieve this in Vite using remark-mdx-frontmatter.

πŸ‘‰ Install the required Remark frontmatter plugins

npm install -D remark-frontmatter remark-mdx-frontmatter

πŸ‘‰ Pass the Remark frontmatter plugins to the MDX Rollup plugin

import mdx from "@mdx-js/rollup";
import { unstable_vitePlugin as remix } from "@remix-run/dev";
import remarkFrontmatter from "remark-frontmatter";
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    mdx({
      remarkPlugins: [
        remarkFrontmatter,
        remarkMdxFrontmatter,
      ],
    }),
    remix(),
  ],
});

In the Remix compiler, the frontmatter export was named attributes. This differs from the frontmatter plugin's default export name of frontmatter. Although it's possible to configure the frontmatter export name, we recommend updating your app code to use the default export name instead.

πŸ‘‰ Rename MDX attributes export to frontmatter within MDX files

  ---
  title: Hello, World!
  ---

- # {attributes.title}
+ # {frontmatter.title}

πŸ‘‰ Rename MDX attributes export to frontmatter for consumers

  import Component, {
-   attributes,
+   frontmatter,
  } from "./posts/first-post.mdx";
Define types for MDX files

πŸ‘‰ Add types for *.mdx files to env.d.ts

/// <reference types="@remix-run/node" />
/// <reference types="vite/client" />

declare module "*.mdx" {
  let MDXComponent: (props: any) => JSX.Element;
  export const frontmatter: any;
  export default MDXComponent;
}
Map MDX frontmatter to route exports

The Remix compiler allowed you to define headers, meta and handle route exports in your frontmatter. This Remix-specific feature is obviously not supported by the remark-mdx-frontmatter plugin. If you were using this feature, you should manually map frontmatter to route exports yourself:

πŸ‘‰ Map frontmatter to route exports for MDX routes

---
meta:
  - title: My First Post
  - name: description
    content: Isn't this awesome?
headers:
  Cache-Control: no-cache
---

export const meta = frontmatter.meta;
export const headers = frontmatter.headers;

# Hello World

Note that, since you're explicitly mapping MDX route exports, you're now free to use whatever frontmatter structure you like.

---
title: My First Post
description: Isn't this awesome?
---

export const meta = () => {
  return [
    { title: frontmatter.title },
    {
      name: "description",
      content: frontmatter.description,
    },
  ];
};

# Hello World
Update MDX filename usage

The Remix compiler also provided a filename export from all MDX files. This was primarily designed to enable linking to collections of MDX routes. If you were using this feature, you can achieve this in Vite via glob imports which give you a handy data structure that maps file names to modules. This makes it much easier to maintain a list of MDX files since you no longer need to import each one manually.

For example, to import all MDX files in the posts directory:

const posts = import.meta.glob("./posts/*.mdx");

This is equivalent to writing this by hand:

const posts = {
  "./posts/a.mdx": () => import("./posts/a.mdx"),
  "./posts/b.mdx": () => import("./posts/b.mdx"),
  "./posts/c.mdx": () => import("./posts/c.mdx"),
  // etc.
};

You can also eagerly import all MDX files if you'd prefer:

const posts = import.meta.glob("./posts/*.mdx", {
  eager: true,
});

Debugging

You can use the NODE_OPTIONS environment variable to start a debugging session:

NODE_OPTIONS="--inspect-brk" npm run dev`

Then you can attach a debugger from your browser. For example, in Chrome you can open up chrome://inspect or click the NodeJS icon in the dev tools to attach the debugger.

vite-plugin-inspect

vite-plugin-inspect shows you each how each Vite plugin transforms your code and how long each plugin takes.

Performance

Remix includes a --profile flag for performance profiling.

remix vite:build --profile

When running with --profile, a .cpuprofile file will be generated that can be shared or upload to speedscope.app to for analysis.

You can also profile in dev by pressing p + enter while the dev server is running to start a new profiling session or stop the current session. If you need to profile dev server startup, you can also use the --profile flag to initialize a profiling session on startup:

remix vite:dev --profile

Remember that you can always check the Vite performance docs for more tips!

Bundle analysis

To visualize and analyze your bundle, you can use the rollup-plugin-visualizer plugin:

import { unstable_vitePlugin as remix } from "@remix-run/dev";
import { visualizer } from "rollup-plugin-visualizer";

export default defineConfig({
  plugins: [
    remix(),
    // `emitFile` is necessary since Remix builds more than one bundle!
    visualizer({ emitFile: true }),
  ],
});

Then when you run remix vite:build, it'll generate a stats.html file in each of your bundles:

build
β”œβ”€β”€ client
β”‚   β”œβ”€β”€ assets/
β”‚   β”œβ”€β”€ favicon.ico
β”‚   └── stats.html πŸ‘ˆ
└── server
    β”œβ”€β”€ index.js
    └── stats.html πŸ‘ˆ

Open up stats.html in your browser to analyze your bundle.

Troubleshooting

Check the debugging and performance sections for general troubleshooting tips. Also, see if anyone else is having a similar problem by looking through the known issues with the remix vite plugin on github.

HMR

If you are expecting hot updates but getting full page reloads, check out our discussion on Hot Module Replacement to learn more about the limitations of React Fast Refresh and workarounds for common issues.

ESM / CJS

Vite supports both ESM and CJS dependencies, but sometimes you might still run into issues with ESM / CJS interop. Usually, this is because a dependency is not properly configured to support ESM. And we don't blame them, its really tricky to support both ESM and CJS properly.

For a walkthrough of fixing an example bug, check out πŸŽ₯ How to Fix CJS/ESM Bugs in Remix.

To diagnose if one of your dependencies is misconfigured, check publint or Are The Types Wrong. Additionally, you can use the vite-plugin-cjs-interop plugin smooth over issues with default exports for external CJS dependencies.

Finally, you can also explicitly configure which dependencies to bundle into your server bundled with Vite's ssr.noExternal option to emulate the Remix compiler's serverDependenciesToBundle with the Remix Vite plugin.

Server code errors in browser during development

If you see errors in the browser console during development that point to server code, you likely need to explicitly isolate server-only code. For example, if you see something like:

Uncaught ReferenceError: process is not defined

Then you'll need to track down which module is pulling in dependencies that except server-only globals like process and isolate code either in a separate .server module or with vite-env-only. Since Vite uses Rollup to treeshake your code in production, these errors only occur in development.

Plugin usage with other Vite-based tools (e.g. Vitest, Storybook)

The Remix Vite plugin is only intended for use in your application's development server and production builds. While there are other Vite-based tools such as Vitest and Storybook that make use of the Vite config file, the Remix Vite plugin has not been designed for use with these tools. We currently recommend excluding the plugin when used with other Vite-based tools.

For Vitest:

import { unstable_vitePlugin as remix } from "@remix-run/dev";
import { defineConfig, loadEnv } from "vite";

export default defineConfig({
  plugins: [!process.env.VITEST && remix()],
  test: {
    environment: "happy-dom",
    // Additionally, this is to load ".env.test" during vitest
    env: loadEnv("test", process.cwd(), ""),
  },
});

For Storybook:

import { unstable_vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";

const isStorybook = process.argv[1]?.includes("storybook");

export default defineConfig({
  plugins: [!isStorybook && remix()],
});

Alternatively, you can use separate Vite config files for each tool. For example, to use a Vite config specifically scoped to Remix:

remix vite:dev --config vite.config.remix.ts

Styles disappearing in development when document remounts

When React is used to render the entire document (as Remix does) you can run into issues when elements are dynamically injected into the head element. If the document is re-mounted, the existing head element is removed and replaced with an entirely new one, removing any style elements that Vite injects during development.

This is a known React issue that is fixed in their canary release channel. If you understand the risks involved, you can pin your app to a specific React version and then use package overrides to ensure this is the only version of React used throughout your project. For example:

{
  "dependencies": {
    "react": "18.3.0-canary-...",
    "react-dom": "18.3.0-canary-..."
  },
  "overrides": {
    "react": "18.3.0-canary-...",
    "react-dom": "18.3.0-canary-..."
  }
}

For reference, this is how Next.js treats React versioning internally on your behalf, so this approach is more widely used than you might expect, even though it's not something Remix provides as a default.

It's worth stressing that this issue with styles that were injected by Vite only happens in development. Production builds won't have this issue since static CSS files are generated.

In Remix, this issue can surface when rendering alternates between your root route's default component export and its ErrorBoundary and/or HydrateFallback exports since this results in a new document-level component being mounted.

It can also happen due to hydration errors since it causes React to re-render the entire page from scratch. Hydration errors can be caused by your app code, but they can also be caused by browser extensions that manipulate the document.

This is relevant for Vite becauseβ€”during developmentβ€”Vite transforms CSS imports into JS files that inject their styles into the document as a side-effect. Vite does this to support lazy-loading and HMR of static CSS files.

For example, let's assume your app has the following CSS file:

* { margin: 0 }

During development, this CSS file will be transformed into the following JavaScript code when imported as a side effect:

import {createHotContext as __vite__createHotContext} from "/@vite/client";
import.meta.hot = __vite__createHotContext("/app/styles.css");
import {updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle} from "/@vite/client";
const __vite__id = "/path/to/app/styles.css";
const __vite__css = "*{margin:0}"
__vite__updateStyle(__vite__id, __vite__css);
import.meta.hot.accept();
import.meta.hot.prune(()=>__vite__removeStyle(__vite__id));

This transformation is not applied to production code, which is why this styling issue only affects development.

Wrangler errors in development

When using Cloudflare Pages, you may encounter the following error from wrangler pages dev:

ERROR: Your worker called response.clone(), but did not read the body of both clones.
This is wasteful, as it forces the system to buffer the entire response body
in memory, rather than streaming it through. This may cause your worker to be
unexpectedly terminated for going over the memory limit. If you only meant to
copy the response headers and metadata (e.g. in order to be able to modify
them), use `new Response(response.body, response)` instead.

This is a known issue with Wrangler.

Acknowledgements

Vite is an amazing project, and we're grateful to the Vite team for their work. Special thanks to Matias Capeletto, Arnaud BarrΓ©, and Bjorn Lu from the Vite team for their guidance.

The Remix community was quick to explore Vite support, and we're grateful for their contributions:

Finally, we were inspired by how other frameworks implemented Vite support:

We're definitely late to the Vite party, but we're excited to be here now!

Docs and examples licensed under MIT