The Framework Wars Are Exhausting
Open any developer forum in 2025 and you will find the same argument playing out in slightly different words. React versus Svelte. Next.js versus Remix versus Astro. Server Components versus client-side rendering versus "just use plain HTML." The discourse is relentless, often hostile, and -- I say this as someone who has been writing React since 2017 -- genuinely exhausting. Every few months, someone publishes a "Why I Left React" post that gets a thousand upvotes. Someone else responds with "Why React Is Still the Best Choice." The cycle repeats.
I am not going to settle the framework wars. What I am going to do is tell you what it was like to build a real application with Next.js 15 over the past three months, what worked, what did not, and -- most honestly -- when you should not use it at all. Not a feature tour. Not a comparison chart. Just the story of building something with it and the scars and satisfactions that came with the process.
The Project: A Recipe Platform
The application was a recipe sharing platform. Users could browse recipes (public, no auth required), create accounts, submit their own recipes with photos, save favorites, leave comments, and search by ingredients or dietary restrictions. Not groundbreaking, but the kind of project that touches every category of web application concern: static content, dynamic data, authentication, image handling, forms, search, and SEO.
I chose Next.js because the project needed server-side rendering for SEO (recipe pages should be crawlable and shareable on social media with proper meta tags), dynamic features for authenticated users, API routes for handling form submissions and image uploads, and image optimization for the hundreds of recipe photos. In theory, Next.js could handle all of this in a single framework. In practice, it could -- but the journey was not as smooth as the marketing suggests.
Week One: The App Router Honeymoon
Setting up the project was pleasant. npx create-next-app@latest gave me a working application in about thirty seconds. TypeScript configured. ESLint configured. Tailwind CSS configured. Folder structure created. Hot module replacement running. The out-of-the-box developer experience is genuinely excellent -- you write code, save the file, and the browser updates almost instantly. Error messages in the browser overlay are clear and helpful, often linking directly to the documentation.
The file-system routing was immediately intuitive. I created app/recipes/page.tsx for the recipe listing page. app/recipes/[slug]/page.tsx for individual recipe pages. app/dashboard/page.tsx for the authenticated user dashboard. Each folder became a route. Each page.tsx file became the UI for that route. Layout files handled shared navigation. Loading files provided skeleton states during data fetching. The convention-over-configuration approach meant I spent zero time on routing setup and all my time on the actual application.
Server Components felt revelatory during that first week. My recipe listing page fetched data directly from the database using Prisma, rendered it to HTML on the server, and sent zero JavaScript to the browser for that component. The page loaded in 120 milliseconds. No loading spinner. No "hydrating" flash. Just... the page, instantly, with all the data already there. For content-heavy pages where interactivity is not needed, Server Components are a genuine step forward in web performance.
Week Three: The Boundary Problem
The honeymoon ended when I started building interactive features. The recipe submission form needed client-side state management for the multi-step form flow, image upload previews, and real-time ingredient validation. The moment I added useState to a component, Next.js threw an error: you cannot use React hooks in a Server Component. Fair enough -- I added "use client" at the top of the file.
But then I needed that client component to receive data from a Server Component parent. And the client component had children that could have been Server Components but were now forced to be client components because they lived inside a "use client" boundary. The mental model of which components run where, which data can cross which boundary, and where to draw the line between server and client became the dominant intellectual challenge of the project. Not the business logic. Not the database design. Not the UI. The rendering boundary.
I spent an afternoon refactoring a page three times because I kept hitting boundary issues. "This component needs to be server-side for the data fetch, but it also needs an onClick handler, which means it needs to be client-side, but its parent is a server component that..." You get the idea. The solution, eventually, was to split components more heavily than I would have in a traditional React application -- thin server wrapper components that fetch data and pass it as props to thin client wrapper components that handle interactivity. It works, but it creates more files and more indirection than feels necessary.
Week Five: Server Actions and the Form
Server Actions were the highlight of the middle phase. Instead of creating a separate API route to handle the recipe submission form, I wrote a function marked with "use server" and called it directly from my form. Next.js handles the network request, the serialization, and the error handling automatically. The code reads almost like a traditional server-side web application from 2010 -- define a function, process the data, redirect the user -- but with the full power of React's component model on the frontend.
The recipe submission form used a Server Action that validated the input with Zod, uploaded the image to Cloudinary, created a database record with Prisma, revalidated the recipe listing page cache, and redirected to the new recipe's page. All in one function. No API route. No fetch call. No manual error handling for the network layer. It was elegant in a way that made me understand why the Next.js team is so excited about this pattern.
Progressive enhancement was the cherry on top: the form works even with JavaScript disabled, because Server Actions fall back to native HTML form submission. For a recipe website that might be accessed on slow connections or low-powered devices, that matters.
Week Eight: The Caching Confusion
Next.js's caching system was the most frustrating part of the entire build. There are multiple layers: the Router Cache, the Full Route Cache, the Data Cache, and React's own caching of Server Component payloads. Each layer has different invalidation rules, different lifetimes, and different override mechanisms. During development, I spent hours debugging why a page was showing stale data after a form submission, only to discover that the Router Cache was serving a cached version of the page on the client side despite the server cache being correctly invalidated.
Version 15 has improved the defaults -- fetch requests are no longer cached by default, and the Router Cache behavior has been made less aggressive. But the complexity is still there. When you call revalidatePath or revalidateTag, you need a mental model of which caches are being cleared and which are not. I found myself adding console.log statements in production to understand whether my data was fresh or stale. That should not be necessary.
To be fair: when the caching works correctly, the performance is remarkable. Static pages load almost instantly. Dynamic pages that use ISR (Incremental Static Regeneration) serve cached versions to most users while regenerating in the background. The system is powerful. It is just harder to reason about than it should be.
The Image and Font Optimization: Effortless Performance
The next/image component is one of those features that makes you wonder why every framework does not do this. You import your recipe photos, drop them into an Image component, and Next.js automatically optimizes them: lazy loading, responsive sizing, format conversion to WebP or AVIF, and blur-up placeholder generation. Our recipe pages with five or six high-resolution photos scored 95+ on Lighthouse performance without any manual image optimization. The images are processed on demand and cached, so there is no build-time cost.
next/font eliminates the layout shift problem that plagued self-hosted fonts for years. You import a Google Font in your layout file, and Next.js downloads, self-hosts, and applies it with zero layout shift using the CSS size-adjust property. No external network request. No FOUT (flash of unstyled text). It just works, invisibly, in a way that would have taken an hour of manual configuration to achieve otherwise.
The Vercel Question
I need to talk about Vercel, because the relationship between Next.js and Vercel is the open-source framework's most controversial aspect. Next.js is free. MIT-licensed. You can deploy it anywhere that runs Node.js -- AWS, DigitalOcean, Railway, Render, your own VPS. But certain features -- middleware, ISR, image optimization, edge functions -- work most smoothly on Vercel's hosting platform. Self-hosting requires additional configuration, sometimes unofficial workarounds, and occasionally accepting that a feature will not work identically.
During this project, I deployed to Vercel. The experience was, as expected, polished: git push, build, deploy, live in under two minutes. Preview deployments for PRs. Serverless function logs. Edge middleware metrics. If you are willing to host on Vercel, the experience is hard to beat. Next.js itself is free; Vercel's Hobby plan for personal projects is free; the Pro plan at $20 per user per month is where teams land. But the soft lock-in is real: choosing Next.js tilts you toward Vercel, and switching hosting later means accepting friction.
The Framework Comparison, From Experience
Remix, now part of Shopify, takes a different philosophical approach. Where Next.js layers abstraction on abstraction -- Server Components, Server Actions, multiple caching layers, the App Router's special file conventions -- Remix leans into web platform primitives. Forms are HTML forms. Data loading is explicit. Caching is HTTP caching. The mental model is simpler and more predictable. If I were building this recipe platform again and SEO was not a priority, I might choose Remix for the clarity of its data flow. But Next.js's ecosystem -- the sheer volume of tutorials, examples, component libraries, and deployment options -- is a practical advantage that Remix has not matched.
Astro would have been the right choice if the recipe platform were primarily a content site with minimal interactivity. Astro ships zero JavaScript by default, hydrates only the interactive "islands," and produces blazingly fast static sites. For a blog, a documentation site, or a marketing page, Astro is simpler and faster than Next.js. But the moment you need authentication, form handling, dynamic data, and real-time features, Astro's limitations become apparent. It is a content-first framework, not a full-stack application framework.
SvelteKit deserves mention because it offers a developer experience that many find more intuitive than React's. Svelte's reactivity model is simpler, the component syntax is cleaner, and SvelteKit's routing and data loading are elegant. The trade-off is ecosystem size: React's library ecosystem is vastly larger, and finding developers who know React is significantly easier than finding Svelte developers. For a personal project or a small team that values developer happiness, SvelteKit is a strong choice. For a startup hiring React developers, Next.js is the pragmatic one.
What Earned My Respect
- Server Components reduced our recipe listing page's JavaScript from 180KB to 12KB -- users on slow connections feel the difference
- Server Actions eliminated API boilerplate for form handling -- the code is clean and readable
- File-system routing is genuinely intuitive and scales well to larger projects
- Image and font optimization are free performance wins that require almost zero effort
- TypeScript support is first-class: typed params, typed metadata, typed everything
- The ecosystem of tutorials, examples, and community solutions is unmatched in the React world
- Deploying to Vercel is almost embarrassingly easy
What Frustrated Me
- The server/client component boundary creates a new category of bugs and architectural decisions that did not exist before
- Caching is powerful but opaque -- debugging stale data requires understanding four separate cache layers
- The rate of breaking changes across major versions is tiring -- patterns that were recommended in version 13 are deprecated in 15
- Vercel coupling is soft but real -- self-hosting means accepting trade-offs and digging through community workarounds
- Build times grow uncomfortably with project size -- our 200-page site took three minutes
- For simple sites, the framework's weight is overkill -- you are paying a complexity tax for capabilities you might not use
Who Should Use This (And the Honest Answer About Who Should Not)
Next.js is the right choice if you are building a React application that needs server-side rendering, SEO, a mix of static and dynamic pages, and the ability to scale from prototype to production without switching frameworks. SaaS dashboards, e-commerce storefronts, content platforms with dynamic features, marketing sites with lead capture forms -- these are the use cases where Next.js earns its complexity budget.
Next.js is not the right choice -- and this is the part most reviews skip -- in these situations:
If your site is primarily static content -- a blog, documentation, a portfolio -- use Astro. It will be faster, simpler, and ship less JavaScript. You do not need React Server Components to render a blog post.
If your team does not know React, do not learn React and Next.js simultaneously. The combined learning curve is steep and the mental model confusion between React patterns and Next.js patterns will slow you down for months. Learn React first, build something with it, and then consider whether you need Next.js's server-side features.
If your application is a purely client-side SPA with no SEO requirements -- an internal admin dashboard, a B2B tool behind a login wall -- a Vite plus React setup is simpler, faster to build, and easier to reason about. You are paying for Next.js's rendering infrastructure without using it.
If you do not want to be on the React upgrade treadmill, consider that Next.js's pace of change is aggressive. The transition from Pages Router to App Router, from getServerSideProps to Server Components, from API routes to Server Actions -- each shift required substantial code changes. If stability and long-term maintenance matter more than cutting-edge features, a more conservative framework might serve you better.
The Verdict
Our Verdict: 4.7 / 5
Next.js 15 is the most capable React framework available, and it is not particularly close. The combination of Server Components, Server Actions, flexible rendering strategies, built-in optimization, and a massive ecosystem creates a platform that can handle nearly any web application challenge. The recipe platform I built performs beautifully: recipe pages load in under 200 milliseconds, the SEO is excellent, and the development workflow -- from local dev to production deploy -- is smooth.
The 4.7 reflects both the power and the cost. The power is in what Next.js lets you build with a single framework: server-rendered pages, static pages, API endpoints, real-time features, optimized images, and progressive enhancement. The cost is the complexity you accept: the boundary between server and client, the layered caching system, the Vercel gravitational pull, and the relentless pace of change. For teams that can absorb that complexity, Next.js rewards them with performance and productivity that no competitor quite matches.
But it is not for everyone. And saying that honestly is more useful than saying it is the future of web development. Sometimes the future is simpler than we think. Sometimes the right framework is the one you do not have to fight. For the applications that need what Next.js offers, though -- 4.7 out of 5 is well earned.
Comments (3)