Next.js 15 i full stack (App Router + Server Actions) · Lektion

Dynamic imports, code splitting og lazy hydration

Udskyd ikke-kritiske komponenter med next/dynamic, og justér indlæsningen for at forkorte tiden til interaktivitet.

Lektion 4 af 413 trin

Dynamic imports, code splitting og lazy hydration er en gratis Next.js 15 i full stack (App Router + Server Actions)-lektion på CoddyKit. Dette er lektion 4 af 4. Du kan læse alle 3 lektioner i dette læringsspor gratis i deres fulde længde — derefter låser CoddyKit PRO alle lektioner op samt praktiske øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. Den er en del af læringsforløbet i Next.js 15 i full stack (App Router + Server Actions), og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Next.js 15 i full stack (App Router + Server Actions)-kurset indeholder 4 lektioner i alt.

Hvorfor kodeopdeling er vigtig i Next.js 15

Moderne Next.js-applikationer kan hurtigt blive store. Uden kodeopdeling downloader browseren alle komponenter og afhængigheder ved den første sideindlæsning — også kode, som brugeren måske aldrig får brug for.

Next.js 15 opdeler automatisk dit bundle på routeniveau ved hjælp af App Router. Men opdeling på routeniveau alene er ikke nok. Overvej disse almindelige ydelsesproblemer:

  • En avanceret teksteditor, der indlæses på alle sider, men kun bruges i administrationspanelet
  • Et tungt diagram-bibliotek, der gengives under det synlige område
  • En modal eller skuffe, som de fleste besøgende aldrig åbner

I disse tilfælde har du brug for kodeopdeling på komponentniveau via next/dynamic samt strategier til forsinket hydrering. Tilsammen reducerer de den indledende JavaScript-mængde, forbedrer Time-to-Interactive (TTI) og hæver dine Core Web Vitals-resultater.

Introduktion til next/dynamic

next/dynamic er Next.js' omslag omkring React.lazy med ekstra funktioner, der er tilpasset SSR. Den returnerer en komponent, der indlæses efter behov — JavaScript-koden til komponenten placeres i en separat chunk og hentes først, når komponenten er ved at blive gengivet.

Den grundlæggende anvendelse er enkel:

  • Importér dynamic fra 'next/dynamic'
  • Angiv en fabrikfunktion, der returnerer en dynamisk import()
  • Angiv eventuelt en loading-reservekomponent, som vises, mens chunken downloades

Den resulterende komponent kan bruges præcis som enhver almindelig React-komponent i din 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>
  );
}

Deaktivering af SSR for klientkomponenter

Nogle komponenter er afhængige af browser-API'er (window, document, localStorage) og kan slet ikke køre på serveren. Hvis du forsøger at bruge SSR til disse, opstår der uoverensstemmelser under hydreringen eller fejl under kørsel.

next/dynamic understøtter indstillingen ssr: false, som fortæller Next.js, at servergengivelse skal springes helt over for denne komponent. En pladsholder (eller ingenting) sendes i HTML'en, og den rigtige komponent monteres først i browseren.

Almindelige anvendelser af ssr: false:

  • Canvas- og WebGL-renderere
  • Animationsbiblioteker, der kun fungerer i browseren (f.eks. GSAP ScrollTrigger)
  • Komponenter, der læser window.matchMedia ved montering
  • Tredjepartswidgets, der indsætter indhold i 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>
  );
}

Navngivne eksporter og next/dynamic

Som standard forventer next/dynamic, at det importerede modul har en standardeksport. Når du vil importere en navngiven eksport dynamisk, skal du udtrække den i fabrikfunktionen.

Det gør du ved at returnere den navngivne eksport fra den asynkrone fabrikfunktion, så den reelt bliver standardeksporten for det dynamiske omslag:

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

Betingede dynamiske importer — indlæsning ved interaktion

Det mest effektive mønster er at udskyde en komponent, indtil brugeren faktisk har brug for den — udløst af en klik-, svæve- eller rullehændelse. På den måde undgår du at indlæse chunken, selv når browseren er inaktiv.

Metoden bruger React-tilstand til betinget at gengive den dynamisk importerede komponent. Indtil brugeren interagerer, bliver chunken aldrig anmodet om. Når brugeren klikker (eller udløser betingelsen), gengiver React den dynamiske komponent, og browseren henter chunken efter behov.

Dette mønster er ideelt til:

  • Modale vinduer og skuffer, der åbnes med en knap
  • Indstillingspaneler
  • Videoafspillere, der starter ved afspilning
  • Kommentarsektioner under en 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)} />
      )}
    </>
  );
}

Forsinket hydrering med grænser for 'use client'

I App Router er komponenter som standard React-serverkomponenter (RSC). Kun komponenter, der er markeret med 'use client', sender JavaScript til browseren og hydreres.

Det betyder, at du får gratis forsinket hydrering ved at beholde komponenter som serverkomponenter, når det er muligt. Serveren gengiver HTML'en, og der betales slet ingen omkostning for hydrering.

Når du har brug for interaktivitet, skal du placere grænsen for 'use client' så langt nede i træet som muligt — på præcis den bladkomponent, der har brug for hændelsesbehandlere eller tilstand. Overordnede layout- og omslagskomponenter forbliver RSC og tilføjer ingen klient-JavaScript.

  • Dårligt: Markér hele sidelayoutet som 'use client', fordi én knap har brug for onClick
  • Godt: Udtræk kun knappen i sin egen 'use client'-komponent; resten forbliver 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 — hydrering ved rulning

Komponenter under det synlige område behøver ikke at blive hydreret med det samme. Et populært mønster er at bruge Intersection Observer API til at udskyde monteringen (og dermed hydreringen) af en komponent, indtil den ruller ind i visningsområdet.

Du kan kombinere dette med next/dynamic for at opnå ægte forsinket hydrering: JavaScript-chunken anmodes først, når komponenten kommer ind i visningsområdet, og monteringen sker straks efter, at chunken er ankommet.

Biblioteker som react-intersection-observer gør dette enkelt at bruge. Mønsteret nedenfor monterer kun CommentsSection, når brugeren ruller hen i nærheden af den:

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

Forudindlæsning af chunks ved svævning

Hvis du venter på et klik med at begynde at downloade en chunk, tilføjer det ventetid: Brugeren ser en indlæsningsindikator, mens netværksanmodningen gennemføres. En smartere brugeroplevelse er at forudindlæse chunken ved svævning — typisk 100–300 ms før brugeren klikker, hvilket ofte er nok tid til, at chunken når frem.

next/dynamic stiller en statisk .preload()-metode til rådighed på den returnerede komponent. Når du kalder den, udløses den dynamiske import uden at gengive noget, og browserens cache fyldes på forhånd, så komponenten næsten vises øjeblikkeligt, når den monteres.

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

Analyse af bundlestørrelse med @next/bundle-analyzer

Før du kan optimere bundlestørrelsen, skal du kunne se den. Pakken @next/bundle-analyzer omslutter Webpack Bundle Analyzer og genererer et visuelt trædiagram over hvert modul i din build.

Opsætningen består af to trin:

  • Installér: npm install --save-dev @next/bundle-analyzer
  • Omslut din Next.js-konfiguration med analysatoren

Kør ANALYZE=true next build, hvorefter to browserfaner åbnes — én for klientens bundle og én for serverens bundle. Se efter:

  • Uventet store moduler i den indledende chunk
  • Duplikerede biblioteker (f.eks. to versioner af date-fns)
  • Biblioteker, der burde importeres dynamisk, men som optræder i hovedchunken
// 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);

Dynamiske importer i serverkomponenter

Du kan også bruge dynamisk import() inde i serverkomponenter — ikke via next/dynamic, men via almindelig dynamisk ES-import. Resultatet gengives stadig på serveren; fordelen er betinget indlæsning på serveren: Du undgår at importere tunge moduler, når de ikke er nødvendige for en bestemt anmodning.

Et typisk eksempel er lokalitetsspecifikke data, renderere, der styres af feature flags, eller valgfrie plugins, som indlæses ud fra konfigurationen:

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

Måling af effekt — Core Web Vitals

Kodeopdeling og forsinket hydrering forbedrer direkte to Core Web Vitals:

  • LCP (Largest Contentful Paint) — mindre blokerende JavaScript betyder, at browseren maler det største element tidligere
  • INP (Interaction to Next Paint) — mindre arbejde på hovedtråden under indlæsningen betyder, at siden reagerer hurtigere på brugerens første tryk

Mål før og efter dine ændringer ved hjælp af:

  • next build && next start + Chrome DevTools Lighthouse (lokalt og reproducerbart)
  • NPM-pakken web-vitals + hooket useReportWebVitals fra next/navigation for at logge målinger til din analysebackend i produktion
  • Vercel Speed Insights eller Google Search Console til data fra virkelige brugere

Et almindeligt resultat efter at have skiftet en tung komponent til next/dynamic er, at den indledende JavaScript-mængde falder med 30–60 KB (gzip-komprimeret), hvilket giver 200–500 ms hurtigere TTI på en mobilforbindelse med middelhastighed.

Videnstjek: Hvornår skal du bruge ssr: false

Du integrerer et tredjepartsbibliotek til kort, som synkront tilgår window.navigator.geolocation under initialiseringen af modulet. Hvilken next/dynamic-konfiguration er korrekt, og hvorfor?

Opsummering — dynamiske importer og forsinket hydrering

I denne lektion har du lært, hvordan du kan reducere time-to-interactive i Next.js 15-applikationer med App Router ved at udskyde JavaScript, der ikke er nødvendigt fra starten.

Vigtigste pointer:

  • next/dynamic opdeler en komponent i en separat chunk, der hentes efter behov — hvilket reducerer den indledende JavaScript-mængde
  • ssr: false springer servergengivelsen over for komponenter, der er afhængige af browser-API'er, og forhindrer fejl under kørsel
  • Navngivne eksporter håndteres ved at udtrække dem i fabrikfunktionen: import(…).then(mod => mod.Named)
  • Betinget gengivelse ({open && <Modal />}) udskyder hentningen af chunken, indtil komponenten gengives første gang
  • Component.preload() ved svævning fylder cachen på forhånd, før brugeren klikker, og skjuler netværksventetiden
  • Ved at holde grænserne for 'use client' så snævre som muligt får du gratis forsinket hydrering af RSC-delene i dit træ
  • Intersection Observer muliggør visningsområdeudløst hydrering af indhold under det synlige område
  • @next/bundle-analyzer og useReportWebVitals lader dig måle den reelle effekt af disse optimeringer

Anvend først disse teknikker på de tungeste komponenter i dit bundle — editorer, diagrammer, kort og medieafspillere — for at opnå de største TTI-forbedringer.

Gratis at komme i gang

Lær TypeScript med en AI-underviser — gratis

Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.

Kurser
22
Lektioner
88

Ofte stillede spørgsmål

Er lektionen “Dynamic imports, code splitting og lazy hydration” gratis?

Ja — alle 3 lektioner i læringssporet Next.js 15 i full stack (App Router + Server Actions), inklusive “Dynamic imports, code splitting og lazy hydration”, kan læses gratis i deres fulde længde her på webstedet. Derefter låser CoddyKit PRO alle lektioner op samt interaktive øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. Next.js 15 i full stack (App Router + Server Actions)-kurset indeholder 4 lektioner i alt.

Hvad lærer jeg i “Dynamic imports, code splitting og lazy hydration”?

Udskyd ikke-kritiske komponenter med next/dynamic, og justér indlæsningen for at forkorte tiden til interaktivitet. Du øver dig i Next.js 15 i full stack (App Router + Server Actions) med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.

Skal jeg have erfaring for at begynde på Next.js 15 i full stack (App Router + Server Actions)?

Der kræves ingen tidligere erfaring. Next.js 15 i full stack (App Router + Server Actions) på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 4 af 4.

Hvor lang tid tager lektionen “Dynamic imports, code splitting og lazy hydration”?

De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.

Kan jeg skrive og køre kode i denne Next.js 15 i full stack (App Router + Server Actions)-lektion?

Ja. Alle Next.js 15 i full stack (App Router + Server Actions)-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.

Alle lektioner i dette kursus

  1. Analyse og reduktion af klientbundlen
  2. Dybdegående gennemgang af Turbopack- og compiler-konfiguration
  3. Modulgrænser med server-only og client-only
  4. Dynamic imports, code splitting og lazy hydration
← Tilbage til Next.js 15 i full stack (App Router + Server Actions)