back to the home page One Year with Next.js App Router — Why We're Moving On A critique of React Server Components and Next.js 15. webdev technical analysis opinion
As I've been using Next.js professionally on my employer's web app, I find the core design of their App Router and React Server Components (RSC) to be extremely frustrating. And it's not small bugs or that the API is confusing, but large disagreements about the fundamental design decisions that Vercel and the React team made when building it.
The more webdev events I go to, the more I see people who dislike Next.js, but still get stuck using it. By the end of this article, I will share how me and my colleagues escaped this hell, seamlessly migrating our entire frontend to TanStack Start.
§A Technical Review, What are Server Components?
The pitch of RSC is that components are put into two categories, "server" components and "client" components. Server components don't have useState , useEffect , but can be async function s and refer to backend tools like directly calling into a database. Client components are the existing model, where there is code on the backend to generate HTML text and frontend code to manage the DOM using window.document.* .
The first disaster: naming!! React is now using the words "server" and "client" to refer to a very specific things, ignoring their existing definitions. This would be fine, except Client components can run on the backend too! In this article, I'll be using the terms "backend" and "frontend" to describe the two execution environments that web apps exist in: a Node.js process and a Web browser, respectively.
This Server/Client component model is interesting. Since built-ins like 
     {server side render of a Suspense boundary} 
   
This solution doubles the size of the initial HTML payload. Except it's worse, because the RSC payload includes JSON quoted in JS string literals, which format is much less efficient than HTML. While it seems to compress fine with brotli and render fast in the browser, this is wasteful. With the hydration pattern, at least the data locally could be re-used for interactivity and other pages.
Even on pages that have little to no interactivity, you pay the cost. To use the Next.js documentation as an example, loading its homepage loads an page that is around 750kB (250kB of HTML and the 500kB of script tags), and content is in there twice.
You can verify that by pressing Cmd + Opt + u on Mac or Ctrl + u on other platforms. And then Cmd / Ctrl + f to locate any string of the blog, such as "building full-stack web applications". It's there twice. And there is no way around this, since it's a fundamental piece of React Server Components.
This RSC format certainly has more waste. But I really don't feel like digging into why the string /_next/static/chunks/6192a3719cda7dcc.js appears 27 separate times. What the hell, guys? Is your bandwidth free???
§Turbopack Sucks
This section is not constructive.
Turbopack isn't fast
Turbopack emits code that is hard to debug in a debugger (in development mode)
Turbopack throws bad error messages in many cases
I wouldn't have given this point a section in the blog normally, but I want to point out three actual examples directly from the project.
The first is a place where during some refactoring to satisfy the Server/Client component models, I accidentally made a Client component async . This one was quite anoying because it didn't say at all where the issue was, but only contained the server stack trace.
Another case of a terrible error message:
After fixing the underlying issue in this second error (which I cannot recall), the Dev server hung and had to be restarted to recover.
The final one is the dozen times I place a debugger breakpoint and the variable name hello gets turned into __TURBOPACK__imported__module__$5b$project$5d2f$client$2f$src$2f$utils$2f$filename$2e$ts__$5b$app$2d$client$5d$__$28$ecmascript$29$__["hello"] and other bullshit.
Okay. This all sucks. What can we do?
§Seamlessly Ditching Next.js and Vercel at Work
There are two types of web projects:
A web site with mostly static content.
A web app with majorly dynamic and interactive components.
And Next.js is the wrong tool for both of these jobs. If you're in the first category with a static web site, go for Astro or Fresh. For everyone who needs the full power of React, this section is about how I replaced the vendor locked Next with TanStack Start, incrementally and seamlessly.
It started with this Vite config.
vite.config.ts const config = defineConfig ( ({ mode }) => { const env = loadEnv ( mode , process . cwd (), "NEXT_PUBLIC_" ); return { server: { port: 3000 }, define: Object . fromEntries ( Object . entries ( env ). map ( ([k, v]) => [ `process.env.${k}` , JSON . stringify ( v )])), plugins: [ viteTsConfigPaths ({ projects: [ "./tsconfig.json" ] }), tailwindcss (), tanstackStart ({ router: { routesDirectory: "src/tanstack-routes" }, }), viteReact (), ], resolve: { alias: { next: path . resolve ( "./src/tanstack-next/" ) }, conditions: [ "tanstack" ], extensions: [ ".tanstack.tsx" , ".tanstack.ts" , ".mjs" , ".js" , ".mts" , ".ts" , ".jsx" , ".tsx" , ".json" , ], }, }; });
Then, I looked for every usage of a Next.js API, and either removed it or made a stub for TanStack. For example, src/tanstack-next/link.tsx implements next/link :
src/tanstack-next/link.tsx import { Link } from "@tanstack/react-router" ; import type { LinkProps } from "next/link" ; export default function LinkAdapter ({ href, ... rest } : LinkProps ) { return < Link { ... rest } to = { href as unknown as any } /> ; }
Some of these stubs can be extremely simple. Starting out, my implementation of useRouter was just return {} , but later I had to add a couple methods to the object. The code here doesn't have to be clean, because it is temporary.
Now, the new site can import nearly every client component by either stubbing out the Next.js APIs it needs, or by using the .tanstack.ts extension to re-implement logic on a file-by-file basis. And shortly after, I got the site's homepage to work in TanStack Start, and we merged the branch.
This first PR only supported one of our pages, and was able to do it in a thousand lines of added code, and 40 lines deleted. I had previous patches to remove the few uses of next/image and next/font .
What was left was porting every other route over. The one thing we lose in migrating from Next.js to any other framework is the ability to await data-fetching functions in the UI. In practice, moving every route into a loader function made it much more clear what happened when a page was SSR'd. For pages that had multiple fetches, these could be combined into a single, special API call that would return all of the relevant data for that page.
To re-iterate in bold font: The migration path from Server Components is to just simplify your code — RSC inherently drives you down a chaotic road of things you do not need. Nearly every complex part of our site got easier to understand for all engineers. The exception to this was having everyone get used to the new file system routing conventions. With enough examples, we all got the hang of it.
With the incremental migration in place, new code did not break the existing deployment. TanStack slowly took over the codebase, and we eventually deleted all of the Next.js stubs and gained all of the beautiful type-safety features that the TanStack Router provides. At the end, the site performed faster from every angle: Development Mode, Production page load times, Soft navigations, and at a lower price than our Next depoyment with Vercel.
We're not the only ones seeing the change. While I try and keep myself off of social media, someone sent me the results of Brian Anglin's work at Superwall, showing incredible CPU reductions on TanStack Start. I also recall ChatGPT switching from Next.js to Remix (random online chatter: [1] [2] [3]) a year ago.
§ next/metadata is Great
In my opinion, this is one of the only good APIs Next.js has, and was the one place in our code where moving to TanStack made things harder to do. Instead of worsening the code, I just ported their metadata API into a regular function, so everyone can use it. Originally, I had a 1:1 port on NPM, but earlier this year I simplified it's API into one short and understandable file. As of this blog post, I have added a TanStack-compatible meta.toTags API, which can be installed from JSR, NPM, or simply copied into your project.
notice: Due to time constraints with writing this article, the library has not yet been updated. I'll probably get around to it by the end of this week (Oct 24th). As a placeholder, I'm able to share the version that is used at work to my website: meta.tanstack.ts .
import * as meta from "@clo/lib/meta.ts" ; export const defineHead = meta . toTags . bind ( null , { base: new URL ( "https://paperclover.net" ), titleTemplate: (title) => [ title , "paper clover" ] . filter ( Boolean ). join ( ' | ' ), }); export const Route = createFileRoute ( "/blog" )({ head: () => defineHead ({ title: "clover's blog" , description: "a catgirl meows about her technology viewpoints" , canonical: "/blog" , embed: {}, extra: <> < meta name = "site-verification" content = "waffles" /> , > , }), component: Page , }); function Page () { ... }
My version wasn't concerned with covering the entire space of Next.js's metadata object, but instead uses inline JSX to fill that gap.
§ next/og is Good Too
No strong opinions. I just want to remind everyone that the @vercel/og package exists.
§My Experience Feels like the Usual
At the Next.js Conf 2024, everyone there was raving about Server Components. I forget exactly who I talked to, but the big people were all in on this. I, having implemented the bundler end of RSC, saw a couple of the problems in the format. With Next 15 "stabilizing" the App Router last year, many companies are building their products on it, realizing these pitfalls first-hand.
I came into the Next.js game late, only starting in June with version 15. But everyone I've talked to at events sympathize with my notes. All the people I talked to on the subject at Bun's 1.3 Party agreed with me. Even some people at Vercel told me they don't like how Next.js is to actually use.
I hope as TanStack Start stabilizes, it becomes the Next.js replacement everyone wants.
A lot of in the JavaScript ecosystem is a mess. That mess is why web development gets made fun of. There were a lot of times I thought that working with the web was an unrecoverable mess, but the mess was actually just the commonly-used libraries I surrounded myself with. When that is peeled back, modern web development technologies are awesome.
I've been making this website from scratch without any framework since late 2024, by writing systems like my own TUI progress widget, static file proxy, incremental build system, and many more components. Working on this code has produced some of my best coding sessions (by happiness) in years. The viewers of paper clover get a better quality website; the mini-libraries I create get extracted for public use, everyone wins.
This level of from-scratch is too much for most people, especially at the workplace. I say that at the minimum, we should only give our attention and money to high quality tools that respect us. And Next.js and the company behind it, Vercel, are not that.
If you use Next.js, and feel that the experience doesn't remind you of respect too, consider whether you and your colleagues want to continue supporting their serverless empire. The Vite ecosystem seems pretty decent to build on right now, but I still have little experience in using their tools at scale in production. The Vite+ launch from Void0 seems interesting, but only time will tell if these venture-funded tools will respect us (end-users and developers) long term.
Next.js Conf 2025, as of writing, is tomorrow. Instead of purchasing a $800 ticket, I decided to put that money toward the TanStack team for respecting and improving the web development ecosystem.
What the Future Holds
Slowly, I've been replacing many pieces of software that disrespect me with better alternatives. Some examples of this are GitHub, Visual Studio Code, DaVinci Resolve, Discord, Google Drive/Workspace, along many more. I plan to write more on this blog about the technical things I do (that progress library, the purpose of my own site generator, learnings from my current job), including some of my past projects at Bun (details on HMR, the crash reporter, and the crazy system for bundling built-in modules). If it interests you, please subscribe to the email list:
click here to send an email to [email protected] , requesting that you would like to be added to the mailing list. (i manage this mailing list manually)
back to top — ask a question about this article