A cleaner path to production for Remix apps

Remix, a server-rendered React framework built on the Web Fetch API, has always been portable across deployment providers. However, deploying it to serverless environments typically required glue code: a custom server.js file, a bespoke vercel.json, and runtime-specific adapter logic.

That setup is now a thing of the past. The Vercel integration (@vercel/remix) removes the need for a separate adapter, letting developers swap out @remix-run/node for @vercel/remix to handle runtime-specific concerns like cookies and streaming. The two teams are also collaborating to contribute changes back upstream to make Remix's build outputs more modular.

Per-route runtime selection

Previously, deploying a Remix application meant making an all-or-nothing choice: the entire app ran on either Node.js or a Web/Edge-oriented runtime. With the advanced integration, that decision is now made at the route level, allowing apps to use Node.js APIs where needed and Edge-oriented Web APIs elsewhere within the same application.

For existing projects on Vercel, the default is straightforward: stick with the Node.js runtime and use Vercel Functions for backend logic.

Leveraging the edge cache for loaders

One of the more compelling features is fine-grained control over caching for route loader responses using standard cache-control headers. Vercel's Edge Network Cache supports modern directives such as stale-while-revalidate, enabling background revalidation without blocking the user.

For instance, a cache-control header can instruct Vercel to:

  • Serve a cached value for repeated requests within the next second as fresh.
  • Mark that value as stale for requests between 1 and 60 seconds later, while still delivering it immediately.
  • Trigger a background revalidation to refresh the cache from the loader.

export function headers() {

return {

"Cache-Control": "s-maxage=1, stale-while-revalidate=59",

};

}

Adding such headers can significantly cut server-rendering latency. For added resilience, the stale-if-error directive lets the edge cache continue serving a stale response if the loader fails—something that keeps a site up during transient database or CMS outages.

export function headers() {

return {

"Cache-Control": "s-max-age=2592000, stale-while-revalidate=86400, stale-if-error=604800",

};

}

That example uses a max-age of 30 days, stale-while-revalidate for 1 day, and stale-if-error for 7 days. For developers who prefer a helper over raw strings, community contributor Jenna Smith's pretty-cache-header package can generate the same output more readably.

import { cacheHeader } from 'pretty-cache-header';

export function headers() {

return {

"Cache-Control": cacheHeader({

sMaxAge: '30days',

staleWhileRevalidate: '1day',

staleIfError: '7days'

})

};

}

Streaming SSR across runtimes

Streaming server-side rendering works uniformly on Vercel, regardless of whether the route runs on Node.js or the Edge. The integration auto-generates an app/entry.server.tsx file configured for streaming if none is present.

import { handleRequest } from '@vercel/remix';

import { RemixServer } from '@remix-run/react';

import type { EntryContext } from '@vercel/remix';

export default function (

request: Request,

responseStatusCode: number,

responseHeaders: Headers,

remixContext: EntryContext

) {

const remixServer = <RemixServer context={remixContext} url={request.url} />;

return handleRequest(

request,

responseStatusCode,

responseHeaders,

remixServer

);

}

Existing projects can also adopt this isomorphic entry point manually. This unlocks React 18's Suspense features within Remix, including defer() and the <Await> component. A demo illustrates the approach by delaying a Promise by one second to simulate a throttled network, then consuming the deferred value inside a React component.

import { Suspense } from 'react';

import { Await, useLoaderData } from '@remix-run/react';

export async function loader({ request }) {

const version = process.versions.node;

return defer({

version: sleep(version, 1000),

});

}

function sleep(val, ms) {

return new Promise((resolve) => setTimeout(() => resolve(val), ms));

}

export default function App() {

const { version } = useLoaderData();

return (

<Suspense fallback={'Loading…'}>

<Await resolve={version}>

{(version) => <strong>{version}</strong>}

</Await>

</Suspense>

);

}

The payoff is global speed. Even on a cache MISS, dynamic server-rendered pages frequently come in under 100ms of latency.

Framework-defined infrastructure

This integration relies on framework-defined infrastructure, meaning Remix applications automatically pick up Vercel Functions, edge caching, and streaming without extra configuration. The focus now is on extending the feature set further based on developer feedback.