Building Modern Web Apps with Next.js 15: Complete Guide
Master Next.js 15's groundbreaking features including the App Router, Server Components, Streaming, and Performance optimizations. Build lightning-fast web applications with the latest React patterns.
Topics covered:

Lazar Kapsarov
Building high-performing SaaS, e-commerce & landing pages with Next.js, React & TypeScript. Helping businesses create digital experiences that convert.

Building a Modern Portfolio with Next.js 15
A portfolio is more than a gallery. It’s your sales engine.
The truth is… most developers treat their portfolio like a photo album. Pretty screens, endless scrolling, but slow. And slow kills trust. Clients don’t wait. They judge you in seconds. Your code, your process, your results — all compressed into that first load. That’s why I build portfolios with Next.js 15. Architecture that scales. Routing that flows. Server Components that cut the fat. And performance baked in from the start. This post breaks it down.
Architecture That Scales
Let’s start with the foundation. A modern portfolio should feel like a product, not a side-project. That means treating it with the same discipline I give SaaS dashboards or e-commerce flows.
Here’s my stack:
- Next.js 15 App Router → clean separation of routes, layouts, and nested UI. Why does this matter? Because portfolios grow. You start with three projects, then ten, then twenty-five. If your foundation is shaky, each new addition makes it slower and more challenging to maintain.
- React 19 + Server Components → less JavaScript in the browser, more speed.
- TypeScript 5 → predictable, safe, no runtime surprises.
- Tailwind CSS + shadcn/ui → design tokens, polished components, fast iteration.
- Framer Motion + GSAP → tasteful motion that supports, not slows.
Routing with the App Router
Speed. Clarity. Momentum.
Advanced Routing with the App Router
The old pages/ system worked, but it was blunt. App Router is surgical.
What you get:
- Nested layouts → keep headers, footers, and sidebars consistent.
- Route groups → structure sections (work, blog, about) without messy URLs.
- Loading & error states baked right in → smoother UX, no blank screens.
Imagine a potential client clicking from Projects → Case Study → Blog. Every step feels instant. No clunky reloads. No broken flow.
Even better? Search engines love the clean structure. SEO is about clarity as much as keywords.
Server Components in Action
This makes it simple to scale your portfolio as you add new projects.
Performance Benefits
And Core Web Vitals love it.
Performance Considerations
Now let’s talk speed.
A slow portfolio is a silent killer. It makes you look outdated, even if your skills aren’t. Here are the non-negotiables I aim for:
- LCP < 2.5s → hero content must load fast. I use next/image with AVIF/WebP, lazy-load below the fold.
- INP < 200ms → interactions should feel instant. That means clean focus states, minimal client JS, and no bloat libraries.
- CLS < 0.1 → no layout shifts. Font subsetting, stable containers, and consistent image ratios. Supporting practices:
- Font strategy: local fonts, subset weights, display: swap.
- Code strategy: dynamic imports, tree-shaking, no heavy libs.
- Caching strategy: ISR (Incremental Static Regeneration) and smart edge caching on Vercel.
- Monitoring: WebPageTest + Lighthouse + Vercel Analytics.
The result? Portfolios that hit 90+ Lighthouse scores on key pages.
Why Performance Matters for a Portfolio
And that’s not vanity. It’s trust.
Why This Matters for a Portfolio
Clients don’t just want to see that you can code. They want to feel that you can deliver.
A modern portfolio does three things:
- Loads in seconds → proves discipline and attention to detail.
- Feels interactive → shows you care about users, not just visuals.
- Highlights results → moves the conversation from “nice design” to “business impact”. Here’s proof:
- A SaaS onboarding flow I rebuilt cut Interaction Latency by 50% and boosted activation rates by 11 percentage points.
- An e-commerce checkout I optimised cut cart abandonment by 16 percentage points.
- A lead-gen landing page lifted qualified enquiries by 45%.
Final Word
That’s what clients see in a portfolio. Not just screens — but outcomes.
Wrap-Up
Building with Next.js 15 isn’t just about using the latest shiny tool. It’s about proving you can ship like a pro.
Fast prototypes. Clean builds. Performance that speaks louder than claims.
Your portfolio is your runway. Get it wrong, and you stall. You can get it right, and you launch.
Advanced App Router Features:
- 📁 Co-located Layouts: Define layouts alongside routes for better organization
- 🔄 Nested Routing: Natural hierarchy that mirrors your UI structure
- ⚡ Loading & Error States: Built-in
loading.tsx
anderror.tsx
support - 📂 Route Groups: Organize routes with
()
without affecting URL paths - 🎯 Parallel Routes: Load multiple pages simultaneously with
@folder
syntax - 🔀 Intercepting Routes: Capture routes for modals and overlays
- 🛡️ Route Handlers: API routes with full middleware support
� Server Components: Zero JavaScript by Default
Server Components revolutionize performance by rendering on the server:
// app/products/page.tsx - Server Component (default)
async function ProductsPage() {
// This runs on the server - no client bundle impact!
const products = await fetch('https://api.example.com/products')
.then(res => res.json())
return (
<div className="products-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
export default ProductsPage
Performance Impact:
- =� Smaller Bundle Size: Server logic stays on the server
- � Faster Initial Load: No JavaScript needed for static content
- = Enhanced Security: Sensitive operations remain server-side
- =� Better SEO: Fully rendered HTML for search engines
<� Client Components: Interactive When Needed
Use Client Components selectively for interactivity:
// components/SearchBar.tsx - Interactive component
'use client'
import { useState } from 'react'
export function SearchBar() {
const [query, setQuery] = useState('')
return (
<div className="search-container">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
className="search-input"
/>
<SearchResults query={query} />
</div>
)
}
Advanced Data Fetching Patterns
Parallel Data Loading
Fetch multiple data sources simultaneously:
// app/dashboard/page.tsx - Parallel data fetching
async function DashboardPage() {
// These requests happen in parallel!
const [user, analytics, notifications] = await Promise.all([
getUser(),
getAnalytics(),
getNotifications()
])
return (
<div className="dashboard">
<UserProfile user={user} />
<AnalyticsChart data={analytics} />
</div>
)
}
Streaming with Suspense
Improve perceived performance with streaming:
// app/dashboard/page.tsx - Streaming data
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<div className="dashboard">
<h1>Dashboard</h1>
{/* This loads immediately */}
<QuickStats />
{/* This streams in when ready */}
<Suspense fallback={<AnalyticsLoading />}>
<AnalyticsChart />
</Suspense>
<Suspense fallback={<RecentOrdersLoading />}>
<RecentOrders />
</Suspense>
</div>
)
}
// This component streams when data is ready
async function AnalyticsChart() {
const data = await getAnalyticsData() // This can be slow
return <Chart data={data} />
}
Performance Optimizations
Image Optimization
Next.js 15's Image component delivers exceptional performance:
import Image from 'next/image'
export function ProductGrid({ products }) {
return (
<div className="grid grid-cols-3 gap-4">
{products.map(product => (
<div key={product.id} className="product-card">
<Image
src={product.image}
alt={product.name}
width={300}
height={200}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
priority={product.featured}
/>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
))}
</div>
)
}
Image Features:
- =� Automatic WebP/AVIF: Modern formats for optimal compression
- =� Responsive Images: Serve appropriate sizes for each device
- � Lazy Loading: Load images as they enter the viewport
- <� Blur Placeholders: Smooth loading experience
Font Optimization
Eliminate layout shift with optimized fonts:
// app/layout.tsx - Font optimization
import { Inter, Playfair_Display } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
})
const playfair = Playfair_Display({
subsets: ['latin'],
display: 'swap',
variable: '--font-playfair',
})
export default function RootLayout({ children }) {
return (
<html lang="en" className={`${inter.variable} ${playfair.variable}`}>
<body className="font-sans">
{children}
</body>
</html>
)
}
Advanced Routing Patterns
Dynamic Routes with Type Safety
// app/products/[category]/[slug]/page.tsx
interface ProductPageProps {
params: {
category: string
slug: string
}
searchParams: {
color?: string
size?: string
}
}
export default function ProductPage({ params, searchParams }: ProductPageProps) {
const { category, slug } = params
const { color, size } = searchParams
return (
<div>
<h1>Product: {slug}</h1>
<p>Category: {category}</p>
{color && <p>Color: {color}</p>}
{size && <p>Size: {size}</p>}
</div>
)
}
// Generate static params for better performance
export async function generateStaticParams() {
const products = await getProducts()
return products.map(product => ({
category: product.category,
slug: product.slug,
}))
}
Route Groups and Layouts
Organize your application structure:
app/
�� (marketing)/
�� layout.tsx
�� page.tsx
�� about/
�� page.tsx
�� (shop)/
�� layout.tsx
�� products/
�� page.tsx
�� cart/
�� page.tsx
�� (auth)/
�� login/
�� page.tsx
�� register/
�� page.tsx
State Management in Next.js 15
Server State vs Client State
// Server State - Fetch once, cache efficiently
async function ProductList() {
const products = await getProducts() // Server-side
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
// Client State - Interactive state management
'use client'
function ShoppingCart() {
const [items, setItems] = useLocalStorage('cart', [])
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>
Cart ({items.length})
</button>
{isOpen && <CartDropdown items={items} />}
</div>
)
}
Deployment and Performance
Edge Runtime
Deploy functions to the edge for global performance:
// app/api/edge/route.ts
export const runtime = "edge";
export async function GET() {
return new Response("Hello from the edge!", {
headers: {
"content-type": "text/plain",
},
});
}
Analytics and Monitoring
Track Core Web Vitals:
// app/layout.tsx - Web vitals tracking
import { Analytics } from '@vercel/analytics/react'
import { SpeedInsights } from '@vercel/speed-insights/next'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
<SpeedInsights />
</body>
</html>
)
}
Best Practices for Production
1. Component Architecture
- Use Server Components by default
- Add 'use client' only when needed
- Compose Client and Server Components effectively
2. Performance First
- Implement streaming for slow data
- Use Suspense boundaries strategically
- Optimize images and fonts
3. SEO Optimization
// app/products/[slug]/page.tsx - Dynamic metadata
export async function generateMetadata({ params }) {
const product = await getProduct(params.slug);
return {
title: `${product.name} - Your Store`,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
images: [product.image],
},
};
}
4. Error Handling
// app/error.tsx - Global error boundary
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="error-container">
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
)
}
Real-World Example: E-commerce App
Here's how all these concepts come together in a production app:
// app/shop/products/page.tsx - Complete example
import { Suspense } from 'react'
import { ProductGrid } from '@/components/ProductGrid'
import { CategoryFilter } from '@/components/CategoryFilter'
import { SearchBar } from '@/components/SearchBar'
interface ProductsPageProps {
searchParams: {
category?: string
search?: string
page?: string
}
}
export default function ProductsPage({ searchParams }: ProductsPageProps) {
return (
<div className="products-page">
<div className="filters-section">
<CategoryFilter />
<SearchBar />
</div>
<Suspense fallback={<ProductGridLoading />}>
<ProductGrid searchParams={searchParams} />
</Suspense>
</div>
)
}
// This component streams in when data is ready
async function ProductGrid({ searchParams }) {
const products = await getProducts({
category: searchParams.category,
search: searchParams.search,
page: Number(searchParams.page) || 1,
})
if (products.length === 0) {
return <EmptyState />
}
return (
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
Migration Guide
From Pages Router to App Router
// Old: pages/products/[id].js
export async function getServerSideProps({ params }) {
const product = await getProduct(params.id)
return { props: { product } }
}
export default function Product({ product }) {
return <div>{product.name}</div>
}
// New: app/products/[id]/page.tsx
export default async function Product({ params }) {
const product = await getProduct(params.id)
return <div>{product.name}</div>
}
Conclusion
Next.js 15 represents the future of React development, offering unprecedented performance, developer experience, and scalability. By embracing Server Components, the App Router, and modern data fetching patterns, you can build applications that are:
- � Lightning Fast - Server Components eliminate unnecessary JavaScript
- <� SEO Optimized - Server-side rendering with streaming capabilities
- =' Developer Friendly - Intuitive patterns and excellent tooling
- =� Infinitely Scalable - Edge runtime and optimized deployments
Start building your next project with Next.js 15 and experience the future of web development today!
Resources
- =� Official Next.js 15 Documentation
- <� Next.js 15 Video Tutorial Series
- =� Example Projects Repository
- =� Deploy on Vercel
Ready to build something amazing? Start your Next.js 15 project today and join the thousands of developers creating the next generation of web applications. Ready to build something amazing? Start your Next.js 15 project today and join the thousands of developers creating the next generation of web applications.