Tilbage til bloggen

Teknisk

Optimering af ydeevne i Next.js 15: Mønstre der flytter Core Web Vitals

· · 12 min. læsetid

Hvad du skal tjekke i et Next.js 15-site i produktion, og hvorfor det betyder noget, med illustrative før- og efter-tal. Dækker Server Components, App Router-mønstre, billedoptimering, caching-strategier og reduktion af bundle-størrelsen.

Core Web Vitals: Før og efter optimering

Måling Før Efter Ændring
LCP 3,8 s 1,2 s 68 % bedre
FID 180 ms 12 ms 93 % bedre
CLS 0,18 0,02 89 % bedre
INP 320 ms 85 ms 73 % bedre
TTFB 1.200 ms 180 ms 85 % bedre

Derfor betyder ydeevne mere end nogensinde

Google har haft sidehastighed som rankingfaktor siden 2021, og brugernes forventninger er kun steget. Analyser fra Google og Deloitte viser, at en forbedring af mobilhastigheden på 0,1 sekund øger konverteringsraten med 8,4 % for detailhandel og 10,1 % for rejsesites. For virksomheder er ydeevne ikke en teknisk detalje - den påvirker omsætningen direkte.

Next.js 15 introducerede flere performance-features, som giver markante forbedringer, når de bruges rigtigt. Men frameworket alene gør ikke et site hurtigt. Måden, du bruger det på, betyder enormt meget. Et Next.js-site kan score dårligt på Core Web Vitals, selvom det kører den nyeste version, fordi standardmønstrene leder udviklere mod klienttunge arkitekturer.

Hvad artiklen dækker

Mønstrene i denne artikel er dem, vi bruger på vores egne sites, herunder twincurrent.dk og DocubotAI.app. Før- og efter-tallene illustrerer, hvad mønstrene typisk giver, og er ikke målinger fra ét bestemt site; mål jeres egne ruter før og efter. Alle kodeeksempler er mønstre, vi bruger i produktion.

React Server Components: Den største gevinst

React Server Components (RSC) er den enkeltstående mest effektfulde performance-feature i Next.js 15. De lader komponenter rendere på serveren og sender kun HTML til klienten - ingen JavaScript-bundle for den komponent. På indholdstunge sider kan det reducere mængden af JavaScript på klientsiden med 60-80 %.

Grundprincippet: Server som standard

I App Router er alle komponenter Server Components som standard. Du tilføjer kun 'use client' -direktivet, når en komponent reelt har brug for interaktivitet på klientsiden: event handlers, useState, useEffect eller browser-API’er. Fejlen, vi ser i de fleste kodebaser, er, at 'use client' sættes på hele sidekomponenter, fordi én lille del kræver interaktivitet.

Mønster: Isolér interaktivitet på klienten

I stedet for at gøre hele siden til en client component kan du trække de interaktive dele ud i små client components og beholde resten som serverrenderet indhold.

// BAD: Entire page is a client component
// because of one interactive element
'use client'
export default function BlogPage() {
  const [filter, setFilter] = useState('all')
  return (
    <div>
      <h1>Blog</h1>           {/* Static - doesn't need client */}
      <p>Long intro text...</p> {/* Static - doesn't need client */}
      <FilterBar onFilter={setFilter} /> {/* Interactive */}
      <ArticleList filter={filter} />    {/* Could be server */}
    </div>
  )
}

// GOOD: Only the interactive part is a client component
// page.tsx (Server Component - no directive needed)
export default function BlogPage() {
  return (
    <div>
      <h1>Blog</h1>
      <p>Long intro text...</p>
      <BlogFilter />  {/* Client component island */}
    </div>
  )
}

// BlogFilter.tsx
'use client'
export function BlogFilter() {
  const [filter, setFilter] = useState('all')
  return <FilterBar onFilter={setFilter} />
}

Hvad mønsteret giver jer

Besparelsen kommer af, hvor stor en del af træet der bliver på serveren. En side, der starter med 'use client' , sender alle komponenter under sig til browseren - uanset hvor statiske de er. Flytter I grænsen ned til de reelt interaktive dele - et filter, en karrusel, en formular - forbliver overskrifter, brødtekst og metadata serverrenderet HTML. Mål jeres egne ruter før og efter med build-outputtet frem for at stole på et tal fra en andens kodebase, inklusive vores.

Server Components og datahentning

Server Components kan hente data direkte uden klientbiblioteker som TanStack Query eller SWR. Det fjerner det klassiske vandfaldsmønster med loading spinners, hvor siden først indlæses, derefter JavaScript, hvorefter dataforespørgslen affyres, og data til sidst renderes. Med Server Components er data hentet og renderet, før HTML’en når browseren.

Mønster: Datahentning i async Server Components

// Server Component - data is fetched at render time
// No loading spinner, no client-side fetch library needed
export default async function ProjectPage({
  params
}: {
  params: { id: string }
}) {
  const project = await prisma.project.findUnique({
    where: { id: params.id },
    include: { tasks: true, client: true }
  })

  if (!project) notFound()

  return (
    <div>
      <h1>{project.name}</h1>
      <ClientInfo client={project.client} />
      <TaskBoard tasks={project.tasks} /> {/* Client component */}
    </div>
  )
}

App Router-mønstre, der faktisk hjælper

App Router i Next.js 15 byder på flere performance-features ud over Server Components. Bruges de rigtigt, gør de en målbar forskel; bruges de forkert, kan de faktisk skade ydeevnen.

Konfiguration af route segments

Brug export const dynamic = 'force-static' til sider, der ikke ændrer sig mellem requests. Blogindlæg, dokumentationssider og marketingsider bør genereres statisk. Det forvandler en dynamisk serverrenderet side til en cachet statisk side, der serveres fra edge.

Effekt: TTFB fra 200-800 ms til 20-50 ms for statiske sider

Parallel routes og loading states

Brug loading.tsx -filer til at vise øjeblikkelige loading states, mens data hentes i baggrunden. Kombinér med Suspense -grænser for at streame sidens sektioner uafhængigt af hinanden. Header og navigation renderes med det samme; datatunge sektioner streames ind, efterhånden som de bliver klar.

Effekt: Oplevet indlæsningstid reduceret med 40-60 %

Metadata API til SEO uden klient-JS

Brug generateMetadata -funktionen eller export const metadata i layout.tsx-filer i stedet for klientbiblioteker til head-håndtering som next-seo. Det genererer meta-tags på serveren helt uden JavaScript-overhead på klienten.

Effekt: Fjerner 15-25 KB klientkode fra SEO-biblioteker

Route groups til layoutoptimering

Brug route groups (group) til at dele layouts uden nesting. Det forhindrer det klassiske problem, hvor et tungt dashboard-layout indlæses på simple marketingsider, der deler route-præfiks. Hver route group får kun de layouts, den faktisk har brug for.

Effekt: Undgår unødvendige layout-rerenders og indlæsning af delte bundles

Billedoptimering: AVIF, WebP og responsiv indlæsning

Billeder er typisk de største assets på enhver webside, og uoptimerede billeder er den hyppigste årsag til dårlige LCP-scores. Next.js leverer Image -komponenten, som håndterer formatkonvertering, skalering og lazy loading automatisk - men du skal bruge den rigtigt.

Mønster: Optimal billedkonfiguration

// next.config.js
module.exports = {
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200],
    imageSizes: [16, 32, 48, 64, 96, 128, 256],
    minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
  },
}

// Usage in components
import Image from 'next/image'

// For above-the-fold hero images: disable lazy loading
<Image
  src="/hero.jpg"
  alt="Project showcase"
  width={1200}
  height={630}
  priority          // Preloads this image
  quality={85}      // Balance quality and size
  sizes="(max-width: 768px) 100vw, 1200px"
/>

// For below-the-fold images: lazy load (default)
<Image
  src="/screenshot.jpg"
  alt="Application screenshot"
  width={800}
  height={450}
  quality={80}
  sizes="(max-width: 768px) 100vw, 800px"
  placeholder="blur" // Show blur while loading
  blurDataURL="..."  // Base64 blur placeholder
/>

Formatsammenligning

Format Størrelse (typisk) Kvalitet Understøttelse
JPEG (original) 100 KB Udgangspunkt Alle browsere
WebP 65 KB Samme kvalitet 97 % af browsere
AVIF 40 KB Samme eller bedre 92 % af browsere

Ved at konfigurere Next.js til at servere AVIF med WebP som fallback bliver billederne typisk meget mindre end JPEG eller PNG uden synligt kvalitetstab. Det betyder mest på billedtunge sider som gallerier og caseoversigter.

Kritisk: priority-attributten

Et udbredt LCP-problem: Hero-billedet har ikke priority sat. Som standard lazy-loader Next.js alle billeder. Dit hero-billede over folden bør altid have priority , så preloading udløses. Når hero-billedet er LCP-elementet, kan alene dette forbedre LCP mærkbart.

Caching-strategier til produktion

Next.js 15 har et caching-system i flere lag. At forstå hvert lag - og vide, hvornår man skal fravælge det - er afgørende for både ydeevne og korrekthed.

Lag 1: Request memoization

Inden for ét enkelt request dedupliceres identiske fetch() -kald til samme URL automatisk. Det betyder, at hvis både dit layout og din sidekomponent henter den aktuelle bruger, affyres requestet kun én gang.

Bedst til: Delte data på tværs af komponenter i samme render

Lag 2: Data cache

Fetch-svar caches på tværs af requests. Brug revalidate til at styre, hvor længe data caches. For en blog er en revalidering hver time fint. For et realtidsdashboard vil du måske slå caching helt fra.

// Revalidate every hour
fetch('https://api.example.com/posts', {
  next: { revalidate: 3600 }
})

// Never cache (real-time data)
fetch('https://api.example.com/live-stats', {
  cache: 'no-store'
})

Bedst til: API-data, der ændrer sig periodisk

Lag 3: Full route cache

Statiske routes renderes ved build og caches som HTML + RSC-payload. Dynamiske routes kan caches med ISR (Incremental Static Regeneration) via revalidate -eksporten. Det er den største performancegevinst for indholdssites - serveren laver nul arbejde for cachede routes.

// Static generation at build time
export const dynamic = 'force-static'

// ISR: regenerate every 60 seconds
export const revalidate = 60

// On-demand revalidation (after CMS update)
import { revalidatePath } from 'next/cache'
revalidatePath('/blog')

Bedst til: Indholdssider, blogindlæg, marketingsider

Typisk caching-fejl

Bruger du cookies() eller headers() i en Server Component, fravælger hele routen automatisk statisk generering. Har du kun brug for auth-data i én komponent, så isolér auth-tjekket, så hele siden ikke bliver dynamisk. Brug Suspense omkring den autentificerede komponent, og hold resten af siden statisk.

Bundle-størrelse: Find og fjern unødvendig kode

Størrelsen på JavaScript-bundlen påvirker Time to Interactive og First Input Delay direkte. Hver eneste kilobyte JavaScript skal downloades, parses og eksekveres, før siden bliver interaktiv. Sådan reducerer vi bundle-størrelsen systematisk.

Trin 1: Analysér bundlen

# Install the analyzer
npm install @next/bundle-analyzer

# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer(nextConfig)

# Run the analysis
ANALYZE=true npm run build

Trin 2: Trinvis optimering

Sådan lægger trinene sig typisk oven i hinanden. Tallene er illustrative og viser størrelsesordenen, ikke målinger fra et bestemt projekt:

Optimeringstrin JS-bundle First load
Alle client components (udgangspunkt) 340 KB 2,8 s
Server Components til statisk indhold 185 KB 1,6 s
+ Dynamic imports af tunge komponenter 120 KB 1,1 s
+ Tree shaking af ikonbiblioteker 95 KB 0,9 s
+ Fjern ubrugte dependencies 78 KB 0,7 s

Typiske kilder til unødvendig kode

  • Ikonbiblioteker: Importerer du hele Lucide React- eller Heroicons-pakken i stedet for enkelte ikoner, koster det 50-100 KB. Brug altid navngivne imports: import { ArrowLeft } from 'lucide-react'
  • Datobiblioteker: Moment.js fylder 300 KB. date-fns med tree shaking fylder 5-15 KB for samme funktionalitet. Eller brug den indbyggede Intl.DateTimeFormat til formatering
  • Animationsbiblioteker: Framer Motion fylder 30-50 KB. Til simple overgange koster CSS-animationer 0 KB. Gem Framer Motion til komplekse, koreograferede animationer
  • Ubrugte dependencies: Kør npx depcheck for at finde pakker i node_modules, som aldrig importeres

Mønster: Dynamic imports af tunge komponenter

import dynamic from 'next/dynamic'

// Chart library only loads when the component is visible
const AnalyticsChart = dynamic(
  () => import('@/components/AnalyticsChart'),
  {
    loading: () => <div className="h-64 animate-pulse bg-white/5 rounded" />,
    ssr: false // Charts don't need server-side rendering
  }
)

// Rich text editor only loads when user starts editing
const RichEditor = dynamic(
  () => import('@/components/RichEditor'),
  { ssr: false }
)

Illustrative før- og efter-tal

Tallene illustrerer, hvor store forbedringer mønstrene ovenfor typisk giver på en klient-tung Next.js-side, sat op mod Googles mål. De er ikke målinger fra et bestemt TwinCurrent-site. Mål jeres egne ruter med PageSpeed Insights eller feltdata før og efter hver ændring.

LCP - Largest Contentful Paint

Mål: < 2,5 s

68 % forbedring

Før optimering

3,8 s

Efter optimering

1,2 s

FID - First Input Delay

Mål: < 100 ms

93 % forbedring

Før optimering

180 ms

Efter optimering

12 ms

CLS - Cumulative Layout Shift

Mål: < 0,1

89 % forbedring

Før optimering

0,18

Efter optimering

0,02

INP - Interaction to Next Paint

Mål: < 200 ms

73 % forbedring

Før optimering

320 ms

Efter optimering

85 ms

TTFB - Time to First Byte

Mål: < 800 ms

85 % forbedring

Før optimering

1.200 ms

Efter optimering

180 ms

Overvågning af ydeevne i produktion

Vi bruger vores egen self-hostede analytics-tracker (tw.js) til at overvåge Core Web Vitals på tværs af alle TwinCurrent-sites i realtid. Det giver løbende performancedata fra rigtige brugere - ikke kun syntetiske Lighthouse-tests. Trackeren registrerer LCP, FID, CLS, INP og TTFB for hver eneste sidevisning og giver os et klart billede af den faktiske brugeroplevelse på tværs af enheder og netværksforhold.

Konklusion: Ydeevne er en feature

Ydeevne på nettet er ikke et flueben, man sætter, når sitet er bygget. Det er en feature, der påvirker brugeroplevelsen, søgeplaceringerne og konverteringsraterne. Next.js 15 leverer værktøjerne til at bygge reelt hurtige applikationer - men kun hvis du forstår at bruge dem.

Optimeringstjeklisten

  • Brug Server Components som standard, og isolér interaktivitet på klienten i små client islands
  • Giv billeder over folden priority , og lad alt andet lazy-loade
  • Konfigurér AVIF + WebP som billedformater i next.config.js
  • Brug statisk generering til indholdssider og ISR til data, der er dynamiske, men kan caches
  • Analysér bundlen med @next/bundle-analyzer, og fjern ubrugte dependencies
  • Brug dynamic imports til tunge komponenter (grafer, editorer, kort), der ikke er nødvendige ved første render
  • Overvåg Core Web Vitals løbende med data fra rigtige brugere - ikke kun Lighthouse

Mønstrene i denne artikel er dem, vi selv bruger på vores sites i produktion. Vigtigere end nogen Lighthouse-score forbedrer de den reelle brugeroplevelse: Siderne indlæses hurtigere, interaktioner føles mere responsive, og layoutet forskubber sig ikke uventet. Dine brugere ved måske ikke, hvad Core Web Vitals er - men de kan tydeligt mærke forskellen.

Har jeres site brug for hurtigere ydeevne?

Vi bygger hurtige sites og kigger gerne på jeres. Fortæl os, hvad der er langsomt, så får I et fast tilbud på rettelsen.

Få et prisoverslag inden 24 timer Se vores projekter

Relaterede artikler

Har I et projekt i tankerne?

Fortæl os, hvad I vil bygge. Vi svarer inden for 24 timer med en ærlig vurdering - uden salgstale.

Kontakt os