Performance
Meet Core Web Vitals targets through code splitting, image optimisation, bundle analysis, and server-side rendering strategies.
Performance is a feature. A 1-second delay in page load time reduces conversions by 7%. Every 100ms improvement in LCP improves user engagement. Performance work is not premature optimisation — it is part of the definition of done for every page.
Core Web Vitals
Google's Core Web Vitals are the primary performance contract. They directly influence search ranking and reflect real user experience.
| Metric | Measures | Good | Needs improvement | Poor |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | Perceived load speed — when the main content is visible | ≤ 2.5s | 2.5–4.0s | > 4.0s |
| INP (Interaction to Next Paint) | Responsiveness — how fast UI responds to user input | ≤ 200ms | 200–500ms | > 500ms |
| CLS (Cumulative Layout Shift) | Visual stability — unexpected layout shifts during load | ≤ 0.1 | 0.1–0.25 | > 0.25 |
| TTFB (Time to First Byte) | Server response time | ≤ 800ms | 800–1800ms | > 1800ms |
| FCP (First Contentful Paint) | Time to first visible content | ≤ 1.8s | 1.8–3.0s | > 3.0s |
INP replaced FID (First Input Delay) as a Core Web Vital in March 2024. Ensure your performance monitoring tools are reporting INP, not FID.
Bundle Optimisation
Code splitting
Split large components and routes so that only the code needed for the current page is loaded. Next.js does route-level splitting automatically. Apply component-level splitting with dynamic() for large, non-critical components.
import dynamic from "next/dynamic"
// Heavy chart library — only loads when component is rendered
const RevenueChart = dynamic(() => import("@/components/charts/revenue-chart"), {
loading: () => <ChartSkeleton />,
ssr: false, // Client-only — avoid server rendering for canvas-based charts
})
// Below-fold content — defer loading
const ActivityFeed = dynamic(() => import("@/components/activity-feed"), {
loading: () => <FeedSkeleton />,
})
export default function DashboardPage() {
return (
<>
<DashboardMetrics /> {/* Always loads */}
<RevenueChart /> {/* Lazy loaded */}
<ActivityFeed /> {/* Lazy loaded */}
</>
)
}Avoiding bundle bloat
- Import only what you use:
import { format } from 'date-fns'notimport * as dateFns from 'date-fns' - Audit dependencies with
@next/bundle-analyzerbefore adding libraries - Prefer native browser APIs over libraries when the functionality is simple (e.g.,
Intl.DateTimeFormatinstead of moment.js) - Use server components for heavy libraries — they never contribute to the client bundle
// next.config.mjs
import bundleAnalyzer from "@next/bundle-analyzer"
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === "true",
})
export default withBundleAnalyzer({
// your next config
})
// Run: ANALYZE=true pnpm build
// Opens a visual treemap of your bundle — identify and eliminate large packagesRendering Strategies
| Strategy | When to use | Next.js implementation |
|---|---|---|
| Static (SSG) | Content that is the same for all users and changes infrequently | No fetch options, or fetch with cache: 'force-cache' |
| ISR (Incremental Static Regeneration) | Content that changes but can tolerate being slightly stale | fetch with next: { revalidate: 3600 } |
| SSR (Server-Side Rendering) | Content personalised per user or request, must be current | fetch with cache: 'no-store' |
| Streaming SSR | Pages with slow data dependencies — render fast parts first | Suspense boundaries + async Server Components |
| CSR (Client-Side Rendering) | Highly interactive UI that changes continuously | Dynamic import with ssr: false |
Image Optimisation
Images are the most common cause of poor LCP. Always use Next.js Image component — it handles WebP conversion, responsive sizes, lazy loading, and prevents CLS automatically.
import Image from "next/image"
// Above-fold hero image — preload with priority
<Image
src="/hero.jpg"
alt="Dashboard overview showing project metrics"
width={1200}
height={630}
priority // Preloads — use only for LCP image
sizes="100vw"
/>
// Below-fold content image — lazy by default
<Image
src={project.thumbnail}
alt={`${project.name} preview`}
width={400}
height={225}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>Font Loading
Unoptimised fonts cause both FCP delays and CLS (layout shifts when fonts swap). Use next/font to load fonts at build time with zero layout shift.
// app/layout.tsx
import { Inter, Geist_Mono } from "next/font/google"
const inter = Inter({
subsets: ["latin"],
variable: "--font-sans",
display: "swap",
preload: true,
})
const geistMono = Geist_Mono({
subsets: ["latin"],
variable: "--font-mono",
display: "swap",
})
export default function RootLayout({ children }) {
return (
<html className={`${inter.variable} ${geistMono.variable}`}>
<body>{children}</body>
</html>
)
}Measuring Performance
| Tool | What it measures | When to use |
|---|---|---|
| Lighthouse (DevTools) | LCP, INP, CLS, TTFB, FCP, accessibility, SEO | Development and CI — run on every major feature |
| WebPageTest | Real device + network simulation, waterfall analysis | Deep diagnosis of specific pages |
| Chrome DevTools Performance tab | JavaScript execution, long tasks, layout thrashing | Debugging specific INP or janky scroll issues |
| Vercel Speed Insights | Real User Monitoring (RUM) in production | Ongoing production monitoring |
| @next/bundle-analyzer | JavaScript bundle composition and size | Before shipping any new large dependency |
Full-Stack Product Understanding
Understand the backend, data, auth, infrastructure, and operational concerns that turn polished prototypes into reliable product software.
Testing Strategy
Build confidence through a layered testing strategy: unit tests for logic, integration tests for component contracts, and end-to-end tests for critical user paths.