Før ES-moduler hadde JavaScript ingen innebygd modulstruktur. Du hadde script-tagger i riktig rekkefølge, IIFE-mønstre for å simulere innkapsling, og deretter et tiår med CommonJS i Node.js. ES-moduler — standardisert i ES2015 og nå støttet overalt — er den ekte løsningen. Denne guiden dekker alt fra det grunnleggende om navngitte vs. standard-eksporter til dynamiske importer, tree-shaking og mønstrene som holder store kodebaser vedlikeholdbare.

Navngitte eksporter vs. standard-eksporter

Det finnes to måter å eksportere fra en modul. Å forstå forskjellen — og avveiningene — er grunnlaget for alt annet:

js
// utils/currency.js

// Named exports — explicit names, multiple per file
export function formatPrice(amount, currency = 'USD') {
  return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
}

export function parseCents(cents) {
  return cents / 100;
}

export const DEFAULT_CURRENCY = 'USD';
js
// Importing named exports — names must match
import { formatPrice, parseCents } from './utils/currency.js';

// You can rename on import
import { formatPrice as fmt } from './utils/currency.js';

// Import the entire namespace as an object
import * as Currency from './utils/currency.js';
Currency.formatPrice(49.99); // works
js
// components/ProductCard.js

// Default export — one per file, no name required
export default function ProductCard({ product }) {
  return `<div class="card">${product.name}</div>`;
}
js
// Importing a default export — you choose the name
import ProductCard from './components/ProductCard.js';
import Card        from './components/ProductCard.js'; // same thing, different name
Mening: Navngitte eksporter er nesten alltid bedre for hjelpemoduler. De er eksplisitte, IDE-vennlige (autofullfør og "gå til definisjon" fungerer perfekt) og trygge å refaktorere — å gi nytt navn til en funksjon fanges opp av verktøyene dine, ikke testene dine. Standard-eksporter gir mening for komponenter som er "den ene tingen" en fil eksporterer, som React/Angular-komponenter eller rutebehandler-filer.

Barrel-filer — nyttige og farlige

En barrel-fil er en index.js som re-eksporterer fra flere moduler og gir forbrukerne en renere importsti. De er overalt i store prosjekter:

js
// utils/index.js — the barrel
export { formatPrice, parseCents, DEFAULT_CURRENCY } from './currency.js';
export { validateEmail, validatePhone }              from './validation.js';
export { debounce, throttle }                        from './timing.js';
export { fetchWithRetry, buildQueryString }          from './http.js';
js
// Consumer — clean import path, no need to know the file structure
import { formatPrice, debounce, fetchWithRetry } from './utils/index.js';
// or with bundlers that resolve index.js automatically:
import { formatPrice, debounce, fetchWithRetry } from './utils';

Faren: barrel-filer kan ødelegge tree-shaking. Hvis du importerer én funksjon fra en barrel som re-eksporterer 40 moduler, trekker noen bundlere inn alle 40 — selv de du ikke bruker. Løsningen er å være selektiv om hva du legger i barrel, og sørge for at bundleren støtter sideEffects: false i package.json.

Dynamisk import() — kodeoppdelingpå forespørsel

Statiske importer (import x from '...') løses ved bundle-tid. Dynamisk import() er en funksjon som laster en modul dovent ved kjøring og returnerer et Promise. Det er slik du implementerer rutebasert kodeoppdeling:

js
// Load a heavy chart library only when the user navigates to the analytics page
async function loadAnalyticsDashboard() {
  const { Chart }   = await import('chart.js');
  const { buildDashboardData } = await import('./analytics/data.js');

  const data   = await buildDashboardData();
  const canvas = document.getElementById('dashboard-chart');
  new Chart(canvas, { type: 'bar', data });
}

// Attach to a route change
router.on('/analytics', loadAnalyticsDashboard);
js
// Feature flags — only load a module if the feature is enabled
async function initFeature(featureName) {
  const flags = await getFeatureFlags();

  if (flags[featureName]) {
    const module = await import(`./features/${featureName}.js`);
    module.init();
  }
}

Webpack, Rollup og Vite forstår alle dynamisk import() og vil automatisk dele den lastede modulen inn i et separat chunk. Resultatet: mindre startbundle, raskere første innlasting.

import.meta — modulmetadata

import.meta er et objekt som gir deg metadata om den gjeldende modulen. Den mest nyttige egenskapen er import.meta.url — URL-en til den gjeldende modulfilen:

js
// Get the directory of the current module (Node.js)
import { fileURLToPath } from 'node:url';
import { dirname, join }  from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname  = dirname(__filename);

// Now you can do path resolution the same way you would with CommonJS
const configPath = join(__dirname, '../config/app.json');
js
// In Vite projects, import.meta.env gives you environment variables
const apiBase = import.meta.env.VITE_API_BASE_URL;
const isProd  = import.meta.env.PROD; // boolean

// import.meta.glob — Vite-specific, import multiple files at once
const modules = import.meta.glob('./pages/*.js');
for (const path in modules) {
  const module = await modules[path]();
  console.log(path, module);
}

ES-moduler vs. CommonJS

Du vil fortsatt støte på CommonJS (require()) i eldre Node.js-kodebaser og noen npm-pakker. Her er en rask referanse for forskjellene som skaper problemer:

js
// CommonJS (CJS) — still common in Node.js
const express = require('express');
const { join } = require('path');
module.exports = { myFunction };
module.exports.default = MyClass;

// ES Modules (ESM) — the standard
import express from 'express';
import { join } from 'path';
export { myFunction };
export default MyClass;

Viktige forskjeller: ES-moduler er statisk analyserbare (importer løses før koden kjører), mens CommonJS require kjøres ved kjøretid. ESM bruker async-innlasting som standard; CJS er synkron. I Node.js behandles filer med filendelsen .mjs eller med "type": "module" i package.json som ESM.

Interop-fallgruve: Du kan import:ere en CommonJS-modul fra en ESM-fil (Node.js håndterer dette), men du kan ikke require():ere en ESM-modul fra en CommonJS-fil. Hvis du trenger det, bruk et dynamisk import()-kall i stedet — det fungerer i begge modulsystemene.

Tree-shaking og navngitte eksporter

Tree-shaking er bundler-prosessen med å fjerne kode du importerer men aldri bruker. Det krever navngitte eksporter og statiske import-setninger — dynamiske importer og CommonJS er mye vanskeligere å tree-shake. Slik skriver du moduler som tree-shaker godt:

js
// ✅ Tree-shakeable — bundler can see exactly what's exported
export function formatDate(date) { /* ... */ }
export function parseDate(str)   { /* ... */ }
export function addDays(date, n) { /* ... */ }

// If you only import formatDate, parseDate and addDays are excluded from the bundle
import { formatDate } from './utils/dates.js';
js
// ❌ Harder to tree-shake — the whole object is one export
const dateUtils = {
  formatDate: (date) => { /* ... */ },
  parseDate:  (str)  => { /* ... */ },
  addDays:    (date, n) => { /* ... */ }
};
export default dateUtils;

// Even if you only use dateUtils.formatDate, the whole object may be bundled

For biblioteker publisert til npm er tree-shaking-støtte forskjellen mellom et bibliotek folk anbefaler og ett de unngår. MDN Modules-guiden dekker de fullstendige statiske analysereglene som muliggjør det.

Native ES-moduler i nettleseren

Du kan bruke ES-moduler direkte i nettleseren — ingen bundler nødvendig. Legg til type="module" i script-taggen din:

js
<!-- index.html -->
<!-- type="module" enables ES Module semantics -->
<!-- deferred by default — runs after HTML is parsed -->
<script type="module" src="./app.js"></script>
js
// app.js — can use import/export directly
import { initRouter }    from './router.js';
import { setupAuth }     from './auth.js';
import { loadDashboard } from './dashboard.js';

async function main() {
  await setupAuth();
  initRouter();
  await loadDashboard();
}

main();

Modulskript er alltid utsatt og kjører i strict-modus som standard. Hver modul kjøres bare én gang, selv om den importeres av flere andre moduler — nettleseren cacher modulinstansen. Det er verdt å vite når du har delt tilstand i moduler.

Et ekte refaktoreringseksempel

Her er hvordan en typisk utils-fil vokser ut av kontroll og deles riktig inn i moduler:

js
// Before: one giant utils.js with everything
export function formatPrice(amount) { /* ... */ }
export function validateEmail(email) { /* ... */ }
export function debounce(fn, ms) { /* ... */ }
export function fetchWithRetry(url, opts) { /* ... */ }
export function parseCSV(text) { /* ... */ }
export function buildQueryString(params) { /* ... */ }

// After: split by concern
// utils/
//   currency.js     → formatPrice, parseCents
//   validation.js   → validateEmail, validatePhone
//   timing.js       → debounce, throttle
//   http.js         → fetchWithRetry, buildQueryString
//   csv.js          → parseCSV, formatCSV
//   index.js        → re-exports all of the above (optional barrel)

Å dele opp etter ansvar betyr at hver fil har ett enkelt ansvar, tester er enklere å skrive og finne, og bundlere kan tree-shake på filnivå. Barrel-filen holder importer rene for forbrukere som foretrekker den enkle importstien.

Nyttige verktøy

Når du bygger modulær JavaScript, holder JS Formatter importblokkene dine rene og konsistente. For å forstå hva som er i den endelige bundlen, viser verktøy som Webpack Bundle Analyzer eller Rollups visualizer hvilke moduler som tar opp plass. TC39 ECMAScript Modules-spesifikasjonen har den nøyaktige løsningssemantikken hvis du trenger å forstå kanttilfeller.

Oppsummering

ES-moduler er grunnlaget for moderne JavaScript-arkitektur. Foretrekk navngitte eksporter for hjelpefunksjoner — de er eksplisitte, tree-shakeable og IDE-vennlige. Bruk dynamisk import() for å dele tung kode ut av startbundlen din. Vær forsiktig med barrel-filer i store kodebaser. Og hvis du fortsatt bruker CommonJS i et nytt Node.js-prosjekt, er det på tide å bytte — økosystemet har fullt ut skiftet til ESM.