v0.12.0 Release Notes

Automatic Performance Optimizations

In the 90's we were told to not go chasing waterfalls. It's great advice for modern web development also.

A request waterfall happens when fetching one resource leads to fetching another resource. If a website imports your root route, and your root route imports React, you have a waterfall. The browser doesn't know you want React until it has already download the root route. If we could fetch both modules in parallel, we'd drastically reduce the amount of time it takes to download all the resources we need.

Remix now uses <link rel="modulepreload"> on all the scripts the page needs, automatically, to prevent these waterfalls. For example, in one of our demo apps we're building, the bottom of the document now has these links.

<link rel="modulepreload" href="/build/_shared/react-2daf095e.js" />
<link rel="modulepreload" href="/build/_shared/react-dom-1e9b93b6.js" />
<link rel="modulepreload" href="/build/_shared/__remix-run/react-624064ed.js" />
<link rel="modulepreload" href="/build/_shared/object-assign-510802f4.js" />
<link rel="modulepreload" href="/build/_shared/scheduler-5591ac82.js" />
<link rel="modulepreload" href="/build/_shared/history-e6417d88.js" />
<link rel="modulepreload" href="/build/_shared/__babel/runtime-88c72f87.js" />
<link rel="modulepreload" href="/build/_shared/react-router-4449037e.js" />
<link rel="modulepreload" href="/build/root-4ac7c97d.js" />
<link rel="modulepreload" href="/build/routes/login-538f9d25.js" />

Previously, we only imported "root-4ac7c97d.js" and "login-538f9d25.js", and then an import waterfall for everything else began: root downloaded and required react, react downloaded and required scheduler, scheduler downloaded and required object assign, etc.

Now, the browser has already started downloading all of the dependencies for the entire page in parallel, greatly reducing the amount of time it takes for the page to hydrate.

Additionally, on script transitions (transitions with <Link> not <a href>), Remix will likewise modulepreload all of the scripts for the next page in parallel with the data. In typical React apps, you fetch data, render, fetch code-split bundles, render, fetch data, render, fetch code split bundles, render, etc. Causing even more waterfalls. Because of nested routes, we can download all of the bundles and all of the data for all of the matching routes in parallel. This prevents both render/fetch waterfalls as well as module import waterfalls!

url: imports

Like last release's image imports, you can now import any file you want, Remix will emit that asset to your browser build directory and return the url to your app to use anywhere.

For example, you can import css like this:

import stylesUrl from "url:./styles/some-style.css";

// `stylesUrl` will be a string like "/public/build/styles/something-2ac45cffe9.css"

You can import any kind of file you want, Remix will simply emit the asset and give it a fingerprint in production based on the content of the file to make it easier to cache.

Keep reading to see where you'll likely use this.

Like a route module meta export, you can now export a links function that tells Remix which links to add to the document when this route is active, and which to remove when it's not.

In your root route, render the <Links> element by the <Meta> element, and then in any route export a links function and Remix will put them in the document.

import type { LinksFunction } from "remix";
import { Links, Meta, Scripts } from "remix";
import { Outlet } from "react-router-dom";

// New!
export let links: LinksFunction = () => {
  return [{ rel: "icon", href: "/favicon.png" }];
};

export default function Root() {
  return (
    <html>
      <head>
        <Meta />
        <Links /> {/* <-- New! */}
      </head>
      <body>
        <Outlet />
        <Scripts />
      </body>
    </html>
  );
}

These links become <link> tags, so any properties on your object become properties on the element.

You can use it along with the recent image import feature:

import favicon from "img:./favicon.png?width=32&height=32";

export let links: LinksFunction = () => {
  return [
    {
      rel: "icon",
      href: favicon.src,
      type: `image/${favicon.format}`
    }
  ];
};

The links function receives the route's data so you can dynamically prefetch stuff, we'll see an example later, but the signature is simple:

export let links: LinksFunction = ({ data }) => {
  // `data` is your route loader's data
};

There is a LOT you can do with the links export, make sure to read the links docs after these release notes.

CSS files in the routes folder are no longer included automatically, we've got something better.

Instead of cluttering up your routes folder with css files, we can use the url: imports and links instead.

import type { LinksFunction } from "remix";
import styles from "url:../styles/login.css";

export let links: LinksFunction = () => {
  return [{ rel: "stylesheet", href: styles }];
};

This gives you more control over what css ends up on the page, and allows you to re-use css across unrelated routes. (It also sets us up for adding css preprocessing features directly into remix with a css: import assertion coming soon.)

For example, what if you wanted to load different css for a route depending on the device size?

import type { LinksFunction } from "remix";
import mobile from "url:../styles/login-mobile.css";
import desktop from "url:../styles/login-desktop.css";

export let links: LinksFunction = () => {
  return [
    { rel: "stylesheet", href: mobile, media: "(max-width: 764px)" },
    { rel: "stylesheet", href: desktop, media: "(min-width: 765px)" }
  ];
};

You don't even need the media queries in your CSS, only ship the styles they're going to use! This was simply not possible with conventional route CSS files.

Prefetching other pages for faster transitions

Some React frameworks automatically download all the JavaScript--and in the case of SSG, all of the data pre-rendered inside the JavaScript--for every link on the page. This is why when you go to a very boring page built with one of these tools your network tab shows 7 megabytes of JavaScript on the page. It downloaded half of the entire website.

We don't want to do that to your user's data plan but we do think it's a great strategy to speed up links to pages the user is likely to visit next.

What do we mean by "likely"? Some examples:

  • User is on the login page, it's likely they'll end up at the dashboard, so prefetch the resources for the dashboard.
  • User is on the shopping cart page, it's likely they'll end up on the checkout page next.
  • User is on an index page of a list of invoices, it's likely they'll end up on an invoice page.

So instead of downloading the code for 20 pages in your footer, you get to choose which transitions you'd like to optimize.

Browsers have a built-in way to prefetch resources, and prime the browser cache, before you use them: <link>. The way to prefetch a page in Remix is the new links export with a special kind of link descriptor (that's what we call them):

Prefetching a page's resources

Let's take the login → dashboard example:

import type { LinksFunction } from "remix";
import styles from "url:../styles/login.css";

export let links: LinksFunction = () => {
  return [
    { rel: "stylesheet", href: mobile, media: "(max-width: 764px)" },
    { page: "/dashboard" } // <-- that's it!
  ];
};

By using page on your link descriptor, Remix will match that route and explode your little {page: "/dashboard" } descriptor into all of the <link rel="prefetch" as="script" href={asset} /> tags the user will need when they get to the dashboard. On one of our demo apps it turns into 24 <link> tags!

These kinds of optimizations would be really hard to do by hand, we're pumped we've got all the right pieces to do it for you.

You can even prefetch the data for the next page with { page: "/users/123", data: true }. You'll prefetch all the JavaScript assets as well as all the loader data for that page. In addition to the script prefetches, Remix will add <link rel="prefetch" as="fetch" href={dataLoaderUrl} /> for any of the routes will need to fetch data. This primes the browser cache so that when the user clicks, the browser can read from the cache. And since it's just using browser features, if the user clicks the link long after the cache headers on the prefetch have expired then the browser will refetch. Nothing special about Remix, that's just how prefetch links work!

Be careful with this feature. You don't want to download 10MB of JavaScript and data for page the user probably won't ever visit.

Blocking Transitions on Preloads

You may have heard of FOUC (flash of unstyled content) and CLS (cumulative layout shift). This happens any time the user visits a page and the styles swap around or the layout shifts all over the place. We'll just call both concepts "jank" for now.

Browsers, by default, wait for the CSS for a page to download before they render the document to prevent jank. But when it comes to script transitions (with client side routing) it's easy to introduce jank because the browser can't know which resources to block and which not to.

With Remix, you can specify which preloaded resources for a page should block a script transition before rendering the next page. Check it out:

import type { LinksFunction } from "remix";
import { block } from "remix";

export let links: LinksFunction = () => {
  return [
    // will not block the transition, will just tell the browser to start
    // downloading this as soon as possible, even before the image tag is
    // rendered
    { rel: "preload", as: "image", href: "/some/image.jpg" },

    // Remix *will* block the transition to this page before rendering
    block({ rel: "preload", as: "image", href: "/some/other/image.jpg" })
  ];
};

If the user clicks a link to this page, Remix will look at the next page's links, find any blocking links, and wait for those resources to download before finishing the transition. This way you can wait for images to load and avoid a bunch of jank.

This is most useful when you don't know the image sizes ahead of time.

A few things to note:

  • You can only block { rel: "preload" } links.
  • You don't need to block { rel: "preload", as: "style" } links, Remix automatically blocks any { rel: "stylesheet" } because that's what browsers do by default on normal document requests.
  • Remix can't block anything on normal document requests, so you'll still get some jank sometimes (like when you don't)

Here's an example that waits for the first five user's avatars before rendering the page:

import type { LinksFunction, LoaderFunction } from "remix";
import { block, useRouteData } from "remix";

export let loader: LoaderFunction = () => {
  return Users.getAll();
};

export let links: LinksFunction = ({ data }) => {
  return data.slice(0, 5).map(user => {
    return block({ rel: "preload", as: "image", href: user.avatarUrl });
  });
};

export default function Users() {
  let users = useRouteData();
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          <img src={user.avtarUrl} />
        </li>
      ))}
    </ul>
  );
}

When user clicks a link to this page, Remix will wait for the avatars to load and then transition to the page with the avatars ready to go.

Be careful with this feature. You rarely want to block a transition on images, but we're not your dads. It can make sense sometimes.

Upgrading

Move your stylesheets out of the "routes" folder

You can put them anywhere you want in the app folder. We recommend app/styles.

Export the stylesheets as links from your routes

  • import it with url:
  • return it from your route links export
import type { LinksFunction } from "remix";
import styles from "url:../styles/whatever.css";

let links: LinksFunction = () => {
  return [{ rel: "stylesheet", href: styles }];
};

Replace <Styles> with <Links>

In app/root.tsx:

import type { LinksFunction } from "remix";
import { Links, Meta, Scripts } from "remix";
import { Outlet } from "react-router-dom";

// grab your old global styles while you're at it
import styles from "url:./global.css";

export let links: LinksFunction = () => {
  // add your global styles to the page the new way
  return [{ rel: "stylesheet", href: styles }];
};

export default function Root() {
  return (
    <html>
      <head>
        <Meta />
        <Links /> {/* <-- Used to be <Styles />, now it's <Links /> */}
      </head>
      <body>
        <Outlet />
        <Scripts />
      </body>
    </html>
  );
}