engineering

Next.js 15 & React 19: Building Modern Web Applications

Explore the latest features in Next.js 15 and React 19, including App Router, Server Components, Partial Prerendering, and the new React Compiler.

S

Sambo Chea

Staff Engineer

October 5, 2025
6 min read

Next.js 15 & React 19: The Future of Web Development

The latest releases of Next.js 15 and React 19 bring revolutionary changes to how we build web applications. In this comprehensive guide, we'll explore the most impactful features and how they transform the development experience at CUBIS.

The App Router Revolution

Next.js 15's App Router represents a fundamental shift in how we structure our applications. Built on React Server Components, it enables a new paradigm of server-first rendering with selective client-side interactivity.

Server Components by Default

// app/products/page.tsx async function ProductsPage() { // This runs on the server - no client-side bundle impact const products = await fetch('https://api.example.com/products'); return ( <div className="grid grid-cols-3 gap-6"> {products.map(product => ( <ProductCard key={product.id} product={product} /> ))} </div> ); }

Benefits we've experienced:

  • 40% smaller bundle sizes - Server Components don't ship to the client
  • Instant data access - Direct database queries without API routes
  • Better SEO - Fully rendered HTML on the server
  • Improved performance - Less JavaScript to download and parse

Client Components When You Need Them

'use client'; import { useState } from 'react'; export function InteractiveSearch() { const [query, setQuery] = useState(''); return ( <input type="text" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search products..." /> ); }

The 'use client' directive lets us opt-in to client-side interactivity exactly where we need it.

Partial Prerendering (PPR): Best of Both Worlds

PPR is arguably the most exciting feature in Next.js 15. It combines static generation and dynamic rendering in a single page.

How PPR Works

// app/dashboard/page.tsx export const experimental_ppr = true; export default async function Dashboard() { return ( <div> {/* Static shell - prerendered at build time */} <header> <Navigation /> </header> {/* Dynamic content - rendered per request */} <Suspense fallback={<SkeletonLoader />}> <UserStats /> {/* Fetches user-specific data */} </Suspense> {/* Static footer */} <footer> <Footer /> </footer> </div> ); }

Real-world impact at CUBIS:

  • 50ms faster TTFB - Static parts serve instantly
  • Personalized content - Dynamic sections load user data
  • Zero layout shift - Suspense boundaries prevent CLS issues

React 19: Compiler & Concurrent Features

React 19 introduces the React Compiler (formerly "React Forget"), which automatically optimizes your components without manual memoization.

Automatic Optimization

Before (React 18):

function ProductList({ products }) { // Need to manually memoize const sortedProducts = useMemo( () => products.sort((a, b) => a.price - b.price), [products] ); return <div>{sortedProducts.map(renderProduct)}</div>; } const renderProduct = useCallback((product) => { return <ProductCard key={product.id} product={product} />; }, []);

After (React 19 with Compiler):

function ProductList({ products }) { // Compiler automatically optimizes this const sortedProducts = products.sort((a, b) => a.price - b.price); return <div>{sortedProducts.map(product => ( <ProductCard key={product.id} product={product} /> ))}</div>; }

New use Hook

The use hook enables asynchronous data fetching in components:

import { use } from 'react'; function ProductDetails({ productPromise }) { // use() unwraps the promise const product = use(productPromise); return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> </div> ); } // Parent component function ProductPage({ id }) { const productPromise = fetchProduct(id); return ( <Suspense fallback={<Loading />}> <ProductDetails productPromise={productPromise} /> </Suspense> ); }

Data Fetching Patterns

Next.js 15 standardizes data fetching with native fetch caching:

Automatic Request Deduplication

// These two fetches are automatically deduplicated async function Header() { const user = await fetch('/api/user').then(r => r.json()); return <div>Welcome, {user.name}</div>; } async function Sidebar() { const user = await fetch('/api/user').then(r => r.json()); return <UserMenu user={user} />; }

Granular Cache Control

// Revalidate every 60 seconds const data = await fetch('/api/data', { next: { revalidate: 60 } }); // No caching for user-specific data const userData = await fetch('/api/user', { cache: 'no-store' }); // Cache with tags for on-demand revalidation const posts = await fetch('/api/posts', { next: { tags: ['posts'] } });

Performance Optimizations

Image Optimization

import Image from 'next/image'; export function ProductImage({ src, alt }) { return ( <Image src={src} alt={alt} width={800} height={600} placeholder="blur" blurDataURL="/placeholder.jpg" loading="lazy" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" /> ); }

Results:

  • WebP/AVIF automatic conversion
  • Responsive image sizing
  • Lazy loading out of the box
  • 60% smaller image sizes

Font Optimization

// app/layout.tsx import { Inter, Roboto_Mono } from 'next/font/google'; const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-inter', }); const robotoMono = Roboto_Mono({ subsets: ['latin'], display: 'swap', variable: '--font-roboto-mono', }); export default function RootLayout({ children }) { return ( <html className={`${inter.variable} ${robotoMono.variable}`}> <body>{children}</body> </html> ); }

Metadata API for SEO

Type-safe metadata generation:

import { Metadata } from 'next'; export async function generateMetadata({ params }): Promise<Metadata> { const product = await fetchProduct(params.id); return { title: product.name, description: product.description, openGraph: { images: [product.image], type: 'product', }, twitter: { card: 'summary_large_image', }, }; }

Streaming & Suspense

Progressive page rendering improves perceived performance:

export default function ProductPage() { return ( <div> <Suspense fallback={<HeaderSkeleton />}> <Header /> </Suspense> <Suspense fallback={<ProductGridSkeleton />}> <ProductGrid /> </Suspense> <Suspense fallback={<ReviewsSkeleton />}> <Reviews /> </Suspense> </div> ); }

Components render and stream to the client as data becomes available, instead of blocking on the slowest query.

Migration Tips

From Pages Router to App Router

  1. Start with layout.tsx - Create your root layout
  2. Convert pages incrementally - Both routers can coexist
  3. Move API routes - Transition to Route Handlers
  4. Update data fetching - Replace getServerSideProps with async components

Adopting React 19

  1. Enable the compiler - Add to next.config.js
  2. Remove manual memoization - Let the compiler handle it
  3. Adopt new hooks - Use use() for async data
  4. Test thoroughly - Compiler is still experimental

Real-World Performance Gains

After migrating our main application to Next.js 15 + React 19:

  • Lighthouse Score: 87 → 98
  • First Contentful Paint: 1.8s → 0.9s
  • Time to Interactive: 3.2s → 1.4s
  • Bundle Size: 245KB → 147KB
  • Build Time: 87s → 34s

Conclusion

Next.js 15 and React 19 represent a massive leap forward in web development. The combination of Server Components, PPR, and the React Compiler creates applications that are:

  • Faster - Smaller bundles, better caching, streaming
  • Simpler - Less boilerplate, automatic optimizations
  • More maintainable - Better separation of server/client code
  • SEO-friendly - Server-rendered by default

At CUBIS, we're all-in on this stack for new projects and actively migrating existing applications. The developer experience and performance gains are too significant to ignore.

Resources


Questions or feedback? Reach out to our engineering team:

Tags

#nextjs#react#frontend#web-development#app-router#server-components