Performance: Images, Code Splitting, Lazy Loading, Caching
Use next/image instead of <img>. It serves modern formats, resizes for the device, and can lazy-load below the fold.
Image optimization
Use next/image instead of <img>. It serves modern formats, resizes for the device, and can lazy-load below the fold.
Always set width/height or fill with a sized parent to avoid layout shift.
Real-life example: next/image is a smart photo lab — it prints the right size print for phone vs billboard, so you do not mail a giant poster to a pocket.
import Image from "next/image";
export function HeroPhoto() {
return (
<Image
src="/images/classroom.jpg"
alt="Students learning Next.js"
width={1200}
height={630}
priority // above-the-fold hero
/>
);
}Code splitting & lazy loading
Next.js splits code by route automatically. For heavy client-only widgets (charts, editors), use next/dynamic with ssr: false or a loading placeholder.
Real-life example: Lazy loading is unpacking the camping stove only when you reach the campsite — not carrying a lit stove on the whole train ride.
import dynamic from "next/dynamic";
const EnrollmentChart = dynamic(
() => import("@/components/EnrollmentChart"),
{
loading: () => <p>Loading chart…</p>,
ssr: false,
}
);
export function AnalyticsPanel() {
return <EnrollmentChart />;
}Caching strategies
Understand three layers: browser cache, Next.js Data Cache / Full Route Cache, and CDN cache on Vercel.
fetch options and revalidatePath / revalidateTag control freshness after Server Actions.
Real-life example: Caching is a fridge leftover plan — static salad lasts days (long revalidate); hot soup needs remaking often (no-store or short revalidate).
- fetch(url, { next: { revalidate: 3600 } }) — ISR-style refresh each hour
- fetch(url, { cache: 'no-store' }) — always fresh (dashboard-like)
- revalidateTag('courses') after mutations
// Cached for 1 hour
const catalog = await fetch("https://api.example.com/courses", {
next: { revalidate: 3600, tags: ["courses"] },
});
// Always fresh
const me = await fetch("https://api.example.com/me", {
cache: "no-store",
headers: { Cookie: "..." },
});Bundle tips & Google Analytics note
Prefer Server Components by default. Mark "use client" only when you need state, effects, or browser APIs. Check the bundle with @next/bundle-analyzer when pages feel heavy.
For Google Analytics (or any third-party script), load it with next/script after interactive so it does not block first paint. Respect consent laws where you publish.
Real-life example: Third-party scripts are guests at a dinner — invite them after the main course (page content) is served, not before the cook starts.
import Script from "next/script";
export function GoogleAnalytics({ gaId }: { gaId: string }) {
return (
<>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}
strategy="afterInteractive"
/>
<Script id="ga-init" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${gaId}');
`}
</Script>
</>
);
}