Back to Articles
Frontend Engineering 2026-07-15 10 min read

Next.js 14 App Router Performance Optimization: Code Splitting & Core Web Vitals

Sameer Khan

Sameer Khan

Full Stack Developer & Software Engineer

Next.js React Performance Web Vitals Tailwind CSS TypeScript

Next.js 14 App Router Performance Optimization: Code Splitting & Core Web Vitals

Web performance directly correlates with user retention, conversion rates, and Google search ranking. Google's **Core Web Vitals** (Largest Contentful Paint, Interaction to Next Paint, Cumulative Layout Shift) serve as explicit ranking signals.

In Next.js 14 App Router, achieving a **100/100 Lighthouse score** requires leveraging React Server Components (RSC), strategic code splitting, font optimization, and zero layout shift image strategies.

---

1. Core Web Vitals Target Breakdown

- **LCP (Largest Contentful Paint):** Target under 1.2 seconds. Speed at which the main hero content paints. - **INP (Interaction to Next Paint):** Target under 50ms. Responsiveness to user clicks & interactions. - **CLS (Cumulative Layout Shift):** Target 0.00. Visual stability of page elements during load.

---

2. React Server Components vs. Client Components

In Next.js 14 App Router, components default to **Server Components**. Server Components execute exclusively on the server, outputting pure HTML stream chunks to the browser with **zero client JavaScript bundle overhead**.

Best Practices: - Keep data fetching inside Server Components. - Push "use client" directive down to the smallest interactive leaf nodes.

tsx
import { fetchProjects } from '@/lib/db';

export default async function ProjectList() { const projects = await fetchProjects();

return ( <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> {projects.map((project) => ( <ClientCard key={project.id} project={project} /> ))} </div> ); } ```

---

3. Below-the-Fold Code Splitting via Dynamic Imports

Heavy JavaScript libraries should never be bundled into the initial page load JS chunk. Use next/dynamic with skeleton fallbacks:

tsx
import dynamic from 'next/dynamic';

const HeavyChart = dynamic(() => import('@/components/HeavyChart'), { loading: () => <SkeletonLoader className="h-64 w-full" />, ssr: false, });

export default function AnalyticsDashboard() { return ( <section> <h2>Analytics Summary</h2> <HeavyChart /> </section> ); } ```

---

4. Summary & Key Takeaways

1. Default to **Server Components** to keep JavaScript bundle size minimal. 2. Use **next/dynamic** for heavy below-the-fold components to reduce initial hydration block time. 3. Optimize font loading using **next/font** with display swap to eliminate FOUT.

*Engineered by Sameer Khan — Full Stack Developer & Software Engineer.*