JavaScript bundel verkleinen — van 1,5MB naar 300KB
Terug naar blog

JavaScript bundel verkleinen — van 1,5MB naar 300KB

AuthorRuthger Idema
8 mei 202610 min leestijd

Een JavaScript-bundel van 1,5MB is geen technisch detail. Het is een conversiekiller. Met tree shaking, code splitting en lazy imports halveer je de bundel zonder functionaliteit te verliezen. Concrete stappen, concrete cijfers.

JavaScript bundel verkleinen — van 1,5MB naar 300KB

1,5MB JavaScript. Dat is de gemiddelde bundel van een Magento Luma-webshop op desktop.

Op een trage mobiele verbinding (4G, 10 Mbps) duurt het downloaden van die bundel alleen al 1,2 seconden. Daarna volgt het parsen en uitvoeren: nog eens 2-4 seconden op een middenklasse Android-telefoon. Je LCP scoort slecht, je INP is ellendig, en je conversieratio op mobiel is 40% lager dan op desktop.

De goede nieuws: dit is oplosbaar. Een bundel van 1,5MB terugbrengen naar 300KB is realistisch met de juiste technieken. Geen magie, geen grote refactor. Tree shaking, code splitting en lazy imports — consequent toegepast.

Wat je leert in dit artikel

  • Hoe je de huidige bundel analyseert met de juiste tools
  • Wat tree shaking is en waarom het bij de meeste projecten niet automatisch werkt
  • Code splitting: routes, componenten en vendor-splits uitgelegd
  • Lazy imports voor zware dependencies
  • Concrete stappenplan van 1,5MB naar 300KB

Stap 1: meten wat je hebt

Je kunt niets optimaliseren wat je niet hebt gemeten. Start met een bundle analyse.

webpack-bundle-analyzer

Als je webpack gebruikt (standaard in veel Magento-setups en Next.js):

bash
# Installeer de analyzer
npm install --save-dev webpack-bundle-analyzer

# Voer een productie-build uit met stats
npx webpack --profile --json > stats.json

# Visualiseer
npx webpack-bundle-analyzer stats.json

Dit geeft een interactieve treemap. Je ziet direct welke modules het grootste deel van de bundel innemen.

Vite rollup-plugin-visualizer

Bij een Vite-project (Hyvä, moderne setups):

bash
npm install --save-dev rollup-plugin-visualizer
javascript
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';

export default {
  plugins: [
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
    })
  ]
};

Wat je zoekt in de analyse

De klassieke boosdoeners in e-commerce JavaScript-bundels:

ModuleTypische grootte (gzip)Probleem
moment.js67KBVolledig geladen, 99% nooit gebruikt
lodash (volledige import)72KBAlleen _.get nodig, alles geladen
date-fns (volledige import)79KBSlechts 3 functies gebruikt
chart.js58KBAlleen op admin-pagina's nodig
swiper.js45KBAlleen op homepage geladen, overal aanwezig
google-maps embed130KB+Op iedere pagina geladen, nodig op 1

Meer over de impact op Largest Contentful Paint lees je in ons artikel over LCP optimaliseren.

Stap 2: tree shaking

Tree shaking verwijdert code die je niet gebruikt. Bundlers als webpack en Rollup/Vite doen dit automatisch — maar alleen als aan de juiste voorwaarden wordt voldaan.

Waarom tree shaking vaak niet werkt

Tree shaking werkt uitsluitend op ES modules (ESM) met statische imports. CommonJS-modules (require()) worden niet geanalyseerd.

Meest voorkomend probleem: een library die CommonJS exporteert, of een import die tree shaking omzeilt.

Slecht — hele lodash-bibliotheek wordt meegenomen:
javascript
import _ from 'lodash';
const result = _.get(obj, 'path.to.key');
Goed — alleen de gebruikte functie wordt gebundeld:
javascript
import { get } from 'lodash-es';
const result = get(obj, 'path.to.key');

Het verschil: 72KB vs 1KB. Dat is geen exaggeratie.

Controleer sideEffects in package.json

Webpack en Vite gebruiken de sideEffects-vlag om te bepalen of een module veilig verwijderd kan worden. Als een library dit niet correct declareert, wordt tree shaking uitgeschakeld voor die library.

json
// package.json van je project
{
  "sideEffects": [
    "*.css",
    "*.scss"
  ]
}

moment.js vervangen door day.js

moment.js is 67KB gzip. day.js doet hetzelfde voor 2KB. De API is nagenoeg identiek.

bash
npm uninstall moment
npm install dayjs
javascript
// Vervang dit:
import moment from 'moment';
const formatted = moment(date).format('DD-MM-YYYY');

// Door dit:
import dayjs from 'dayjs';
const formatted = dayjs(date).format('DD-MM-YYYY');

Directe besparing: 65KB gzip per pagina die moment.js laadde.

Stap 3: code splitting

Code splitting verdeelt je bundel in kleinere stukken die alleen worden geladen wanneer ze nodig zijn.

Route-gebaseerde code splitting

Het meest impactvolle type. Laad per pagina alleen de code die die pagina nodig heeft.

javascript
// React — lazy loading per route
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

const ProductPage = lazy(() => import('./pages/ProductPage'));
const CheckoutPage = lazy(() => import('./pages/CheckoutPage'));
const AccountPage = lazy(() => import('./pages/AccountPage'));

function App() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Routes>
        <Route path="/product/:id" element={<ProductPage />} />
        <Route path="/checkout" element={<CheckoutPage />} />
        <Route path="/account" element={<AccountPage />} />
      </Routes>
    </Suspense>
  );
}

Resultaat: de checkout-code wordt alleen gedownload wanneer een gebruiker naar de checkout navigeert. Niet op de homepage, niet op de productpagina.

Component-gebaseerde code splitting

Voor zware componenten die niet direct zichtbaar zijn:

javascript
// Laad de afbeeldingsgalerij pas wanneer de productpagina rendert
const ImageGallery = lazy(() => import('./components/ImageGallery'));

// Laad de recensies pas wanneer de gebruiker ernaar scrollt
const ReviewSection = lazy(() =>
  import('./components/ReviewSection')
);

Vendor splitting

Splits je eigen code van third-party libraries. Libraries veranderen minder vaak dan je eigen code. Hierdoor kunnen gebruikers je eigen code herladen terwijl de vendor-bundel uit de cache wordt geserveerd.

javascript
// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor-react': ['react', 'react-dom'],
          'vendor-ui': ['@headlessui/react', 'framer-motion'],
          'vendor-utils': ['date-fns', 'axios'],
        }
      }
    }
  }
};

Stap 4: lazy imports voor zware dependencies

Sommige dependencies zijn zwaar maar worden zelden direct gebruikt. Importeer ze asynchroon op het moment dat ze nodig zijn.

Voorbeeld: Google Maps alleen laden wanneer nodig

javascript
// Niet bij pageload laden — alleen als gebruiker de kaart bekijkt
async function loadMap(containerId) {
  const { default: initMap } = await import('./utils/googleMaps');
  initMap(containerId);
}

// Gebruik IntersectionObserver om te detecteren wanneer de kaart in beeld komt
const observer = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) {
    loadMap('map-container');
    observer.disconnect();
  }
});

observer.observe(document.getElementById('map-container'));

Voorbeeld: grafiekbibliotheek alleen op dashboard-pagina

javascript
// Op de productpagina laadt dit component nooit de grafiekcode
async function renderSalesChart(data) {
  const { Chart } = await import('chart.js/auto');
  const canvas = document.getElementById('sales-chart');
  new Chart(canvas, {
    type: 'line',
    data: data,
  });
}

Stap 5: optimaliseer bestaande imports

Specifieke icon-imports

Icon-libraries zijn grote bundels. Importeer alleen wat je gebruikt.

javascript
// Slecht: hele bibliotheek geladen (500KB+)
import * as Icons from '@heroicons/react/24/outline';

// Goed: alleen gebruikte icons (3KB)
import { ShoppingCartIcon, UserIcon, SearchIcon }
  from '@heroicons/react/24/outline';

CSS-in-JS versus statische CSS

CSS-in-JS-oplossingen (styled-components, Emotion) voegen runtime-overhead toe aan je JavaScript-bundel. Bij 10.000 gestylede componenten loopt dit op tot 50-80KB extra JavaScript.

Tailwind CSS met PurgeCSS produceert statische CSS zonder JavaScript-overhead. Voor e-commerce-frontends die performance prioriteren is dit de betere keuze.

De resultaten: wat je realistische verwachting is

Op basis van de projecten die wij uitvoeren:

StartpuntNa tree shakingNa code splittingNa lazy importsEindresultaat
1,5MB (Luma)1,1MB650KB per route420KB initieel~300KB initieel
800KB (custom build)600KB380KB per route280KB initieel~200KB initieel
400KB (Hyvä)320KB220KB per route180KB initieel~150KB initieel

De "initieel geladen" bundel is wat telt voor Core Web Vitals. Alles wat lazy wordt geladen, blokkeert de eerste render niet.

Lees meer over de performance-vergelijking tussen Luma en Hyvä in ons artikel Hyvä vs. Luma performance vergelijking.

Veelgemaakte fouten

  1. Tree shaking configureren maar CommonJS-dependencies gebruiken — controleer altijd of de lodash-es, date-fns of andere ESM-varianten beschikbaar zijn.
  2. Code splitting te grof toepassen — te veel kleine chunks vergroot het aantal HTTP-verzoeken. Gebruik de 20KB-regel: chunks kleiner dan 20KB gzip zijn zelden de moeite waard om apart te splitsen.
  3. Lazy loading zonder skeleton/loading state — gebruikers zien een lege plek. Gebruik altijd een Suspense fallback of skeleton component.
  4. Vendor splitting vergeten — als je vendor-bundel bij elke deployment verandert, verdwijnt het cache-voordeel.
  5. Analyseren na development-build — analyseer altijd de productiebuild. Development-builds bevatten geen tree shaking en zijn 3-5x groter.

Stap 6: compressie en caching op serverniveau

Bundel-optimalisatie stopt niet bij de build. Hoe je bestanden serveert maakt ook uit.

Brotli en gzip compressie

Brotli compresseert JavaScript 15-20% beter dan gzip. Moderne browsers ondersteunen het allemaal. Configureer je server om Brotli-gecomprimeerde bestanden te serveren als de browser het ondersteunt.

Nginx-configuratie:

nginx
# Nginx: Brotli compressie inschakelen
brotli on;
brotli_comp_level 6;
brotli_types application/javascript text/css text/html;

# Fallback naar gzip voor oudere browsers
gzip on;
gzip_types application/javascript text/css text/html;
gzip_comp_level 6;

Vercel, Cloudflare en de meeste moderne hostingplatforms activeren dit automatisch. Op eigen servers moet je dit handmatig inschakelen.

Cache-headers voor JavaScript-bestanden

Gebundelde JavaScript-bestanden hebben doorgaans een content-hash in de bestandsnaam (main.a3f2bc.js). Dat maakt langdurige caching veilig: de bestandsnaam verandert alleen als de inhoud verandert.

nginx
# JavaScript met content-hash: agressief cachen
location ~* \.(js)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

Met immutable hint je de browser dat het bestand nooit zal veranderen. De browser slaat het op tot een jaar lokaal op en voert geen revalidatieverzoeken uit. Dat scheelt één HTTP-roundtrip per bezoek voor terugkerende gebruikers.

HTTP/2 en HTTP/3: parallelle lading

HTTP/2 maakt multiplexing mogelijk: meerdere bestanden worden parallel via één verbinding geladen. Dat vermindert de overhead van veel kleine chunks.

HTTP/3 (QUIC) verbetert dit verder op mobiele netwerken met hoge latentie. Cloudflare en moderne hostingproviders ondersteunen HTTP/3 standaard.

Bij code splitting naar veel kleine chunks is HTTP/2 of HTTP/3 een vereiste. Bij HTTP/1.1 is zes parallelle verbindingen het maximum — meer chunks betekent wachtrijen.

Performance budget: borgen wat je hebt gebouwd

Een bundel die je vandaag terugbrengt naar 300KB groeit morgen weer als er geen bewaking is.

Een performance budget stelt maxima in voor bundelgrootte, die worden gecontroleerd in de CI/CD-pipeline. Elke build die het budget overschrijdt, faalt.

javascript
// vite.config.js — performance budget
export default {
  build: {
    rollupOptions: {
      output: {
        // Waarschuwing bij chunks groter dan 200KB
      }
    },
    // Faalt de build als de totale asset-grootte het budget overschrijdt
    chunkSizeWarningLimit: 200,
  }
};

Voor CI/CD-integratie met Lighthouse:

yaml
# GitHub Actions: Lighthouse CI
- name: Lighthouse CI
  uses: treosh/lighthouse-ci-action@v11
  with:
    budgetPath: './budget.json'
    uploadArtifacts: true
json
// budget.json
[
  {
    "path": "/*",
    "resourceSizes": [
      { "resourceType": "script", "budget": 300 },
      { "resourceType": "total", "budget": 600 }
    ]
  }
]

Als een pull request de scriptbundel boven 300KB brengt, faalt de CI-check. Zo wordt iedere bijdrage aan bundelgroei direct zichtbaar.

Conclusie: 300KB is haalbaar

Van 1,5MB naar 300KB is geen theoretische oefening. Het is een kwestie van consequent toepassen van tree shaking, route-gebaseerde code splitting en lazy imports voor zware dependencies — en dat daarna bewaken met een performance budget.

Begin met de bundle-analyse. Identificeer de drie grootste boosdoeners. Pak die aan. Zet een CI-check op zodat de winst niet wegvloeit. Herhaal per kwartaal.

Elke 100KB die je van je bundel afhaalt, is gemiddeld 200-400ms minder wachttijd op een mobiele verbinding. Bij een conversieratio van 3% en een mobiel aandeel van 60% is dat geld. Lees ook ons artikel over third-party scripts en performance en de impact op Largest Contentful Paint.

Wij optimaliseren JavaScript-bundels voor Magento- en Shopify-webshops. Neem contact op voor een performance-audit.

Veelgestelde vragen

Hoe groot mag een JavaScript-bundel zijn?

Er is geen universele norm, maar een gangbaar performance budget is 300KB gzip voor de initieel geladen JavaScript. Alles wat lazy wordt geladen telt minder zwaar, omdat het de first render niet blokkeert. Google's Core Web Vitals meten de impact indirect via LCP en INP.

Werkt tree shaking automatisch in webpack en Vite?

Ja, maar alleen als aan de voorwaarden wordt voldaan. Tree shaking werkt uitsluitend op ES modules (ESM). Libraries die CommonJS gebruiken worden niet geanalyseerd. Controleer of je dependencies ESM-exports ondersteunen. Veel populaire libraries hebben een -es of /esm variant.

Wat is het verschil tussen defer en async voor scripts?

defer wacht met uitvoering tot de HTML volledig is geparsed, en bewaart de volgorde van scripts. async voert uit zodra het script is gedownload, ongeacht HTML-parse-status en ongeacht volgorde. Gebruik defer voor scripts die de DOM nodig hebben, async voor volledig onafhankelijke scripts (analytics, trackers).

Is code splitting altijd beter?

Nee. Te veel kleine chunks vergroot het aantal HTTP-verzoeken en kan bij HTTP/1.1 langzamer zijn dan één grotere bundel. De praktische richtlijn: splits chunks pas uit als ze groter zijn dan 20KB gzip en niet op de initiële pagina nodig zijn. Gebruik HTTP/2 of HTTP/3 als je veel kleine chunks serveert.

Hoe behoud ik de winst na de optimalisatie?

Stel een performance budget in dat gecontroleerd wordt in de CI/CD-pipeline. Tools als Lighthouse CI of bundlesize voorkomen dat bundelgroei onopgemerkt blijft. Definieer een maximum bundelgrootte in kilobytes, en laat de build falen als het budget wordt overschreden.

Ruthger Idema

Geschreven door Ruthger Idema

15+ jaar ervaring in e-commerce development. Gespecialiseerd in Magento, Shopify en Laravel maatwerk.

Meer over ons team →
Deel dit artikel:

Wil je jouw e-commerce naar het volgende niveau?

Plan een vrijblijvend gesprek met onze experts over Magento, Shopify of Laravel maatwerk.

Plan een Tech Check