https://remix.run/blog/remix-vs-next Blog Docs GitHub Remix and Next.js Logos January 11th, 2022 Remix vs Next.js [r] Ryan Florence Co-Founder Easily the biggest question we get asked is something like: How is Remix different from Next.js? It appears we have to answer this question! We'd like to address it directly and without drama. If you're a fan of Remix and want to start tweeting smug reactions to this article, we kindly ask that you drop the smugness before hitting the tweet button . A rising tide lifts all boats. We've been friends with folks at Vercel long before Vercel was founded. They are doing great work and we respect the work they do! But make no mistake, we think Remix has a better set of tradeoffs than Next.js. (Otherwise we wouldn't have built it...) We encourage you to read this entire article. There is a lot of nuance in this conversation that's left un-captured in the shiny graphs and animations. By the end, hopefully you'll consider Remix for your next project (no pun intended ). tl;dr * Remix is as fast or faster than Next.js at serving static content * Remix is faster than Next.js at serving dynamic content * Remix enables fast user experiences even on slow networks * Remix automatically handles errors, interruptions, and race conditions, Next.js doesn't * Next.js encourages client side JavaScript for serving dynamic content, Remix doesn't * Next.js requires client side JavaScript for data mutations, Remix doesn't * Next.js build times increase linearly with your data, Remix build times are nearly instant and decoupled from data * Next.js requires you to change your application architecture and sacrifice performance when your data scales * We think Remix's abstractions lead to better application code Background We figured the fairest way to compare the frameworks would be to take a Next.js example app that the Vercel team wrote themselves. Since they wrote it, the decisions they made should reflect how they intend you to build your app. It should also show off the features the Vercel team is most proud of. We ported the Commerce Example from the Next.js examples page. It has a handful of real world features we liked, and seems to be the one they put the most effort into. * Initial page load is critical for ECommerce * Dynamic data on the search page * Data mutations with the shopping cart * Ability to integrate with multiple providers that illustrates how the frameworks help you abstract We actually built two versions: * Minimal Port - We simply copy/pasted/tweaked the Next.js code to run on Remix instead of Next.js. This is deployed to Vercel just like the Next.js Demo. This is a great comparison of frameworks because everything but the framework is the same. * Rewrite - The two frameworks don't actually have a lot of API overlap and Remix can run on different infrastructure than Next.js. To really exercise Remix's design, we rewrote the example into idiomatic Remix, and even built a quick image optimization route into the app so it's 100% Remix. Note that this app doesn't get to exercise everything we think is cool about Remix (like nested routes!). Once we answer this question, we can move on to just talking about Remix, so stay tuned! Additionally, we shared this post with Vercel before publishing. Turns out their example was running on an older version of Next.js, they updated it, so we took the time to rework this to compare to their latest example. Seriously, We Like Vercel We consider them friends and even partners as Vercel is an excellent deployment target for Remix. I've deployed Remix apps to pretty much every hosting service I've ever heard of. Vercel's developer experience is easily my favorite. The "Develop, Preview, Ship" mantra has real effects. Just this morning @gt_codes and I were trying to figure out a production bug and having every preview deployment available with a little screenshot of each one helped us find the bad commit in seconds. It's good stuff. It's a funny relationship now because we're not just friends and technology partners, we're framework competitors! So, to our friend, partner, and competitor Vercel, Lee put the motivation behind this article wonderfully: When there's competition in DevTools, developers win: * Svelte is pushing React * Remix is pushing Next.js * Prisma is pushing ORMs * Deno is pushing Node.js * Supabase is pushing Firebase * esbuild / SWC are pushing JS tooling * Bun is pushing SWC What else? -- Lee Robinson (@leeerob) November 30, 2021 Please read this post with that context. Let's get pushing! Self-Descriptions I think you can tell a lot about something by how the people who built it describe it. (If you follow me on twitter you'll know I've been iterating laboriously over ours!) Next.js describes itself as: The React Framework for Production. Next.js gives you the best developer experience with all the features you need for production: hybrid static & server rendering, TypeScript support, smart bundling, route pre-fetching, and more. No config needed. Next.js is built by Vercel. Looking at the GitHub repo for the Vercel platform it states: Vercel is a platform for static sites and frontend frameworks, built to integrate with your headless content, commerce, or database. We describe Remix as: Remix is an edge native, full stack JavaScript framework for building modern, fast, and resilient user experiences. It unifies the client and server with web fundamentals so you can think less about code and more about your product. We'll leave it to you to contrast those descriptions. Home Page, Visually Complete Is Remix as fast as Next.js? This is usually the first question people ask. Next.js often uses the phrase "performance by default" and they've got it in spades! Let's see how fast each app can render a "Visually Complete" page. We ran the sites through WebPageTest. It's a great tool that generates the comparison gifs in this post. In every comparison, we gave each framework five runs and took the best one from each. Above each comparison is a caption that links to the results that generated the animation. You are free to validate everything yourself by simply clicking "rerun test" on WebPageTest.com. This first one was run from Virginia with a cable modem connection to the internet. Home Page, Virginia, Cable Remix loads in 0.7s, Next in 0.8s Before we say anything, let's acknowledge that all three versions are so dang fast it's not even worth comparing who is faster. It's a bit unfair to Next.js too, because the little animation of the cookie banner factors into "visually complete" and the Remix site doesn't have it. Let's look at it in slow motion: Home Page, Virginia, Cable, Slow-Mo Remix loads in 0.7s, Next in 0.8s Now we can see that Next.js is actually done 0.8s. Again, they are all fast. I also ran them all through the same test with a 3G network connection and it was the same story: all fast, all looked about the same. Remix is as fast as Next.js Why The Apps Are Fast Why Next.js is fast: The homepage uses Static Site Generation (SSG) with getStaticProps. At build time, Next.js pulls data from Shopify, renders a page to an HTML file and puts it in the public directory. When the site is deployed, the static file is now served at the edge (out of Vercel's CDN) instead of hitting an origin server at a single location. When a request comes in, the CDN simply serves the file. Data loading and rendering have already been done ahead of time so the visitor doesn't pay the download + render cost. Also, the CDN is distributed globally, close to users (this is "the edge"), so requests for statically generated documents don't have to travel all the way to a single origin server. Why the Remix port is fast: Remix doesn't support SSG so we used the HTTP stale-while-revalidate caching directive (SWR, not be consfused with Vercel's swr client fetching package). The end result is the same: a static document at the edge (even on the same CDN, Vercel's). The difference is how the documents get there. Instead of fetching all of the data and rendering the pages to static documents at build/deploy time, the cache is primed when you're getting traffic. Documents are served from the cache and revalidated in the background for the next visitor. Like SSG, no visitor pays the download + render cost when you're getting traffic. If you're wondering about cache misses, we'll discuss that a little later. SWR is a great alternative to SSG. Another thing that makes deploying to Vercel great is that their CDN supports it. You might wonder why the Remix port isn't as fast as Next.js. Since Remix doesn't have built-in image optimization (yet), we just pointed the images at the Next.js app . The browser has to open a connection to both domains and this delays the images from loading by 0.3s (you can verify this on the network waterfall). If the images were self-hosted, it would be right there with the other two around 0.7s. Why the Remix rewrite is fast: Instead of caching documents at the edge with SSG or SWR, this version caches data at the edge in Redis. In fact, it actually runs the application at the edge too with Fly.io . Finally, it's got a quick image optimization Resource Route that writes to a persistent volume. It's basically its own CDN . This might have been difficult to build a few years ago, but the server landscape has changed significantly in the past few years and is only getting better. Loading Dynamic Pages How is Remix Different than Next.js? This is the next question we get. There are a lot of differences in feature sets, but one major, architectural difference is that Remix doesn't rely on SSG for speed. In practically every app, you will eventually hit a case that SSG can't support. For the application we're comparing here, it's the search page. The constraint is that users can submit an infinite number of queries. With the universe's current constraints on space and time, you can't statically generate infinite pages. SSG is off the table. Search Page, Cached, Virginia, Cable Remix in 0.8s, Next.js 1.9 Because SSG doesn't scale to dynamic pages, Next.js switched to clientside data fetching from the user's browser. Taking a peak at the network waterfall will tell us why it's 2.3x slower than Remix. Remix Search [wpt-search] Next.js Search [wpt-search] The Remix apps are completely done before the Next.js app even starts loading images. Perhaps the most important thing to get right in web performance is parallelizing the network waterfall. At Remix, we are fanatical about it. Why Next.js is slower: Next.js introduced what we call a "network waterfall request chain". Because SSG can't be used here, the app is fetching the search results from the user's browser. It can't load images until it has fetched data, and it can't fetch data until it has loaded, parsed, and evaluated the JavaScript. Fetching in the client also means more JavaScript over the network, and more time for parse/eval. Sometimes we forget about parse/eval, but you can see the JS execution on the 15th request took longer than the document did to download! Next.js is sending 1.5x more JavaScript than Remix with 566 kB vs. 371 kB unpacked. Over the network it's 50 kB more compressed (172 kB vs. 120 kB). Doing more work in the browser starts to add up. Look at the bottom rows that show CPU utilization and the browser's main thread activity. The Next.js app is quite busy with a big red "long task" slowing things down. Why Remix is still as fast as the homepage: Neither Remix example actually had to talk to the Shopify API in the request. While SSG can't cache the search page, the Remix versions can: with either SWR or Redis. When you have a single, dynamic way to generate pages, you can tweak your caching strategy without changing your application code. The result is SSG speed on commonly visited pages. The "/ search" page will likely be primed, as well as the categories on the left nav and common queries like "tshirt". Dynamic Page Cache Miss Yeah, but what about a cache miss? You're probably not going to believe me on this one, and I have no way to prove that our cache was empty, but here is a cache miss in Remix (cross my heart, swear to die, stick a needle in my eye). Search Page, Empty Cache, Virginia, Cable Remix loads in 3.9s, Next in 8s Actually, I lied. That's a cache hit for the Remix Rewrite. The cache miss was faster (0.6s ). I really didn't think you'd believe me, so I put the slower cache hit in the graphic Impossible! Turns out the Shopify API is quite fast. Since the Next.js app is fetching directly from the browser to the Shopify API, we can look at the network graph of the test and see that the request only took 224ms. It took longer for the browser to establish a connection with the API than to make the request! (They could speed that up with a in their initial HTML.) If the user's browser could make the request to Shopify that quickly, the Remix server can certainly do it faster. The user's connection to the cloud is always going to be slower than your server's, probably best to keep the data fetching there. Bottom line is, caching is nearly pointless when using the Shopify API. Cache hits or misses will be virtually indistinguishable from each other. This is best illustrated by slowing down the user's network and seeing what happens. Let's do another cache miss, this time from Hong Kong on a 3G connection. Search Page, Empty Cache, Hong Kong, 3G Remix loads in 3.1s, Next in 6.6s Next.js is now 3.5 seconds behind, even on a cache miss. What's up? You said the Shopify API was fast! Next.js can't load images until it loads data, it can't load data until it loads JavaScript, and it can't load JavaScript until the document loads. The user's network is a multiplier for every single step in that chain . In Remix, the only chain is waiting for the document to be able to load the images. The Remix design of always fetching on the server removes the user's network as a multiplier everywhere else. Remix can start fetching from Shopify immediately when a request is received. It doesn't have to wait for the browser to download the document and then the JavaScript. It doesn't matter how slow the user's network is, the fetch to the Shopify API on the server doesn't change and is probably under 200ms. Architectural Divergence The user experience wasn't the only thing that took a hit when Next.js moved to fetching in the client. The app now has two different sets of abstractions for talking to Shopify: one set for SSG and another for the browser. Architectural divergences like this bring up some major questions: * Do you have to authenticate in the browser? * Does the API support CORS? * Does the API SDK even work in the browser? * How do we share code between build and browser code? * Is it okay to expose the API token in the browser? * What permissions does our token that we just shipped to every visitor have? * Can this function use process.env? * Can this function read window.location.origin? * How do I make a network request that works in both places? * Can we cache these responses somewhere? * Should we make an isomorphic cache object that works in both places and pass it in to the different data fetching functions? (omigosh I said isomorphic) (that's orthogonal to this post) (gah PROFUNCTOR OPTICS!) Let's answer these questions for Remix, where you only have to abstract the Shopify API on the server: * Do you have to authenticate in the browser? (no) * Does the API support CORS? (doesn't matter) * Does the API SDK even work in the browser? (doesn't need to) * How do we share code between build and browser code? (you don't have to) * Is it okay to expose the API token in the browser? (don't need to) * What permissions does our token that we just shipped to every visitor have? (you didn't!) * Can this function use process.env? (yes) * Can this function read window.location.origin? (no) * How do I make a network request that works in both places? (however you want, it's not in the browser) * Can we cache these responses somewhere? (sure, HTTP, redis, lru-cache, persistent volume, sqlite...) * Should we make an isomorphic cache object that works in both places and pass it in to the different data fetching functions? (don't need to!) The simpler these questions are to answer, the better your abstractions will be, resulting in simpler code to work with. If the Next.js app moved away from client fetching, and used getServerSideProps, they would probably close the gap and have simpler answers to these questions. It's interesting to note that the Next.js docs push you away from server fetching and into SSG or client fetching often, though: If you do not need to pre-render the data, then you should consider fetching data on the client side. They also encourage client fetching for pages with user data, pushing you, again to more architectural divergence. [Client fetching] works well for user dashboard pages, for example. Because a dashboard is a private, user-specific page, SEO is not relevant As we've seen here, server rendering is about better performance, too, not just SEO. The fundamental difference here is that Next.js has four "modes" for getting data on the page: * getInitialProps - called server and client side * getServerSideProps - called server side * getStaticProps - called at build time * client fetching - called in the browser Remix only has one: loader. It's easier to abstract around one thing that only runs in one place than four things that run in three places. The Cost of Architectural Divergence Let's try to quantify the cost of this architectural divergence. Perhaps the most difficult development task of this app is abstracting the commerce back end. The app is designed in a way that you can plug anything into it: Shopify, BigCommerce, Spree, Saleor etc. In the Next.js app, the Shopify integration lives in this folder. Running cloc on it today yields: 101 text files. 93 unique files. 8 files ignored. github.com/AlDanial/cloc v 1.92 --------------------------------------------------------------------- Language files blank comment code --------------------------------------------------------------------- TypeScript 88 616 2256 5328 GraphQL 1 1610 5834 2258 Markdown 1 40 0 95 JSON 2 0 0 39 JavaScript 1 1 0 7 --------------------------------------------------------------------- SUM: 93 2267 8090 7727 --------------------------------------------------------------------- Almost 8,000 lines of code across nearly 100 files. I ran it for the other integrations and it's the same story. They're all approaching 100 files and hover around 10,000 lines of code. Nearly all of that code makes it to the browser, too. Here is the Remix integration with Shopify. * 1 file * 608 lines of code * None of it goes to the browser This, right here, is the cost of architectural divergence. The Next.js abstractions have to anticipate, and participate in the build and the browser. The Remix abstraction is only on the server. You might wonder if the two Shopify providers have the same feature sets, and maybe we're being deceptive. There is some code in many of them for authentication and wishlists, but the Shopify provider didn't use either one (but did have to export modules for them). Using the two websites they appear to have the same feature set. Regardless, if we did miss something, it would be hard to imagine it would take 7,000 lines of code to get there when the visible features in the app only took 1/10th of that. Even if Next.js moved to getServerSideProps for the search page, they'd still need almost all of that code for the data mutation features, but I'm getting ahead of myself now! Edge Native We talk a lot about "deploying to the edge". What does that mean? Here's another cache miss from Hong Kong, this time with a fast user network: Search Page, Empty Cache, Hong Kong, Cable Remix loads in 3.9s, Next in 8s This time we're going to talk about the difference between the two Remix apps. We already know the Next.js version is slower because of the network waterfall chains. Both Remix apps fetch on the server, so why is the Remix port so far behind the Remix Rewrite? The answer is simple: the Remix Port is running in a Vercel function, and Vercel's functions don't run your code at the edge, they run in one region, defaulting to Washington DC. That's pretty far away from Hong Kong! This means the user has to get all the way from Hong Kong to Washington, DC, before the server can start fetching the data from Shopify. When the server is done, it has to send the document all the way back. The Remix Rewrite is running in Washington DC, too, but it's also running in Hong Kong! That means the user has a very quick hop to the Remix server where everything is going to be faster. It's like riding your bike to the train to get into town, instead of riding your bike the whole way. ----------------------------------------- -----==== You can see this play out in the network waterfall (as usual): Remix Rewrite @ Edge [wpt-hkg-se] Remix Port in US East [wpt-hkg-se] The infrastructure difference manifests in the first blue bar for the document. In the Remix Port, it's way bigger. That's the user riding their bike half way around the world in the Vercel function bike lane. In the Remix Rewrite, it got on the train and made it to the Shopify API and back much sooner. This version runs on Fly.io that can run Node.js servers in dozens of regions around the world. Remix doesn't depend on Node.js though. It can run in any JavaScript environment. In fact, it already runs in Cloudflare Workers, which means you're running your code on their 250 servers distributed around the world. Can't get any closer to users than that! This is why we say that Remix is "edge native". Next.js depends on Node.js so it's ability to deploy the edge is limited today. We still have a lot of work to do in this area to make the developer experience better. We only officially support Node.js and Cloudflare right now, but we're actively working on Deno, and community members have Remix running on Fastly. When you're using an "edge native" framework like Remix, you no longer have to decide which users get a faster experience. You can give every user a fast experience no matter where they are in the world. The edge is what Remix was built for. As you can see, it's very promising. As we understand it, the Vercel team is working hard on deploying your apps to the edge, too. Remix is ready for it, we can't wait to try it out. Clientside Transitions Both frameworks enable instant transitions with link prefetching, but Next.js only does this for pages created from SSG. The search page is out, again. (maybe next time, sport) However, Remix can prefetch any page because there was no architectural divergence for data loading. Prefetching an unknowable, user-driven search page URL is not any different than prefetching a knowable product URL. In fact, Remix prefetching isn't limited to just links, it can prefetch any page, at any time, for any reason! Check this out, prefetching the search page as the user types: Search Input Prefetching, Fast 3G No spinners, no skeletons, instant user experiences, even on slow networks This was super easy to do, too. import { Form, PrefetchPageLinks } from "remix"; function Search() { let [query, setQuery] = useState(""); return (
); } Since Remix uses HTML's (instead of an in memory cache like Next.js) the browser actually makes the requests, not Remix. Watching the video you can see how the requests are cancelled as the user interrupts the current fetch. Remix didn't have to ship a single character of code for that top-notch handling of asynchrony. #useThePlatform ... or, uh, #reuseThePlatform ?! Data Mutations This is where Remix and Next.js start to look completely different. Half of your app code is related to data mutations. It's time your web framework respects that. How mutations work in Next.js: Next.js doesn't do anything for you here.