Optimering af ydeevne i Next.js 15: Benchmarks fra virkelige projekter
Omfattende benchmarks og optimeringsstrategier for Next.js 15 i produktion - med før- og efter-målinger fra rigtige applikationer. 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
Indholdsfortegnelse
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. Mange af de Next.js-applikationer, vi gennemgår, scorer dårligt på Core Web Vitals, selvom de kører den nyeste version - fordi standardmønstrene leder udviklere mod klienttunge arkitekturer.
Hvad artiklen dækker
Hver eneste optimering i denne artikel stammer fra applikationer i produktion, som vi har bygget og vedligeholder hos TwinCurrent - herunder porteføljesitet twincurrent.dk, DocubotAI.app og kundeprojekter. Benchmarkene er reelle målinger, ikke syntetiske tests. 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} />
}Målt effekt
På porteføljesitet twincurrent.dk reducerede vi JavaScript-bundlen med 62 % ved at refaktorere fra client components på sideniveau til isolerede client islands. Blogoversigten bruger kun 'use client' til kategorifilteret og Framer Motion-animationerne. Alt artikelindhold, al metadata og alle statiske elementer renderes på serveren.
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.
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.
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.
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.
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
Ved at konfigurere Next.js til at servere AVIF med WebP som fallback får du cirka 60 % mindre billeder uden synligt kvalitetstab. På billedtunge sider som vores projektportefølje reducerede det sidens samlede vægt fra 2,4 MB til 0,9 MB.
Kritisk: priority-attributten
Det mest udbredte LCP-problem, vi ser i Next.js-gennemgange: 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. Alene dette forbedrer typisk LCP med 500 ms-1,5 s.
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.
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'
})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')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 buildTrin 2: Trinvis optimering
Her er den faktiske rækkefølge, vi fulgte, da vi optimerede DocubotAI.app-bundlen - med målte resultater for hvert trin:
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 depcheckfor 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 }
)Virkelige benchmarks fra sites i produktion
Her er Core Web Vitals-målingerne fra TwinCurrents sites i produktion, målt med Google PageSpeed Insights (som bruger Lighthouse) og CrUX-data, hvor de er tilgængelige.
LCP - Largest Contentful Paint
FID - First Input Delay
CLS - Cumulative Layout Shift
INP - Interaction to Next Paint
TTFB - Time to First Byte
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 løftede vores sites i produktion fra middelmådige Lighthouse-scores på 60-70 til stabile scores på 95+. Endnu vigtigere forbedrede 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 gennemgår og optimerer Next.js-applikationer med fokus på reel ydeevne. De fleste optimeringer er leveret inden for en uge.