Remix vs Next.js Comparison

I have been using Next for years, since the v1 was released, I even become an early contributor of the framework and joined Vercel for a year. Years to the future, Remix was announced and after reading some of their newsletters I purchased the indie license to start to play with it, published a few libraries to use it Remix and once it become Open Source I become a contributor.

Why I'm telling you that? Because I have used both, a lot, and I think that put me in a good position to compare them.

What is Next.js?

Next is a React Framework, it uses React as the view layer and build features on top of it.

What is Remix?

Remix is a Web Framework, it uses React as the view layer like Next, but it's not tied to React and it's more intended to be a full-stack web application framework.

Now, let's compare the features of both frameworks.

The Router

Because we are building a web application, the route is one of the most important parts of an application. It's what decide what to render based on the user URL.

In Next.js, they have their own router using the file system, so you create a pages folder and put files there

pages/
  index.js
  about.js
  contact.js

Those files are going to becomes pages inside the application, with the following URLs:

- / (this is index)
- /about
- /contact

They also have a useRouter hook to access data from the router like the search (query) params or methods like reload or push to navigate to another URL.

In Remix, they use React Router v6 internally but they provide a file system based system, instead of pages Remix call them routes, but the general is similar.

routes/
  index.js
  about.js
  contact.js

Those files are going to become routes with the same URLs as in Next. The main difference here comes with the introduction of Layout Routes.

Layout Routes

One common requirement of user interfaces is to re-use a layout between two URLs, a common example is to keep a header and footer on every page, but this can become more complex. A great example of this is Discord, let's analyze it quickly.

You can see at least four main areas

  1. The left column with the list of servers
  2. The next column with the list of channels
  3. The widest column with the list of messages
  4. The right column with the list of users of the server
  5. It's not on the image but you can have a list of messages for a thread replacing the users

If you wanted to build this UI in Next.js, you would need to create a file at pages/[serverId]/[channelId].tsx, get the data of each list and render a component like this:

function Screen() {
  return (
    <Layout>
      <Servers />
      <Channels />
      <Messages />
      <Users />
    </Layout>
  )
}

When the user navigate to another server or channel, based on the data loading strategy you used, you may need to get load everything again with the new channel or server. This is because Next doesn't have support for layout routes, so every page renders everything on the screen, including shared layouts between screens.

Contrary to Next.js, Remix has support for that, so in Remix we would create a file structure like this:

routes/
  __layout.tsx
  __layout/
    $serverId.tsx
    $serverId/
      index.tsx
      $channelId.tsx
      $channelId/
        index.tsx
        $thread.tsx

While you have more files, this will help you to keep the code more organized and to make loading data more optimized.

The __layout.tsx creates what Remix calls a Pathless Layout Route, this is a route component that works as a layout but without adding segments to the URL, so you will not see a /__layout in the URL, and instead you will go directly to the /:serverId part.

The $serverId.tsx creates a layout route with a dyanmic parameter serverId that can be used in the server to load the data of the route. The same goes for $channelId.tsx.

The index.tsx inside the $serverId folder will be used when the user goes to /:serverId without a channel ID, there we can render something special or we can redirect to a channel. The index.tsx inside the $channelId folder would be rendered when the user is not in a specific thread, in that case we can render the list of users of the channel.

Those files will generate the following routes:

/:serverId
/:serverId/:channelId
/:serverId/:channelId/:threadId

Now, each route file will be able to render parts of the UI.

  • The __layout could render the Layout component, load the list of servers and render it, also render an <Outlet /> to indicate where the nested routes will be placed in the UI.
  • The $serverId will load the data of the server, this is the list of channels and extra server info, then render them together with an <Outlet />.
  • The $channelId will load the data of the channel, this is the list of messages and extra channel info, then render them together with an <Outlet />.
  • The $channelId/index will load the data of the list of users of the channel and render it.
  • The $threadId will load the data of the thread and render it.

Aside of being able to have individual route files with their own data and UI, it has another benefit, if the user is on the URL /server-a/channel-a and navigates to /server-a/channel-b Remix knows that the server ID didn't changed and can get the data of the cannel without touching the server, so you can avoid loading data you already have.

To get the same behavior in Next.js you should move your data loading to be completely client-side so the component can re-use the data it already has, using something like SWR or React Query. So this is a huge performance benefit of using layout routes that you can't get as easy and with the same UX in Next.

Data Loading Strategies

In a web application, you can use different strategies to load the data your app needs, some are:

  1. Server-side at runtime
  2. Server-side at build time
  3. Client-side at runtime
  4. A mix of 1 and 3 or 2 and 3

In Next you can use all of them with different functions you can export from a page file.

  1. Use getServerSideProps to load the data server-side at runtime
  2. Use getStaticProps and getStaticPaths to load the data server-side at build time.

If you want to load data client-side Next let you do it however you want, some popular libraries are SWR, from the authors of Next, and React Query.

In Remix, they support only two strategies, server-side at runtime and client-side at runtime.

  1. Export a loader function from a route to load data server-side at runtime
  2. Use the useFetcher hook from Remix to load data client-side at runtime.

You can, of course, keep using SWR or React Query with Remix, the useFetcher however does a really good job to simplify the code and most of the time it's all you need.

Based on that, Next supports SSR (Server-Side Rendering), SSG and ISR (Static Site Generation and Incremental Site Regeneration) and CSR (Client-Side Rendering). While Remix supports only SSR and CSR.

SSR vs SSG

Next first versions only supported SSR and CSR, it wasn't until the release 9.3 when they added support for SSG, and later in 9.5 they added stable support for ISR. Since they added support for SSG and ISR the team started to pitch SSG as a solution for lots of websites and proposed to combine it with CSR for dynamic or provide data.

The idea, is to use SSG to generate a static site with the public information of a website and then use CSR to get the rest, and example of this is when you build an ecommerce you can generate a page for every product with the public data, and then using CSR get the user-specific data like their shopping cart.

The problem starts when your app needs to show more and more private or dynamic data, in a big enough e-commerce the price of the products may change during the day, the user may have coupons, there could be seasonal discounts, a product may have a different price depending on the user's location, or maybe a discount is only for a few units and once they are sold it should go back to the original.

Another example could be if you want to show suggestions to the user for related products, should they be the same for every user or should they differ? In the first scenario you could get the suggestions at build time, in the later which is the most common, you will need to do it client-side at runtime.

And for product listing pages, with search and filters, SSG is just not an option because generating every possible search plus filter combination is going to take a huge amount of time.

At that moment, you will need to make a decision, use CSR or SSR. If you go with the CSR way you will degrade the UX of your app adding lots of spinners or loading state, if you go with the SSR way you will get a better UX but now your TTFB (Time To First Byte) will be slower, however a fast TTFB is meaningless if the TTI (Time To Interactive) is high because you do lots of request on the client to get the data needed to show something useful for the user.

Because of that, Remix decided to go with only SSR, and not SSG, most apps will eventually need private data and if you have a truly static route you can cache it at the edge using a CDN.

Non UI Routes

Let's define a UI route as any route rendering HTML (a UI), and a non-UI route as the rest, in Next.js this are called API routes, which is probably the most common term used for them, in Remix they decided to call them Resource Routes.

Aside of what they expect the files to look like, they are intended to solve the same use case, your app may need some routes which are not sending HTML and instead return something else, typically they're used for sending JSON or in Next to receive a JSON from the browser and perform a mutation server-side (e.g. update a DB).

Remix decided to go with the name Resource Routes to differentiate from the idea they are only used for an API, you can use a resource route to send or receive JSON, but also send CSS, images, JS scripts, PDFs, etc. Anyway, you could do that with Next.js's API routes, it's just not the most common thing to do.

Data Mutations

Next.js comes with many ways to load the data but it doesn't have a built-in way to perform mutations, so you need to do it manually. This typicalle involves creating a form, attach an onSubmit, prevent the default behavior, get the inputs values (probably from React state or a Form management library like Formik or React Hooks Form), then send the data using the Fetch API to an API route, and of course add a few states to handle the loading state, store any error, etc.

Instead, with Remix you can use their built-in Form component, this component works as a normal Form, you decide if the method is GET or POST, you can add an action with a URL to send the data to which is optional and defaults to the same route fo the form, and put the inputs inside.

But now you don't need to do anything else, Remix takes care of sending the values from the form to the action with the correct method and keep a loading state, abort requests if the form is sent again, and way more things you may forgot to do manually, then with the useTransition hook you can know the status of the request.

The request can be sent to any route, not only resource routes, if the method is GET Remix will run the exported loader function, if the method is POST it will run the exported action function.

And, if you need to send a form programmatically, you can use the useFetcher hook or useSubmit hook, useful to send on every keystroke or after checkbox is clicked.

Sessions and Cookies

Sessions are used commonly to store the user authentication data, you can store a session in a cookie or in an external data store like a DB or the file system, then save a session ID on a cookie.

A cookie can be used to store the session data or session ID or for any other data outside the session you may need, it could be tracking, discount codes, etc.

In Next.js, you don't have anything special built-in to work with cookies or session, there are popular libraries like Cookie.js which can work with Next or NextAuth.js to do user authentication and store some session data in a cookie.

Remix comes with cookies and sessions support out of the box, you can create a cookie calling a function and then serialize data to store there or parse data to read it.

With session, you can choose between storing the session data on a cookie, in memory, on the file system, if you deploy to Cloudflare on Worker KV and you can create your own session storage to store the session data in your DB or somewhere else.

Deployment

Next.js can be deployed to any server where you can run Node.js by doing next build && next start, additionally it has built-in integration to run in serverless mode when deploying to Vercel, and the Netlify team created an adapter to deploy to their service in serverless mode. You could of course write an adapter to convert the platform request/response objects to Next.js request/response objects but it's not documented or common.

Remix was create with the idea to be deployable to any platform, and be integrated anywhere, that's why Remix is a request handler inside an HTTP server, so you can use any server. When you create a Remix app you are asked where you want to deploy and, at the moment of writing this, you have the following options:

  • Remix App Server
  • Express Server
  • Architect (AWS Lambda)
  • Fly.io
  • Netlify
  • Vercel
  • Cloudflare Pages

The first is a minimal Express server wrapped inside a package so you don't need to maintain it, the second gives you the server-code so you can control it yourself and add other things, useful to do framework migrations, or run an API using some other technology.

The rest of the options comes with code required to run Remix inside those platforms, you are not going to own the server because most of them are serverless. This is possible because Remix has server adapters to ensure the request/response objects of the server can be converted to the Request and Response objects used by Remix which are the standard ones used by Fetch API.

Styling

There's not too much to say about this from Next.js, it comes with styled-jsx as default CSS in JS solution and you can also use CSS Modules out of the box, any other framework or CSS in JS library can be added with some configuration or plugin quite simple.

Remix instead has an opinion, any CSS should be loaded with a <link /> tag because that helps with cacheability and you can use media queries to load or not a stylesheet based on that. And while CSS in JS still possible if the library needs some compiler plugin it will not be usable since it's not possible to change the compiler.

Disabling JavaScript

Next.js doesn't has stable support for disabling runtime JavaScript in a specific page, it only has a prefixed unstable API.

Remix lets you control if you want to add or note runtime JavaScript in your routes and has a documented approach to control it in a per route basis. This is useful to remove JS on static pages like a landing page and enable it back in application-like routes.

Link, Meta and other <head> tags

Next.js lets you add content to your page <head> by using the next/head component to wrap what you want to render there.

Remix lets you export a links and meta functions on any route module and return a list of links/metas to add a simple array of plain objects. It also has documentation on how to use PostCSS, TailwindCSS and any CSS in JS which doesn't require Babel. This functions will access your route data got in a loader function as a way to use it to define the content.

Page and Data Preload

Next.js automatically prefetch the runtime JavaScript required of any page linked in the current page using the next/link component. You can manually disable this in a per link basis.

Remix doesn't preload anything automatically, but it lets you return a list of assets to preload in the links function of any route module. Along the usual link with the rel="preload" you can add there a an object with the property page and a URL, then Remix will preload the runtime JavaScript and other assets that page will need, it's also possible to define data: true and then Remix will preload the data for the page along the assets for instant page navigation.

Remix also has support for client-side only blocking navigation assets, this lets you define which assets you are adding as links you want to ensure they are loaded by the user browser before completing the navigation to that page. This way you can block the navigation to a route until images or another asset is completely loaded. This last feature only works when navigating client-side, on the first render there's no way to enforce it.

Internationalized Routing

Next.js has built-in support for internationalized routing and can use popular libraries for content i18n.

Remix doesn't has built-in support for internationalized routing, however thank to the possibility to completely replace file system routing with something custom it's possible to add it.

Image Optimization

Next.js has built-in automatic image optimization support when using the next/image component.

Remix lets you import images and get a URL back or use the img: prefix in the import path and get the image URL and perform some simple transformation, algo get multiple URLs to use in srcset, a placeholder URL and change the quality.

Error Handling

Next.js lets you define some special pages for 500, 404 and any error, then renders that in case of an error.

Remix lets you export an ErrorBoundary component inside any route module which will handle any error of that specific route. If a route is nested it will only handle errors on that part, the parent route will continue to work.

Live Reload vs Fash Refresh

Next.js has support for React Fash Refresh wich will update your components as you save files without reloading the whole page and losing UI state.

Remix has support to enable Live Reload (not enabled by default) which will reload the page as you save files. This will cause the UI state to be lost but will allow you to easily see updates on loaders. Fast Refresh support is coming.

Zero Config

Next.js is zero config by default, you can still customize the webpack and Babel if needed, together with other parts of the internal stack.

Remix is also zero config by default, it comes with everything you need to run it after installing it.

TypeScript Support

Both, Next.js and Remix has built-in support for TypeScript without any special setup.

Environment Variables

Next.js comes with .env support out of the box, it also has a convention to prefix environment variables you want to expose to the client-side code. There's additionally a way to avoid the convention and manually expose a variable.

Remix doesn't comes with .env support out of the box too, and it exposes process.env.NODE_ENV to the client-side code, it has a guide in the docs on their recommended approach to expose environment variables to the client. Adding .env support is rather simple by using the dotenv package.

Automatic Polyfilling

Next.js automatically polyfill fetch, Object.assign and URL in servers so you can use it inside API routes, getStaticProps, getStaticPaths and getServerSideProps.

Remix automatically polyfill fetch in the server so you can use it inside your loaders and actions.

Google's AMP

Next.js has out of the box support for AMP, you can enable it in a per page basis.

Remix doesn't come with any special support for AMP.