Performance Overview
Built-in optimizations for fast-loading applications
The framework delivers fast page loads through Next.js 15's automatic optimizations. Code splitting, edge caching, and asset optimization work by default without manual configuration.
Pro Tip
Performance optimizations are built-in: automatic code splitting, aggressive tree-shaking via optimizePackageImports
, and CDN-friendly caching for public content.
Core Optimizations
Code Splitting
Route-level code splitting - users download only what they need
Edge Caching
Public data serves from CDN locations near users
Asset Optimization
Images and fonts load in optimal formats without layout shifts
Bundle Analysis
Visual treemaps reveal optimization opportunities
Automatic Code Splitting
Next.js 15 splits code at route boundaries automatically. Users visiting /dashboard
don't download /billing
JavaScript. Each route loads independently.
Tree-Shaking Configuration
The framework enables aggressive tree-shaking for large dependencies:
import { INTERNAL_PACKAGES } from "@workspace/ui/lib/internal-packages";
export default {
experimental: {
optimizePackageImports: ["lucide-react", ...INTERNAL_PACKAGES],
},
};
This transforms icon imports from bundling entire libraries to importing only used icons:
// Before optimization: ~500KB bundle
import { CheckIcon, XIcon, AlertIcon } from "lucide-react";
// After optimization: ~5KB bundle
// Next.js imports only these 3 icons
Pro Tip
Add heavy dependencies to optimizePackageImports
to enable automatic tree-shaking. Works for lucide-react
, @radix-ui/*
, and most component libraries.
Streaming and Suspense
Server components stream HTML progressively. Users see page structure immediately while data loads:
import { Suspense } from "react";
export default function DashboardPage() {
return (
<>
<DashboardHeader /> {/* Renders immediately */}
<Suspense fallback={<TicketsSkeleton />}>
<SupportTickets /> {/* Streams when data is ready */}
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity /> {/* Streams independently */}
</Suspense>
</>
);
}
Each Suspense
boundary streams independently. Fast queries render before slow ones. No waterfall blocking.
Heads up
Suspense requires async server components. Client components with 'use client'
cannot use Suspense for data fetching - use loading states instead.
Static vs Dynamic Rendering
Marketing App (Static)
Public pages use static generation with periodic revalidation:
export const revalidate = 600; // Regenerate every 10 minutes
export default function PricingPage() {
return <PricingTable />;
}
Benefits:
- Instant response from edge CDN
- No server requests for cached pages
- Automatic global distribution
Dashboard App (Dynamic)
User-specific pages require fresh data:
export const revalidate = 0;
export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export default async function DashboardPage() {
const session = await getCurrentSession();
// Return user-specific content
}
Why dynamic:
- User data must never be cached between users
- Billing state changes require fresh queries
- Security: stale sessions are rejected immediately
Measuring Performance
pnpm lighthouse
Runs Lighthouse CI on key pages with mobile throttling. Generates reports in apps/marketing/reports/lighthouse/
.
ANALYZE=true pnpm build
Opens interactive treemap showing JavaScript distribution. Identify heavy dependencies and optimization opportunities.
pnpm dev --turbopack
Turbopack provides near-instant HMR during development. Fast iteration improves developer experience.
Core Web Vitals
Target metrics for production:
Metric | Description | Target |
---|---|---|
LCP | Largest Contentful Paint - Loading performance | Under 2.5s |
INP | Interaction to Next Paint - Interactivity | Under 200ms |
CLS | Cumulative Layout Shift - Visual stability | Under 0.1 |
Pro Tip
Use next/image
and next/font
components to automatically optimize LCP and CLS. These components handle format selection, lazy loading, and layout reservation.
Performance Patterns
Lazy Load Heavy Components
import dynamic from "next/dynamic";
// Load chart library only when needed
const Chart = dynamic(() => import("~/components/chart"), {
loading: () => <ChartSkeleton />,
ssr: false, // Skip server-side rendering for client-only libs
});
export default function AnalyticsPage() {
return (
<div>
<h1>Analytics</h1>
<Chart data={data} />
</div>
);
}
Optimize Third-Party Scripts
import Script from "next/script";
export default function Layout({ children }) {
return (
<html>
<body>
{children}
{/* Load analytics after page interactive */}
<Script
src="https://analytics.example.com/script.js"
strategy="lazyOnload"
/>
</body>
</html>
);
}
Script strategies:
beforeInteractive
- Critical scripts (rare)afterInteractive
- Analytics, tracking (default)lazyOnload
- Non-critical widgets