Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) · Les

Dynamische imports, code splitting en lazy hydration

Stel niet-kritieke componenten uit met next/dynamic en stem het laden af om time-to-interactive te verkorten.

Les 4 van 413 stappen

Dynamische imports, code splitting en lazy hydration is een gratis Fullstackontwikkeling met Next.js 15 (App Router + Server Actions)-les op CoddyKit. Dit is les 4 van 4. Je kunt 3 lessen uit dit leerpad gratis volledig lezen — daarna ontgrendelt CoddyKit PRO alle lessen, plus praktische oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Fullstackontwikkeling met Next.js 15 (App Router + Server Actions). Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) bevat in totaal 4 lessen.

Waarom het splitsen van code belangrijk is in Next.js 15

Moderne Next.js-toepassingen kunnen snel groot worden. Zonder codesplitsing downloadt de browser bij het laden van de eerste pagina elk component en elke afhankelijkheid — zelfs code die de gebruiker misschien nooit nodig heeft.

Next.js 15 splitst je bundel automatisch op routeniveau met de App Router. Alleen splitsen op routeniveau is echter niet genoeg. Denk aan deze veelvoorkomende prestatieremmers:

  • Een rich-texteditor die op elke pagina wordt geladen, maar alleen in het beheerdashboard wordt gebruikt
  • Een zware grafiekbibliotheek die onder het zichtbare deel van de pagina wordt weergegeven
  • Een modaal venster of zijpaneel dat door de meeste bezoekers nooit wordt geopend

Voor deze gevallen heb je codesplitsing op componentniveau via next/dynamic en strategieën voor uitgestelde hydratatie nodig. Samen verkleinen ze de hoeveelheid JavaScript die aanvankelijk wordt geladen, verbeteren ze de Time-to-Interactive (TTI) en verhogen ze je Core Web Vitals-scores.

Introductie tot next/dynamic

next/dynamic is de wrapper van Next.js rond React.lazy, met extra functies die zijn afgestemd op SSR. Het levert een component op dat op aanvraag wordt geladen — de JavaScript voor dat component wordt in een aparte chunk geplaatst en pas opgehaald wanneer het component bijna wordt weergegeven.

De basis is eenvoudig:

  • Importeer dynamic uit 'next/dynamic'
  • Geef een fabrieksfunctie door die een dynamische import() retourneert
  • Geef desgewenst een terugvalcomponent loading door dat wordt weergegeven terwijl de chunk wordt gedownload

Je kunt het resulterende component in je JSX precies zo gebruiken als elk ander React-component.

'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>
  );
}

SSR uitschakelen voor alleen-clientcomponenten

Sommige componenten zijn afhankelijk van browser-API's (window, document, localStorage) en kunnen helemaal niet op de server worden uitgevoerd. Als je op deze componenten SSR probeert toe te passen, ontstaan er verschillen tijdens de hydratatie of uitvoeringsfouten.

next/dynamic ondersteunt de optie ssr: false. Daarmee geef je Next.js de opdracht om serverweergave volledig over te slaan voor dat component. De tijdelijke aanduiding (of niets) wordt in de HTML verzonden en het echte component wordt pas in de browser gemonteerd.

Veelvoorkomende toepassingen van ssr: false:

  • Canvas- en WebGL-renderers
  • Animatiebibliotheken die alleen in de browser werken (bijvoorbeeld GSAP ScrollTrigger)
  • Componenten die bij het monteren window.matchMedia uitlezen
  • Widgets van derden die elementen invoegen in 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>
  );
}

Benoemde exports en next/dynamic

Standaard verwacht next/dynamic dat de geïmporteerde module een standaardexport heeft. Wanneer je dynamisch een benoemde export wilt importeren, moet je deze binnen de fabrieksfunctie selecteren.

Dit doe je door de benoemde export vanuit de asynchrone fabriek te retourneren, zodat deze voor de dynamische wrapper feitelijk de standaardexport wordt:

'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} />;
}

Voorwaardelijke dynamische imports — laden bij interactie

Het krachtigste patroon is om een component uit te stellen totdat de gebruiker het daadwerkelijk nodig heeft — bijvoorbeeld na een klik, muisaanwijzing of scrollgebeurtenis. Zo voorkom je dat de chunk zelfs tijdens inactiviteit wordt geladen.

Bij deze aanpak gebruik je de React-status om het dynamisch geïmporteerde component voorwaardelijk weer te geven. Zolang de gebruiker geen interactie heeft, wordt de chunk nooit opgevraagd. Zodra de gebruiker klikt (of de voorwaarde activeert), geeft React het dynamische component weer en haalt de browser de chunk op aanvraag op.

Dit patroon is ideaal voor:

  • Modale vensters en zijpanelen die met een knop worden geopend
  • Instellingenpanelen
  • Videospelers die bij het afspelen starten
  • Opmerkingensecties onder een lang artikel
'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)} />
      )}
    </>
  );
}

Uitgestelde hydratatie met grenzen voor 'use client'

In de App Router zijn componenten standaard React Server Components (RSC). Alleen componenten met de markering 'use client' sturen JavaScript naar de browser en worden gehydrateerd.

Dit betekent dat je gratis uitgestelde hydratatie krijgt door componenten waar mogelijk Server Components te laten blijven. De server rendert de HTML; er zijn helemaal geen hydratatiekosten.

Wanneer je wel interactiviteit nodig hebt, plaats je de grens voor 'use client' zo laag mogelijk in de boomstructuur — precies bij het bladcomponent dat gebeurtenishandlers of status nodig heeft. Bovenliggende layout- en wrappercomponenten blijven RSC en voegen geen client-JavaScript toe.

  • Slecht: markeer de volledige paginalayout als 'use client' omdat één knop onClick nodig heeft
  • Goed: haal alleen de knop onder in een eigen 'use client'-component; de rest blijft 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 — hydrateren tijdens het scrollen

Componenten onder het zichtbare deel van de pagina hoeven niet onmiddellijk te worden gehydrateerd. Een veelgebruikt patroon is de Intersection Observer API te gebruiken om het monteren (en daarmee het hydrateren) van een component uit te stellen totdat het tijdens het scrollen in de viewport verschijnt.

Je kunt dit combineren met next/dynamic voor echte uitgestelde hydratatie: de JS-chunk wordt pas opgevraagd wanneer het component in de viewport verschijnt en het monteren vindt direct na aankomst van de chunk plaats.

Bibliotheken zoals react-intersection-observer maken dit eenvoudig. Het onderstaande patroon monteert CommentsSection pas wanneer de gebruiker ernaartoe scrollt:

'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>
  );
}

Chunks vooraf laden bij mouse-over

Als je pas na een klik begint met het downloaden van een chunk, ontstaat er vertraging: de gebruiker ziet een laadindicator terwijl het netwerkverzoek wordt voltooid. Een slimmere gebruikerservaring is de chunk vooraf te laden bij mouse-over — meestal 100–300 ms voordat de gebruiker klikt, wat vaak genoeg tijd is om de chunk te laten aankomen.

next/dynamic stelt een statische methode .preload() beschikbaar op het geretourneerde component. Als je die aanroept, wordt de dynamische import gestart zonder iets weer te geven. Zo wordt de browsercache alvast gevuld en wordt het component bij het monteren vrijwel onmiddellijk geladen.

'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)} />}
    </>
  );
}

Bundelgrootte analyseren met @next/bundle-analyzer

Voordat je de bundelgrootte kunt optimaliseren, moet je deze kunnen zien. Het pakket @next/bundle-analyzer verpakt Webpack Bundle Analyzer en genereert een visuele treemap van elke module in je build.

De installatie bestaat uit twee stappen:

  • Installeren: npm install --save-dev @next/bundle-analyzer
  • Je Next.js-configuratie omwikkelen met de analyzer

Voer ANALYZE=true next build uit. Er worden twee browsertabbladen geopend — één voor de clientbundel en één voor de serverbundel. Let op:

  • Onverwacht grote modules in de aanvankelijke chunk
  • Dubbele bibliotheken (bijvoorbeeld twee versies van date-fns)
  • Bibliotheken die dynamisch geïmporteerd zouden moeten worden, maar in de hoofdchunk staan
// 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);

Dynamische imports in servercomponenten

Je kunt dynamische import() ook binnen Server Components gebruiken — niet via next/dynamic, maar via een gewone dynamische ES-import. Het resultaat wordt nog steeds op de server weergegeven; het voordeel is voorwaardelijk laden op de server: je voorkomt dat zware modules worden geïmporteerd wanneer ze voor een bepaald verzoek niet nodig zijn.

Typische gevallen zijn gegevens die specifiek zijn voor een taalgebied, renderers die door functievlaggen worden ingeschakeld of optionele plug-ins die op basis van configuratie worden geladen:

// 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;

Impact meten — Core Web Vitals

Codesplitsing en uitgestelde hydratatie verbeteren rechtstreeks twee Core Web Vitals:

  • LCP (Largest Contentful Paint) — minder blokkerende JavaScript betekent dat de browser het grootste element eerder weergeeft
  • INP (Interaction to Next Paint) — minder werk op de hoofdthread tijdens het laden betekent dat de pagina sneller reageert op de eerste tik van de gebruiker

Meet vóór en na je wijzigingen met:

  • next build && next start + Chrome DevTools Lighthouse (lokaal en reproduceerbaar)
  • het npm-pakket web-vitals + de hook useReportWebVitals uit next/navigation om metrieken in productie naar je analysetoepassing te loggen
  • Vercel Speed Insights of Google Search Console voor gegevens van echte gebruikers

Een veelvoorkomend resultaat nadat je een zwaar component hebt omgezet naar next/dynamic: de aanvankelijke hoeveelheid JavaScript daalt met 30–60 KB (gecomprimeerd met gzip), wat neerkomt op een 200–500 ms snellere TTI via een mobiele verbinding met gemiddelde snelheid.

Kennischeck: wanneer ssr: false gebruiken

Je integreert een bibliotheek voor kaarten van derden die tijdens de initialisatie van de module synchroon toegang heeft tot window.navigator.geolocation. Welke configuratie van next/dynamic is juist en waarom?

Samenvatting — dynamische imports en uitgestelde hydratatie

In deze les heb je geleerd hoe je de time-to-interactive in Next.js 15 App Router-toepassingen verkort door JavaScript die niet meteen nodig is, uit te stellen.

Belangrijkste punten:

  • next/dynamic splitst een component op in een aparte chunk die op aanvraag wordt opgehaald — zo wordt de aanvankelijke hoeveelheid JavaScript kleiner
  • ssr: false slaat serverweergave over voor componenten die afhankelijk zijn van browser-API's en voorkomt uitvoeringsfouten
  • Benoemde exports verwerk je door ze binnen de fabriek te selecteren: import(…).then(mod => mod.Named)
  • Voorwaardelijke weergave ({open && <Modal />}) stelt het ophalen van de chunk uit totdat het component voor het eerst wordt weergegeven
  • Component.preload() bij mouse-over vult de cache voordat de gebruiker klikt en verbergt zo de netwerkvertraging
  • Door de grenzen voor 'use client' zo smal mogelijk te houden, krijg je gratis uitgestelde hydratatie voor de RSC-delen van je boomstructuur
  • Intersection Observer maakt hydratatie van inhoud onder het zichtbare deel van de pagina mogelijk zodra die in de viewport verschijnt
  • Met @next/bundle-analyzer en useReportWebVitals kun je de werkelijke impact van deze optimalisaties meten

Pas deze technieken eerst toe op de zwaarste componenten in je bundel — editors, grafieken, kaarten en mediaspelers — voor de grootste TTI-winst.

Gratis beginnen

Leer TypeScript met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
22
Lessen
88

Veelgestelde vragen

Is de les “Dynamische imports, code splitting en lazy hydration” gratis?

Ja — je kunt hier op het web alle 3 lessen van het leerpad Fullstackontwikkeling met Next.js 15 (App Router + Server Actions), waaronder “Dynamische imports, code splitting en lazy hydration”, gratis volledig lezen. Daarna ontgrendelt CoddyKit PRO alle lessen, plus interactieve oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. De cursus Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) bevat in totaal 4 lessen.

Wat leer ik in “Dynamische imports, code splitting en lazy hydration”?

Stel niet-kritieke componenten uit met next/dynamic en stem het laden af om time-to-interactive te verkorten. Je oefent met Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) te beginnen?

Ervaring vooraf is niet nodig. Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 4 van 4.

Hoe lang duurt de les “Dynamische imports, code splitting en lazy hydration”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over Fullstackontwikkeling met Next.js 15 (App Router + Server Actions)?

Ja. Elke les over Fullstackontwikkeling met Next.js 15 (App Router + Server Actions) bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. De clientbundle analyseren en verkleinen
  2. Turbopack- en compilerconfiguratie uitgediept
  3. Modulegrenzen met server-only en client-only
  4. Dynamische imports, code splitting en lazy hydration
← Terug naar Fullstackontwikkeling met Next.js 15 (App Router + Server Actions)