v0.15.0 Release Notes

This release introduces an experimental new compiler based on esbuild, which is much faster than Rollup. In our tests, we've found the new compiler to be anywhere from 20-100x faster than the current compiler.

To help with migrating to the new compiler, this release ships with both our current compiler and the new one. So you can upgrade your app to Remix 0.15 and run it on the current compiler, then try out the new compiler and see what breaks. We think you'll enjoy the speed improvements so much that you'll want to switch as soon as possible. The time you spend making the migration will easily be worth the time you won't spend waiting on future builds 😅

There are also a few minor changes to a few of our packages that are going to help us get onto more platforms in the future. Right now Remix runs only on node. But as more JavaScript runtimes appear on different cloud providers (hello Cloudflare Workers!) we are going to run Remix on all of them.

Upgrading Summary

To upgrade from Remix 0.14:

  • Replace @remix-run/data with @remix-run/node in your package.json dependencies
    • npm remove @remix-run/data
    • npm add @remix-run/node
  • Change all @remix-run/data imports to @remix-run/node
  • Remove deep imports from @remix-run/react:
    • In app/entry.client.js, change import Remix from "@remix-run/react/browser" to import { RemixBrowser as Remix } from "remix"
    • In app/entry.server.js, change import Remix from "@remix-run/react/server" to import { RemixServer as Remix } from "remix"

Please see the "Notes on Package Changes" section below for background on these changes.

As always, remember to use the starters as your guide. We currently have starter repos for:

Using the New Compiler

Note: Before you attempt this, please be sure you follow the notes in the Upgrading Summary section (above) first! Once you get your app working with the current compiler, come back here and try to use the new one.

This release ships with a new experimental compiler based on esbuild. We have been blown away by how fast esbuild is, and we think you're going to really enjoy the speed improvements too. However, there are some changes you'll need to make to your app in order to use it.

The first thing you'll need to change is the CLI command you use to invoke the compiler. In this release, remix build will still invoke the old compiler and remix build2 will invoke the new one. Eventually remix build2 will become remix build and we'll remove the old compiler entirely. This is the same for remix run and remix run2. You can also use remix watch2 if you just want to run the build in watch mode without firing up the dev server.

The new compiler does not support:

  • .client.js and .server.js file extensions
  • url:, css:, and img: imports
  • .mdx route modules

I know that's a lot to drop on you in a single release, but that's why we're calling this compiler "experimental"! Please allow me to explain in greater detail how we are thinking about each of these features.

Client/server-only Modules

In v0.13 we introduced the .client.js and .server.js file naming scheme for manually hinting to the compiler which files to include in the build. We used these hints as a way to try and speed up the compiler by giving hints to Rollup about which files it needed to include in the build, but esbuild is so fast that we don't need them anymore.

We still rely on tree-shaking to get rid of server-only code in your client bundles, so that hasn't changed. The main difference is that esbuild uses the sideEffects: false flag in package.json to determine whether a module has side effects or not. This is a webpack-ism that has been around for a few years now, so it should already be supported in all the packages you need in your app. However, you will need to add that field to your app's package.json in order for esbuild to eliminate your server-only modules from the client bundles.

Add this to your app's package.json:

  "sideEffects": false

If you have browser-only code, instead of using a .client.js file extension, you can make sure it won't run on the server using a traditional typeof window guard:

if (typeof window !== "undefined") {
  // browser-only stuff goes here

URL, CSS, and Image Imports

The new compiler includes support for importing many different types of files as URLs including SVG, fonts, CSS, and images. When you import a file, it will be copied to the build output and you will get a cache-able (hashed) URL to the asset.

import logoImageUrl from "../images/logo.png";

function Logo() {
  return <img src={logoImageUrl} />;

We are still working out how we'd like to support CSS and images. esbuild includes several different loaders for different content types, and they are working on native support for CSS and CSS modules, so we are following that work closely to see how it pans out.

As for images, we are still working out how we can best support all the different sources of images without slowing down the compiler. There are many different sources for images, but the img: import strategy only really works for image files that are stored alongside your source code. That won't be the case if you have a lot of images or if they are generated by users. So we are rethinking how to best handle these in the new compiler. There are also many different ways to serve images. Services like Akamai and Cloudinary are popular choices for hosting images, and they make it really easy to generate different formats. CDNs like Fastly and Cloudflare also have image optimization capabilities, and it's also very compelling to be able to build them on the fly as needed instead of building it into the build step of your app.

For now, when you import a .css or image file using the new compiler, you'll get a hashed URL to that file. We will also be writing up some detailed guides about the various strategies for handling CSS and images in Remix apps using the new compiler. The guide to using PostCSS in Remix is already a good start.

MDX Route Modules

The new compiler does not support using .mdx files as route modules. We are planning on re-introducting first-class support for .mdx files as route modules as soon as possible.

In the mean time, if you're using MDX one project you might be interested in is Kent C. Dodds' mdx-bundler. It's a fast tool (also based on esbuild 🙌) that will bundle up your MDX for you and give you the code you need to actually render your component. You could possibly move your MDX out of the app/routes directory and into some other directory like app/pages. The new compiler will give you the text of the file (instead of a URL) when you import text from "../pages/something.mdx", so you could do something like this:

import { bundleMDX } from "mdx-bundler";
import { getMDXComponent } from "mdx-bundler/client";
import { useRouteData } from "remix";

// You can get the MDX from your own filesystem. But if you have a lot
// of content you're probably going to get this from a database somewhere.
import text from "../pages/something.mdx";

export async function loader() {
  // You could load MDX from a database here!
  // let text = await getText("https://github.com/my/repo/posts/something.mdx");
  return await bundleMDX(text);

export default function MyPage() {
  let { code } = useRouteData();
  let Component = React.useMemo(() => getMDXComponent(code), [code]);
  return <Component />;

We believe this is a great way to handle MDX; as content instead of source code. Now you don't have to slow down your build compiling a bunch of MDX files!


Aaaaaaand that should be it if you'd like to try out the new compiler. Please let us know if we missed something so we can add it to these notes. And please let us know how it goes so we can continue to improve the new compiler!

Notes on Package Changes

In the last release (0.14) and this one some packages have moved around, and I thought it'd be nice to put a few notes here about how we are thinking about structuring our packages going forward so it's clear why we are making these changes. Hopefully this will clarify how we are thinking about supporting Cloudflare Workers in the near future as well.

We currently ship three separate packages that run on node: @remix-run/architect, @remix-run/express, and @remix-run/vercel. Each package shares a common dependency, @remix-run/node, and exports a createRequestHandler function that is suited for working with that particular provider's HTTP server API.

In this release, we eliminated the @remix-run/data package and elevated @remix-run/node to an app-level dependency (in 0.14 it was a transitive dependency called @remix-run/core) to more accurately reflect its target runtime. So if you're building a node app with Remix, your app-level dependencies are:

  • @remix-run/node (the "environment" you're running in)
  • Your "provider" (currently @remix-run/architect, @remix-run/express, or @remix-run/vercel)
  • @remix-run/react

We will follow this same pattern to support other JS runtimes in the near future. So e.g. if you're running your app in Cloudflare Workers, the packages you'll need will be (names may change):

  • @remix-run/service-worker
  • @remix-run/cloudflare-workers
  • @remix-run/react

Hopefully that clarifies how we are thinking about structuring dependencies going forward! Thanks for your patience as we work this out.