Hvorfor JavaScript-bundles er din sides største performance-fjende
JavaScript er det dyreste indhold, din browser skal håndtere. Og det mener jeg helt bogstaveligt. Mens et 200 KB billede bare downloades og vises, skal 200 KB JavaScript downloades, parses, kompileres og eksekveres — alt sammen på hovedtråden. Din side fryser, mens browseren tygger sig igennem dine scripts.
Har du læst vores guides til LCP, INP og TTFB-optimering, ved du allerede, hvor kritisk hovedtråden er. JavaScript-bundles er den primære årsag til, at den bliver overbelastet.
Tallene taler for sig selv. Ifølge HTTP Archive (2025) loader den gennemsnitlige webside 24 JavaScript-filer med en samlet median på 613 KB komprimeret. Det svarer til over 1,5 MB ukomprimeret kode, som browseren skal parse og eksekvere. Og det er bare medianen — mange sider sender langt mere.
Et af mine yndlingseksempler er Pinterest. De reducerede deres JavaScript-bundles fra 2,5 MB til under 200 KB og skar deres Time-to-Interactive fra 23 sekunder til 5,6 sekunder. Resultatet? 44% mere omsætning og 753% flere tilmeldinger. Det er ikke en lille forbedring — det er en helt anden forretning.
Så lad os dykke ned i det. Vi gennemgår alt fra analyse af din nuværende bundle til code splitting, tree shaking og de konkrete konfigurationer, der faktisk gør forskellen i 2026.
Forstå dit udgangspunkt: Analysér din bundle
Før du begynder at optimere, skal du vide præcis, hvad der fylder i din bundle. Du kan ikke forbedre det, du ikke kan måle — det er en kliché, men den holder. Heldigvis findes der fremragende værktøjer til formålet.
Webpack Bundle Analyzer
Webpack Bundle Analyzer genererer et interaktivt treemap, der visuelt viser størrelsen af hver modul i din bundle. Store blokke = store filer. Det er ærligt talt det hurtigste overblik, du kan få.
npm install --save-dev webpack-bundle-analyzer
Tilføj det til din Webpack-konfiguration:
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
};
Kør din build, og en interaktiv visualisering åbner automatisk i browseren. Klik på blokke for at zoome ind, og brug musehjulet til at navigere. Første gang du ser det, bliver du nok overrasket over, hvor meget plads visse biblioteker fylder.
rollup-plugin-visualizer (til Vite)
Bruger du Vite (som bygger med Rollup under motorhjelmen), er rollup-plugin-visualizer dit foretrukne analyseværktøj:
npm install --save-dev rollup-plugin-visualizer
// vite.config.ts
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
visualizer({
open: true,
gzipSize: true,
brotliSize: true
})
]
});
Source Map Explorer
Source Map Explorer er bundler-agnostisk — det virker med enhver bundler, så længe du har et source map. Det viser præcis, hvilke bytes der stammer fra hvilke filer:
npx source-map-explorer dist/main.js dist/main.js.map
Import Cost i VS Code
Import Cost-udvidelsen til VS Code viser inline størrelsen af hvert import-statement direkte i editoren, mens du koder. Jeg har selv fanget adskillige dyre imports med denne udvidelse, før de nåede at ende i en produktionsbuild. Klart værd at installere.
Chrome DevTools Coverage
Åbn Chrome DevTools, gå til panelet Coverage (Ctrl+Shift+P → "Show Coverage"), og genindlæs siden. Du får en oversigt over, hvor meget af din JavaScript (og CSS) der faktisk bruges. Rød markering = ubrugt kode.
Det er et fremragende udgangspunkt for at identificere, hvad der kan lazy-loades eller fjernes helt. Du bliver næsten altid overrasket over, hvor meget ubrugt kode der sendes til brugeren.
Tree shaking: Fjern ubrugt kode automatisk
Tree shaking er en teknik, hvor din bundler automatisk fjerner kode, der ikke bruges i din applikation. Forestil dig et træ: den kode, du faktisk bruger, er de grønne blade. Død kode er de brune blade, der falder af, når du ryster træet. Deraf navnet.
Sådan virker det
Tree shaking fungerer ved statisk analyse af ES Module-imports og -exports. Bundleren kan se, hvilke funktioner og variabler der importeres, og hvilke der aldrig bruges — og fjerner sidstnævnte fra den endelige bundle.
Det kræver dog, at du bruger ES Module-syntaks (import/export), ikke CommonJS (require/module.exports). Det er en afgørende forskel:
// ✅ ES Modules — tree-shakeable
import { debounce } from 'lodash-es';
// ❌ CommonJS — IKKE tree-shakeable
const _ = require('lodash');
const debounce = _.debounce;
sideEffects i package.json
For at tree shaking skal fungere optimalt, skal bundleren vide, hvilke filer der har "side effects" — altså filer, der ændrer global tilstand ved import. Markér dine filer som side-effect-frie i package.json:
{
"name": "min-app",
"sideEffects": false
}
Har du filer med side effects (f.eks. CSS-imports eller polyfills), kan du angive dem eksplicit:
{
"sideEffects": [
"*.css",
"./src/polyfills.js"
]
}
Det her er en detalje, mange springer over — og det koster dem ofte en del unødvendig bundle-størrelse.
Typiske tree-shaking-fælder
- Import af hele biblioteker:
import _ from 'lodash'trækker hele biblioteket ind (ca. 70 KB minificeret). Bruglodash-eseller importér specifikke funktioner:import debounce from 'lodash/debounce'. - Klasser kan ikke tree-shakes: I modsætning til funktioner kan individuelle metoder i klasser ikke fjernes af bundleren, selvom de ikke bruges. Det er en begrænsning, man bør kende til.
- Re-exports via barrel files: Filer som
index.ts, der re-eksporterer alt fra en mappe, kan forhindre effektiv tree shaking. Overvej direkte imports i stedet. - Ikke-ESM-biblioteker: Mange ældre biblioteker bruger stadig CommonJS. Tjek med
is-esmnpm-pakken, om et bibliotek er ESM-kompatibelt.
Code splitting: Load kun det, der er nødvendigt
Code splitting deler din JavaScript-bundle i mindre chunks, der loades on-demand. I stedet for at sende hele applikationen til browseren på én gang, loader du kun den kode, der er nødvendig for den aktuelle side eller funktion.
Resten hentes, når brugeren navigerer videre. Simpelt koncept, stor effekt.
Dynamiske imports
Det mest grundlæggende værktøj til code splitting er dynamiske imports med import()-syntaksen. Til forskel fra statiske imports, der resolver ved build-tid, loader dynamiske imports moduler asynkront ved runtime:
// Statisk import — inkluderet i main bundle
import { HeavyChart } from './HeavyChart';
// Dynamisk import — separat chunk, loadet on-demand
const { HeavyChart } = await import('./HeavyChart');
Din bundler (Webpack, Vite/Rollup) opretter automatisk en separat chunk-fil for det dynamisk importerede modul. Ingen ekstra konfiguration nødvendig.
Route-baseret code splitting
Den mest effektive code-splitting-strategi er at splitte på rute-niveau. Hver side i din applikation bliver sin egen chunk:
// React med React.lazy og Suspense
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
);
}
I Next.js er route-baseret code splitting automatisk — hver side i pages/- eller app/-mappen bliver sin egen chunk uden yderligere konfiguration. Det er en af de ting, Next.js bare gør rigtig godt.
Komponent-baseret code splitting
Udover ruter kan du splitte tunge komponenter, der ikke er nødvendige ved initial load. Tænk modale vinduer, grafer, rig-tekst-editorer eller admin-paneler:
// Lazy-load en tung editor-komponent
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
function ArticlePage() {
const [isEditing, setIsEditing] = useState(false);
return (
<div>
<article>...</article>
{isEditing && (
<Suspense fallback={<p>Indlæser editor...</p>}>
<RichTextEditor />
</Suspense>
)}
</div>
);
}
Vendor splitting og caching-strategi
Tredjepartsbiblioteker (React, lodash, date-fns osv.) ændrer sig sjældent sammenlignet med din applikationskode. Ved at adskille dem i en separat vendor chunk kan browseren cache dem separat.
Det betyder, at når du deployer en opdatering af din applikationskode, behøver brugerne ikke downloade React igen. Det lyder som en lille ting, men det gør en mærkbar forskel for returning visitors.
Vite/Rollup: manualChunks
I Vite konfigurerer du vendor splitting via manualChunks i Rollup-konfigurationen:
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Kernbiblioteker i én chunk
vendor: ['react', 'react-dom', 'react-router-dom'],
// UI-bibliotek i en separat chunk
ui: ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
// Utility-biblioteker
utils: ['date-fns', 'zod']
}
}
}
}
});
For mere granulær kontrol kan du bruge funktionsformen, der automatisk splitter hver node_modules-pakke i sin egen chunk:
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
// Udpak pakkenavnet
const parts = id.split('node_modules/');
const packageName = parts[1].split('/')[0];
return `vendor-${packageName}`;
}
}
}
}
}
});
En advarsel her: pas på med at splitte for granulært. Har du 50 separate vendor-chunks, kan det faktisk skade performance på grund af overhead fra for mange HTTP-requests (selvom HTTP/2 håndterer det bedre end HTTP/1.1).
Webpack: splitChunks
I Webpack bruger du optimization.splitChunks-konfigurationen:
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
};
Prefetching og preloading: Gør code splitting usynligt
En rimelig bekymring ved code splitting er, at brugere oplever forsinkelse, når de navigerer til en ny side, fordi chunken først skal downloades. Den gode nyhed? Det kan du løse med prefetching og preloading.
Prefetch: Download i baggrunden
Med prefetch fortæller du browseren: "Download denne ressource i baggrunden, når du har tid — vi får nok brug for den snart." Browseren henter den med lav prioritet, uden at påvirke den aktuelle side:
// Webpack magic comment for prefetch
const Settings = lazy(() =>
import(/* webpackPrefetch: true */ './pages/Settings')
);
Webpack genererer automatisk et <link rel="prefetch">-tag i dit HTML. Når brugeren klikker på "Indstillinger", er chunken allerede i cache. Det føles øjeblikkeligt.
Preload: Download med høj prioritet
Preload er mere aggressiv — den downloader ressourcen med det samme med høj prioritet. Brug det til chunks, du er sikker på, brugeren snart får brug for:
// Webpack magic comment for preload
const Dashboard = lazy(() =>
import(/* webpackPreload: true */ './pages/Dashboard')
);
Router-baseret prefetching
Mange moderne frameworks har indbygget prefetching. Next.js prefetcher automatisk alle synlige <Link>-komponenter, hvilket er genialt. I en custom React-app kan du implementere det selv ved at prefetche on hover:
function PrefetchLink({ to, children, importFn }) {
const handleMouseEnter = () => {
// Prefetch chunken, når musen hover over linket
importFn();
};
return (
<Link to={to} onMouseEnter={handleMouseEnter}>
{children}
</Link>
);
}
// Brug:
const settingsImport = () => import('./pages/Settings');
<PrefetchLink to="/settings" importFn={settingsImport}>
Indstillinger
</PrefetchLink>
Den her tilgang fungerer overraskende godt i praksis. De fleste brugere hover over et link i et par hundrede millisekunder, før de klikker — og det er typisk nok til at hente en chunk.
Erstat tunge afhængigheder
En af de hurtigste (og mest tilfredsstillende) måder at reducere din bundle på er at erstatte tunge biblioteker med lettere alternativer. Her er de mest almindelige syndere:
| Tungt bibliotek | Størrelse (min+gzip) | Lettere alternativ | Størrelse (min+gzip) |
|---|---|---|---|
| moment | ~72 KB | date-fns / dayjs | ~7 KB / ~3 KB |
| lodash | ~70 KB | lodash-es (tree-shakeable) | ~2-5 KB (kun brugte funktioner) |
| axios | ~13 KB | Natives fetch API | 0 KB |
| uuid | ~3 KB | crypto.randomUUID() | 0 KB |
| classnames | ~1 KB | clsx | ~0,3 KB |
Spørg dig selv ved hvert bibliotek: "Har vi virkelig brug for dette, eller kan vi bruge en native browser-API i stedet?" I 2026 understøtter alle moderne browsere fetch(), crypto.randomUUID(), structuredClone(), Array.at(), Object.groupBy() og meget mere.
Mange polyfills og utility-biblioteker er simpelthen ikke nødvendige længere. Jeg har set projekter, der stadig bundler moment.js til datoformatering, selvom Intl.DateTimeFormat har haft fuld browsersupport i årevis.
Minificering og komprimering
Minificering og komprimering er det sidste lag i din optimeringsstrategi — og det er ofte den nemmeste gevinst at hente. Lav indsats, stor effekt.
Minificering
Minificering fjerner whitespace, kommentarer og forkorter variabelnavne. Moderne bundlere gør det automatisk i production mode:
- Vite/Rollup: Bruger esbuild til minificering som standard — det er skrevet i Go og er ekstremt hurtigt.
- Webpack: Bruger Terser som standard. Du kan skifte til esbuild eller SWC for hurtigere builds.
Brotli- og Gzip-komprimering
Komprimering reducerer den overførte størrelse yderligere. Brotli leverer typisk 15–20% bedre komprimering end Gzip for JavaScript-filer. De fleste CDN'er og webservere understøtter begge dele.
Her er et eksempel med Nginx:
# Nginx: Aktivér Brotli og Gzip
brotli on;
brotli_types application/javascript text/css;
brotli_comp_level 6;
gzip on;
gzip_types application/javascript text/css;
gzip_min_length 256;
Med Vite kan du generere prækomprimerede filer som en del af build-processen, så din server slipper for at komprimere on-the-fly:
npm install --save-dev vite-plugin-compression
// vite.config.ts
import compression from 'vite-plugin-compression';
export default defineConfig({
plugins: [
compression({ algorithm: 'brotliCompress' }),
compression({ algorithm: 'gzip' })
]
});
Performance-budget: Hold din bundle i kontrol
Her er sandheden: Optimering er ikke en engangsting. Uden et performance-budget vokser din bundle stille og roligt, hver gang nogen tilføjer et nyt bibliotek. En kollega installerer et fancy animationsbibliotek, en anden tilføjer en date-picker — og pludselig er du tilbage ved udgangspunktet.
Sæt klare grænser og håndhæv dem automatisk.
Det anbefalede budget
En bredt anerkendt tommelfingerregel er under 200 KB gzipped JavaScript for den initielle load. Det svarer til ca. 1 MB ukomprimeret kode. Det er ikke et tilfældigt tal — det er baseret på, hvad en gennemsnitlig mobilenhed kan parse og eksekvere inden for et rimeligt tidsvindue.
Webpack performance hints
// webpack.config.js
module.exports = {
performance: {
maxAssetSize: 250000, // 250 KB per fil
maxEntrypointSize: 250000, // 250 KB total for entry point
hints: 'error' // Fejl i build ved overskridelse
}
};
bundlesize i CI/CD
Integrér bundle-størrelse-tjek i din CI/CD-pipeline, så pull requests, der overskrider budgettet, automatisk fejler. Det er den bedste måde at forhindre "bundle creep" på:
// package.json
{
"bundlesize": [
{
"path": "./dist/assets/*.js",
"maxSize": "200 kB",
"compression": "gzip"
}
],
"scripts": {
"check-size": "bundlesize"
}
}
Fjern unødvendige polyfills
I 2026 understøtter alle moderne browsere ES2020+ features. Alligevel sender mange projekter stadig polyfills for funktioner, der har haft universel browsersupport i årevis. Det er spild af bytes.
Tjek din browserslist-konfiguration og sørg for, at den matcher dit reelle publikum:
// .browserslistrc — moderne konfiguration
last 2 versions
not dead
not op_mini all
> 0.5%
Har du brugt core-js med useBuiltIns: 'usage' i Babel, så vær opmærksom på, at det kan tilføje polyfills, du ikke har brug for. Overvej at skifte til mere målrettede løsninger — eller fjern dem helt, hvis din browserslist kun inkluderer moderne browsere. Du bliver måske overrasket over, hvor meget det kan spare.
Opsummering: Din bundle-optimerings-tjekliste
- Analysér din nuværende bundle med Webpack Bundle Analyzer, rollup-plugin-visualizer eller Source Map Explorer
- Aktivér tree shaking ved at bruge ES Modules og markere
sideEffectsi package.json - Implementér code splitting på rute- og komponentniveau med dynamiske imports
- Separér vendor-kode i egne chunks for bedre caching
- Prefetch chunks, brugeren sandsynligvis navigerer til
- Erstat tunge biblioteker med lettere alternativer eller native API'er
- Aktivér Brotli-komprimering på din server eller CDN
- Sæt et performance-budget og håndhæv det i CI/CD
- Fjern unødvendige polyfills ved at opdatere din browserslist
- Monitorér løbende med Chrome DevTools Coverage og feltdata fra web-vitals
Husk: Hver kilobyte tæller — især på mobilnetværk. Den gennemsnitlige webside sender over 600 KB JavaScript, men med de teknikker, vi har gennemgået her, kan du komme langt under det. Dine brugere (og din Lighthouse-score) vil takke dig.
Ofte stillede spørgsmål
Hvad er forskellen på code splitting og tree shaking?
Tree shaking fjerner ubrugt kode fra din bundle — kode der er importeret men aldrig refereret. Code splitting deler din brugte kode i mindre chunks, der loades on-demand. De to teknikker komplementerer hinanden: tree shaking reducerer den samlede mængde kode, og code splitting fordeler den over tid, så brugeren kun downloader det nødvendige.
Hvor stor bør min JavaScript-bundle være?
Den bredt anerkendte tommelfingerregel er under 200 KB gzipped for den initielle load. Det svarer til ca. 1 MB ukomprimeret kode. Husk, at det er den samlede størrelse af alle JavaScript-filer, der loades ved den første sidevisning — ikke den totale størrelse af hele applikationen.
Virker tree shaking med alle npm-pakker?
Desværre nej. Tree shaking kræver, at pakken bruger ES Module-syntaks (import/export). Mange ældre pakker bruger stadig CommonJS (require/module.exports), som ikke kan tree-shakes effektivt. Tjek om en pakke er ESM-kompatibel med is-esm npm-pakken, eller kig efter "type": "module" i pakkens package.json.
Hvilken bundler er bedst til bundle-optimering i 2026?
Vite (med Rollup) er det foretrukne valg for nye projekter i 2026. Den tilbyder hurtigere builds, mere aggressiv standard code splitting og bedre tree shaking end Webpack. Webpack er stadig relevant for store enterprise-projekter med komplekse konfigurationer, men for de fleste teams er Vite det rigtige valg.
Påvirker code splitting SEO negativt?
Nej — tværtimod. Code splitting forbedrer typisk SEO, fordi det reducerer den initielle JavaScript-load, hvilket forbedrer Core Web Vitals som LCP og INP. Google bruger Chromium til rendering og kan sagtens håndtere dynamisk loadet indhold. Sørg dog for, at kritisk indhold ikke udelukkende er afhængigt af JavaScript — server-side rendering (SSR) er stadig den bedste praksis for SEO-kritisk indhold.