0Pricing
Next.js 15 Fullstack (App Router + Server Actions) · Leçon

Importations dynamiques, division du code et hydratation différée

Différez les composants non critiques avec next/dynamic et ajustez le chargement pour réduire le délai avant interaction.

Importations dynamiques, division du code et hydratation différée est une leçon Next.js 15 Fullstack (App Router + Server Actions) gratuite sur CoddyKit. Ceci est la leçon 4 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage Next.js 15 Fullstack (App Router + Server Actions), et ta progression se synchronise sur le web et l'application CoddyKit. Le cours Next.js 15 Fullstack (App Router + Server Actions) comprend 4 leçons au total.

Certaines parties de cette leçon n'ont pas encore été traduites et s'affichent en anglais.

Why Code Splitting Matters in Next.js 15

Modern Next.js applications can grow large fast. Without code splitting, the browser downloads every component and dependency on the first page load — even code the user may never need.

Next.js 15 splits your bundle automatically at the route level using the App Router. But route-level splitting alone is not enough. Consider these common performance killers:

  • A rich text editor loaded on every page but only used in the admin dashboard
  • A heavy chart library rendered below the fold
  • A modal or drawer that is never opened by most visitors

For these cases you need component-level code splitting via next/dynamic and lazy hydration strategies. Together they reduce your initial JavaScript payload, improve Time-to-Interactive (TTI), and raise your Core Web Vitals scores.

Introducing next/dynamic

next/dynamic is Next.js's wrapper around React.lazy with additional features tailored for SSR. It returns a component that is loaded on demand — the JavaScript for that component is placed in a separate chunk and only fetched when the component is about to render.

Basic usage is straightforward:

  • Import dynamic from 'next/dynamic'
  • Pass a factory function that returns a dynamic import()
  • Optionally pass a loading fallback component shown while the chunk downloads

The resulting component can be used exactly like any normal React component in your JSX.

'use client';

import dynamic from 'next/dynamic';

// The HeavyEditor chunk is NOT included in the initial bundle.
// It is fetched only when <HeavyEditor /> is first rendered.
const HeavyEditor = dynamic(
  () => import('@/components/HeavyEditor'),
  {
    loading: () => <p>Loading editor…</p>,
  }
);

export default function AdminPage() {
  return (
    <main>
      <h1>Admin Dashboard</h1>
      <HeavyEditor />
    </main>
  );
}

Disabling SSR for Client-Only Components

Some components depend on browser APIs (window, document, localStorage) and cannot run on the server at all. Attempting SSR on these causes hydration mismatches or runtime errors.

next/dynamic supports an ssr: false option that tells Next.js to skip server rendering entirely for that component. The placeholder (or nothing) is sent in the HTML, and the real component is mounted only in the browser.

Common use cases for ssr: false:

  • Canvas / WebGL renderers
  • Browser-only animation libraries (e.g. GSAP ScrollTrigger)
  • Components that read window.matchMedia on mount
  • Third-party widgets that inject into document.body
'use client';

import dynamic from 'next/dynamic';

// This component uses `window` and `document` internally.
// ssr: false prevents Next.js from attempting to render it on the server.
const ConfettiBlast = dynamic(
  () => import('@/components/ConfettiBlast'),
  {
    ssr: false,
    loading: () => null, // render nothing until JS loads
  }
);

export default function CelebrationBanner() {
  return (
    <section>
      <h2>You did it! 🎉</h2>
      <ConfettiBlast particleCount={200} />
    </section>
  );
}

Named Exports and next/dynamic

By default next/dynamic expects the imported module to have a default export. When you need to dynamically import a named export, you must extract it inside the factory function.

This is done by returning the named export from the async factory, effectively making it the default for the dynamic wrapper:

'use client';

import dynamic from 'next/dynamic';

// Module exports: { LineChart, BarChart, PieChart }
// We only need LineChart — extract it inside the factory.
const LineChart = dynamic(
  () =>
    import('@/components/charts/ChartLibrary').then(
      (mod) => mod.LineChart
    ),
  { loading: () => <div className="h-64 animate-pulse bg-gray-100" /> }
);

interface SalesChartProps {
  data: { month: string; revenue: number }[];
}

export default function SalesChart({ data }: SalesChartProps) {
  return <LineChart data={data} width={600} height={300} />;
}

Conditional Dynamic Imports — Load on Interaction

The most powerful pattern is to defer a component until the user actually needs it — triggered by a click, hover, or scroll event. This avoids loading the chunk even during idle time.

The approach uses React state to conditionally render the dynamically imported component. Until the user interacts, the chunk is never requested. Once they click (or trigger the condition), React renders the dynamic component and the browser fetches the chunk on demand.

This pattern is ideal for:

  • Modals and drawers opened by a button
  • Settings panels
  • Video players that start on play
  • Comment sections below a long article
'use client';

import { useState } from 'react';
import dynamic from 'next/dynamic';

const FeedbackModal = dynamic(
  () => import('@/components/FeedbackModal'),
  { loading: () => <p>Opening…</p> }
);

export default function FeedbackButton() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button
        onClick={() => setOpen(true)}
        className="btn-primary"
      >
        Leave Feedback
      </button>

      {/* FeedbackModal chunk is only fetched after the first click */}
      {open && (
        <FeedbackModal onClose={() => setOpen(false)} />
      )}
    </>
  );
}

Lazy Hydration with 'use client' Boundaries

In the App Router, components are React Server Components (RSC) by default. Only components marked 'use client' ship JavaScript to the browser and hydrate.

This means you get free lazy hydration by keeping components as Server Components whenever possible. The server renders the HTML; no hydration cost is paid at all.

When you do need interactivity, push the 'use client' boundary as far down the tree as possible — to the exact leaf component that needs event handlers or state. Parent layout and wrapper components stay as RSC and add zero client JS.

  • Bad: Mark the entire page layout as 'use client' because one button needs onClick
  • Good: Extract only the button into its own 'use client' component; the rest stays RSC
// app/products/[id]/page.tsx — Server Component (no 'use client')
import { getProduct } from '@/lib/db';
import AddToCartButton from '@/components/AddToCartButton'; // 'use client'

interface PageProps {
  params: { id: string };
}

export default async function ProductPage({ params }: PageProps) {
  // Runs on the server — zero client JS for this component
  const product = await getProduct(params.id);

  return (
    <article>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p className="price">${product.price}</p>

      {/* Only this leaf component ships client JS */}
      <AddToCartButton productId={product.id} />
    </article>
  );
}

Intersection Observer — Hydrate on Scroll

Components below the fold do not need to hydrate immediately. A popular pattern is to use the Intersection Observer API to defer mounting (and therefore hydrating) a component until it scrolls into the viewport.

You can combine this with next/dynamic to achieve true lazy hydration: the JS chunk is requested only when the component enters the viewport, and mounting happens right after the chunk arrives.

Libraries like react-intersection-observer make this ergonomic. The pattern below mounts CommentsSection only once the user scrolls near it:

'use client';

import { useRef, useState, useEffect } from 'react';
import dynamic from 'next/dynamic';

const CommentsSection = dynamic(
  () => import('@/components/CommentsSection'),
  { loading: () => <div className="h-32 animate-pulse bg-gray-100" /> }
);

export default function ArticlePage() {
  const sentinelRef = useRef<HTMLDivElement>(null);
  const [showComments, setShowComments] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setShowComments(true);
          observer.disconnect(); // hydrate once, then stop observing
        }
      },
      { rootMargin: '200px' } // start loading 200px before viewport
    );

    if (sentinelRef.current) observer.observe(sentinelRef.current);
    return () => observer.disconnect();
  }, []);

  return (
    <article>
      <h1>A Very Long Article</h1>
      <p>…article content…</p>

      {/* Sentinel sits at the bottom; triggers chunk fetch when visible */}
      <div ref={sentinelRef} />
      {showComments && <CommentsSection />}
    </article>
  );
}

Preloading Chunks on Hover

Waiting until a click to start downloading a chunk adds latency: the user sees a spinner while the network request completes. A smarter UX is to preload the chunk on hover — typically 100–300 ms before the user clicks, which is often enough time for the chunk to arrive.

next/dynamic exposes a static .preload() method on the returned component. Calling it triggers the dynamic import without rendering anything, priming the browser cache so that when the component mounts it is nearly instant.

'use client';

import dynamic from 'next/dynamic';

const ShareDialog = dynamic(
  () => import('@/components/ShareDialog')
);

import { useState } from 'react';

export default function ShareButton() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button
        // Preload the chunk as soon as the user hovers
        onMouseEnter={() => ShareDialog.preload()}
        // Open (render) on click — chunk is likely already cached
        onClick={() => setOpen(true)}
        className="btn-secondary"
      >
        Share
      </button>

      {open && <ShareDialog onClose={() => setOpen(false)} />}
    </>
  );
}

Analyzing Bundle Size with @next/bundle-analyzer

Before you can optimise bundle size you need to see it. The @next/bundle-analyzer package wraps Webpack Bundle Analyzer and generates a visual treemap of every module in your build.

Setup is two steps:

  • Install: npm install --save-dev @next/bundle-analyzer
  • Wrap your Next.js config with the analyzer

Run ANALYZE=true next build and two browser tabs open — one for the client bundle, one for the server bundle. Look for:

  • Unexpectedly large modules in the initial chunk
  • Duplicate libraries (e.g. two versions of date-fns)
  • Libraries that should be dynamically imported but appear in the main chunk
// next.config.ts
import type { NextConfig } from 'next';
import bundleAnalyzer from '@next/bundle-analyzer';

const withBundleAnalyzer = bundleAnalyzer({
  enabled: process.env.ANALYZE === 'true',
  openAnalyzer: true,
});

const nextConfig: NextConfig = {
  // your existing config…
  experimental: {
    optimizePackageImports: [
      // Tell Next.js to tree-shake these icon/component libraries
      // so only the icons you actually import are bundled
      '@heroicons/react',
      'lucide-react',
      '@radix-ui/react-icons',
    ],
  },
};

export default withBundleAnalyzer(nextConfig);

Dynamic Imports in Server Components

You can also use dynamic import() inside Server Components — not via next/dynamic, but via plain ES dynamic import. The result is still server-rendered; the benefit is conditional loading on the server: you avoid importing heavy modules when they are not needed for a given request.

A typical case is locale-specific data, feature-flag-gated renderers, or optional plugins loaded based on configuration:

// app/report/page.tsx — Server Component
import type { NextPage } from 'next';

interface ReportPageProps {
  searchParams: { format?: string };
}

const ReportPage: NextPage<ReportPageProps> = async ({ searchParams }) => {
  const format = searchParams.format ?? 'html';

  if (format === 'pdf') {
    // Heavy PDF renderer is only imported when the query param is 'pdf'.
    // It never reaches the browser — this is pure server-side splitting.
    const { renderPDF } = await import('@/lib/pdf-renderer');
    const pdfBuffer = await renderPDF({ title: 'Q2 Report' });

    return new Response(pdfBuffer, {
      headers: { 'Content-Type': 'application/pdf' },
    }) as unknown as JSX.Element;
  }

  // Default: lightweight HTML version
  const { ReportView } = await import('@/components/ReportView');
  return <ReportView />;
};

export default ReportPage;

Measuring Impact — Core Web Vitals

Code splitting and lazy hydration directly improve two Core Web Vitals:

  • LCP (Largest Contentful Paint) — less blocking JS means the browser paints the largest element sooner
  • INP (Interaction to Next Paint) — smaller main-thread work during load means the page responds faster to the first user tap

Measure before and after your changes using:

  • next build && next start + Chrome DevTools Lighthouse (local, reproducible)
  • web-vitals npm package + the useReportWebVitals hook from next/navigation to log metrics to your analytics backend in production
  • Vercel Speed Insights or Google Search Console for real-user data

A common result after switching a heavy component to next/dynamic: the initial JS payload drops by 30–60 KB (gzipped), translating to 200–500 ms faster TTI on a median mobile connection.

Knowledge Check: When to Use ssr: false

You are integrating a third-party mapping library that accesses window.navigator.geolocation synchronously during module initialisation. Which next/dynamic configuration is correct and why?

Recap — Dynamic Imports and Lazy Hydration

In this lesson you learned how to cut time-to-interactive in Next.js 15 App Router applications by deferring JavaScript that is not needed upfront.

Key takeaways:

  • next/dynamic splits a component into a separate chunk fetched on demand — reducing the initial JS payload
  • ssr: false skips server rendering for components that rely on browser-only APIs, preventing runtime errors
  • Named exports are handled by extracting them inside the factory: import(…).then(mod => mod.Named)
  • Conditional rendering ({open && <Modal />}) defers the chunk fetch until the first render of the component
  • Component.preload() on hover primes the cache before the user clicks, hiding network latency
  • Keeping 'use client' boundaries as narrow as possible gives you free lazy hydration for the RSC parts of your tree
  • Intersection Observer enables viewport-triggered hydration for below-the-fold content
  • @next/bundle-analyzer and useReportWebVitals let you measure the real impact of these optimisations

Apply these techniques to the heaviest components in your bundle first — editors, charts, maps, and media players — for the greatest TTI gains.

Questions Fréquemment Posées

La leçon « Importations dynamiques, division du code et hydratation différée » est-elle gratuite ?

Oui — le texte complet de « Importations dynamiques, division du code et hydratation différée » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours Next.js 15 Fullstack (App Router + Server Actions), passe à CoddyKit PRO. Le cours Next.js 15 Fullstack (App Router + Server Actions) comprend 4 leçons au total.

Qu'est-ce que j'apprendrai dans « Importations dynamiques, division du code et hydratation différée » ?

Différez les composants non critiques avec next/dynamic et ajustez le chargement pour réduire le délai avant interaction. Tu pratiques Next.js 15 Fullstack (App Router + Server Actions) avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.

Dois-je avoir de l'expérience pour commencer Next.js 15 Fullstack (App Router + Server Actions) ?

Aucune expérience préalable n'est requise. Next.js 15 Fullstack (App Router + Server Actions) sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 4 sur 4.

Combien de temps prend la leçon « Importations dynamiques, division du code et hydratation différée » ?

La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.

Peux-tu écrire et exécuter du code dans cette leçon Next.js 15 Fullstack (App Router + Server Actions) ?

Oui. Chaque leçon Next.js 15 Fullstack (App Router + Server Actions) inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.

Toutes les leçons de ce cours

  1. Analyser et réduire le paquet client
  2. Exploration approfondie de la configuration de Turbopack et du compilateur
  3. Frontières entre modules avec server-only et client-only
  4. Importations dynamiques, division du code et hydratation différée
← Retour à Next.js 15 Fullstack (App Router + Server Actions)