R
Rishtaara
Next.js: Zero to Full-Stack
Lesson 34 of 42Article20 min

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.

Optimized Image
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.

Dynamic import of a heavy chart
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 vs fresh fetch
// 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.

Tip: Put analytics in the root layout only after you understand privacy/consent for your audience. A blocked cookie banner is better than silent tracking.
Google Analytics with next/script
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>
    </>
  );
}