I've shipped more Next.js applications in the last year than most agencies ship in three. Six complete web design templates with 32 routes — each with its own design system, animation architecture, and responsive behavior. A full e-commerce platform with payment processing, admin dashboard, and CRM. A portfolio site with a blog system, dev log, product showcase, and SEO infrastructure. All Next.js. All deployed on Vercel. All in production serving real users.
This isn't a review based on a to-do app tutorial. It's a production report from someone who depends on this framework for their livelihood.
The Scale of What I've Shipped
Let me be specific because scale matters. Studio Noir: dark creative portfolio with horizontal scroll, custom cursor, side navigation — 4 pages. Elevate: corporate SaaS with 3D tilt cards, gradient blobs — 5 pages. Pop Store: neo-brutalist e-commerce with spring physics — 4 pages. Vitalis: healthcare with pill navigation, rounded elements — 4 pages. Roast: coffee shop with parallax and editorial layout — 5 pages. Mono: ultra-minimal portfolio, one font — 4 pages. Each template has its own animation system built on GSAP, its own color palette, its own responsive breakpoints, its own component library.
Then the 88 Badminton House e-commerce platform: product catalog, cart, checkout, payment integration, admin dashboard with 15+ pages, customer CRM, order management, stringing service tracker. Then my portfolio site: 80+ components, dynamic blog with TOC and reading progress, vault showcase with waitlist forms, changelog system, privacy policy, SEO with structured data and OpenGraph images generated per page.
This is what I mean by 'in production.' Not one project. A body of work that exercises every part of the framework.
Why Next.js and Not Something Else
I evaluated the alternatives seriously. Remix — excellent DX, but the ecosystem is smaller and the deployment story was less mature when I started. Astro — brilliant for content sites, but I need client-side interactivity for dashboards and animation-heavy pages. SvelteKit — genuinely tempting, but the React ecosystem's breadth (GSAP React hooks, component libraries, hiring pool) won out. Nuxt — I've built production Vue.js applications, but React/Next.js is where my clients' future developers will be most comfortable.
The decision isn't about what's technically best in isolation. It's about what gives the best outcome for the full lifecycle of a project: development speed, deployment reliability, ecosystem support, and long-term maintainability by someone who isn't me. Next.js wins on that composite score.
App Router in Production — The Real Story
The App Router is simultaneously the best and most frustrating part of modern Next.js. The file-system routing is intuitive — a page.tsx in a folder is a route. Layouts compose naturally. Loading and error states have first-class support. The mental model is clean.
The frustration comes from the Server Component / Client Component boundary. In theory, it's elegant: server components render on the server, ship zero JavaScript, and are fast. Client components hydrate on the client and handle interactivity. In practice, the error messages when you accidentally use a hook in a server component or pass a non-serializable prop across the boundary are cryptic. After 30+ routes, I've internalized the rules. But every new developer who touches one of my codebases trips on this within the first hour.
The params pattern changed in Next.js 16 — params became a Promise that needs to be awaited in page components. I discovered this the hard way when my changelog detail pages were 404ing in production. The build succeeded locally but the runtime behavior changed. This kind of breaking change in a minor version is my biggest frustration with the framework's evolution. The fix took two minutes. Finding the cause took an hour.
Turbopack Changed My Development Speed
Turbopack replaced Webpack as the dev server bundler and the difference is not incremental — it's transformational. My portfolio site with 80+ components starts the dev server in under 2 seconds. Hot module replacement is nearly instant. Switching between pages in the browser feels like navigating a local application, not waiting for a bundler.
For the template development workflow — where I'm constantly tweaking animations, adjusting spacing, testing responsive breakpoints — Turbopack's speed means I see changes as fast as I can save the file. The feedback loop between intention and result is as tight as it can get. This directly translates to build quality because I iterate more, try more variations, and catch visual issues faster.
Server Components vs Client Components
After shipping 32 routes with mixed server and client components, here's my practical rule: server by default, client when you need interactivity. My blog pages are server components — they fetch data, render HTML, ship zero JavaScript for the content. The reading progress bar is a client component because it needs scroll position. The table of contents highlighting is a client component because it needs intersection observer. The blog text itself? Pure server component. Fast, lightweight, SEO-friendly.
For the design templates, almost everything is a client component because GSAP animations require client-side JavaScript. This is fine — these are showcase pages where animation IS the product. The key insight is knowing which components genuinely need the client and which are server components wearing a 'use client' directive unnecessarily. Every unnecessary 'use client' is JavaScript your user downloads but doesn't need.
The Metadata API Is Quietly Excellent
Next.js's metadata API is the most underappreciated feature for production applications. Each page exports a generateMetadata function that produces title, description, OpenGraph tags, canonical URLs, and structured data. It's type-safe, composable with the layout hierarchy, and handles edge cases like dynamic routes gracefully.
I generate unique OpenGraph images per blog post using opengraph-image.tsx — a React component that renders to a 1200x630 PNG at build time. Each blog post gets a branded card with its title, category, and read time. When someone shares a link on LinkedIn, the preview card shows the post title on my branded dark background, not a generic fallback. This cost me one file and zero external services.
The SEO infrastructure I've built on the metadata API includes: per-page canonical URLs, OpenGraph tags with article type for blog posts, Twitter cards, JSON-LD structured data (Person, WebSite, ProfessionalService schemas), geo-targeting for Malaysia, and hreflang tags. All of this is defined in TypeScript, type-checked at build time, and composed through the layout hierarchy. Every page automatically inherits the site-level metadata and can override what it needs.
Deploying on Vercel — The Full Picture
I deploy everything on Vercel because the integration with Next.js is seamless — which makes sense given they make both. Git push triggers a build. Preview deployments for branches. Production deployments for main. Rollback with one click. This workflow is so reliable that I don't think about deployment anymore. It's a solved problem.
The edge network means my site loads fast in Malaysia, in the US, in Europe. The first-byte time is consistently under 200ms globally. For a Malaysian business serving worldwide clients, this matters — a visitor from San Francisco gets the same performance as someone in KL. I didn't configure CDN rules or edge caching. Vercel handles it.
Where Vercel earns its keep beyond hosting: the analytics are useful (Core Web Vitals, page-level performance), the domain management is trivial, and the environment variable system is clean. Where it frustrates: the free tier limits are opaque — you hit them without warning, and the upgrade to Pro is a jump from $0 to $20/month. For client projects, I always budget for Pro from the start.
Performance in the Real World
My portfolio site scores 100 on Lighthouse for Best Practices and SEO, with strong accessibility scores. Performance on mobile sits around 70 — honest cost of heavy GSAP animations and client-side JavaScript on an animation-driven showcase site. The non-animated pages (blog, privacy, changelog) score higher because they ship less JavaScript. Next.js makes the defaults good: image optimisation with next/image, automatic code splitting per route, font optimisation with next/font, static generation for pages that don't need dynamic data. These are framework-level decisions that benefit every page without per-page effort.
The 88BH e-commerce site is heavier — product images, client-side cart state, dynamic pricing — and still scores 80+ on mobile Lighthouse. The techniques that matter: lazy loading below-the-fold images, prefetching critical routes, keeping the main thread clear of heavy computation. Next.js provides the tools. Using them correctly is still the developer's job.
Security Headers and CSP
Next.js's next.config.ts supports custom headers per route, and I use this extensively. Every project ships with: Content Security Policy (restricting script, style, font, image, and connection sources), HSTS with preload, X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy, and a Permissions-Policy that explicitly disables unused browser APIs.
Writing a correct CSP for a Next.js application is non-trivial because of the framework's own requirements. Next.js injects inline scripts for hydration data, which means you need 'unsafe-inline' in your script-src or a nonce-based approach. Tailwind generates styles at build time, but third-party fonts require style-src exceptions. GSAP animations don't need special CSP rules because they manipulate the DOM, not inject scripts. These are details I've learned by debugging CSP violations in production across multiple projects.
Where Next.js Has Friction
The build output is opaque. When something fails in production that worked in development, debugging the compiled output is painful. Source maps help, but the server component / client component split means errors can originate in places that don't map cleanly to your source code.
Breaking changes between versions are more frequent than I'd like for a framework this widely used. The params Promise change in Next.js 16 broke my changelog pages. The metadata API changed between 14 and 15. The App Router itself was a complete paradigm shift from the Pages Router. Each change is justified, but the migration cost accumulates for someone maintaining 8+ Next.js projects simultaneously.
The bundle size for animation-heavy pages is a genuine concern. GSAP, SplitType, and Lenis add significant JavaScript that runs on the client. For the design templates — where animation is the product — this is acceptable. For content-heavy pages like blog posts, I'm careful to keep animations lightweight and load GSAP only on pages that use it. Next.js's per-route code splitting helps, but the developer has to be intentional about it.
From a Product Manager's Lens
Next.js is the framework I recommend to clients because of what happens after I leave. The React ecosystem has the largest developer talent pool. TypeScript provides guardrails for whoever maintains the code next. The file-system routing is self-documenting — a new developer can understand the application structure by looking at the folder tree. The deployment is automated through Git. These are product decisions, not technology decisions.
When I scope a project, I can estimate accurately because I know exactly how long each Next.js feature takes to implement. A new route with server-side data: 30 minutes. An API route with validation and error handling: an hour. A dynamic OG image: 45 minutes. Authentication with middleware protection: half a day. This predictability is what makes fixed-price projects profitable.
From a Developer's Lens
The DX is the best in the React ecosystem. Turbopack's speed, TypeScript-first APIs, the metadata system, the image component, the font optimisation — every major pain point of building React applications has been addressed at the framework level. I spend my time on business logic and design, not on build configuration and performance hacks.
The integration with Claude Code is excellent because Next.js projects have predictable structure. Claude understands the App Router conventions, the metadata API, the server/client component split. When I ask Claude to add a new route, it creates the right files in the right places following the right patterns. Predictable framework conventions make AI-assisted development dramatically more effective.
What I'd Change
Stable APIs between major versions. The pace of change is impressive from an innovation standpoint and exhausting from a maintenance standpoint. I want to upgrade my projects to get security patches without rewriting how params work.
Better error messages at the server/client boundary. The current errors assume you understand the rendering model deeply. New developers don't, and the error messages don't teach them. A message that says 'you're using useState in a server component — add use client to this file or move the interactive part to a child component' would save thousands of hours across the community.
Despite the friction, I keep choosing Next.js because the alternative is worse. The framework handles the hard problems — routing, rendering, optimisation, deployment — so I can focus on the problems that actually matter: building systems that run businesses.
“The best framework is the one that gets out of your way. Next.js isn't perfect, but it disappears more often than any alternative I've tried.”