Web Development

JavaScript ES Modules: Import, Export & Tree Shaking

Tutorial lengkap ES Modules β€” named exports, default exports, dynamic imports, top-level await, re-exports, tree shaking, dan best practices modular JavaScript

1. Pengenalan ES Modules

ES Modules (ESM) adalah sistem modul resmi yang diperkenalkan dalam spesifikasi ECMAScript 2015 (ES6). ESM memungkinkan Anda memecah kode JavaScript menjadi file-file terpisah yang saling terhubung melalui mekanisme import dan export. Ini adalah standar modern yang didukung oleh semua browser utama dan Node.js.

Sebelum ESM, JavaScript tidak memiliki sistem modul bawaan di browser. Pengembang mengandalkan pola seperti IIFE (Immediately Invoked Function Expressions), CommonJS (Node.js), atau AMD (RequireJS). ESM menyatukan semuanya menjadi standar yang konsisten.

Mengapa ES Modules Penting?

Keunggulan Penjelasan
ModularitasKode terorganisir dalam file terpisah, mudah dikelola dan dipelihara
EncapsulationSetiap modul memiliki scope sendiri, variabel tidak bocor ke global
Tree ShakingBundler bisa menghapus kode yang tidak digunakan untuk mengurangi ukuran bundle
Static AnalysisStruktur import/export bisa dianalisis saat compile time
Browser NativeDidukung langsung oleh browser modern tanpa perlu bundler
Async LoadingDynamic import memungkinkan lazy loading modul secara asynchronous

ES Modules vs CommonJS vs AMD

Aspek ES Modules CommonJS AMD
Sintaksimport/exportrequire/module.exportsdefine/require
LoadingStatic & AsyncSynchronousAsynchronous
EnvironmentBrowser & Node.jsNode.js (server)Browser
Tree Shakingβœ… Ya❌ Tidak❌ Tidak
Top-level Awaitβœ… Ya❌ Tidak❌ Tidak
Strict ModeOtomatisManualManual
Diagram: Arsitektur ES Modules
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚               APLIKASI JAVASCRIPT                     β”‚
β”‚                                                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚ main.js  β”‚  β”‚ utils.js β”‚  β”‚ components.js    β”‚    β”‚
β”‚  β”‚          β”‚  β”‚          β”‚  β”‚                  β”‚    β”‚
β”‚  β”‚ import   │←─│ export   β”‚  β”‚ export default   β”‚    β”‚
β”‚  β”‚ { fungsi}β”‚  β”‚ { fungsi}β”‚  β”‚ class Komponen   β”‚    β”‚
β”‚  β”‚          β”‚  β”‚          β”‚  β”‚                  β”‚    β”‚
β”‚  β”‚ import   │──│→ import  │←─│ import dari utilsβ”‚    β”‚
β”‚  β”‚ default  β”‚  β”‚          β”‚  β”‚                  β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                                                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚              Build Tool (Vite/webpack)          β”‚  β”‚
β”‚  β”‚   β€’ Resolusi dependency                         β”‚  β”‚
β”‚  β”‚   β€’ Tree shaking (hapus unused exports)         β”‚  β”‚
β”‚  β”‚   β€’ Code splitting (lazy load chunks)           β”‚  β”‚
β”‚  β”‚   β€’ Output: optimized bundles                   β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Setup Awal

Untuk menggunakan ES Modules di browser, tambahkan atribut type="module" pada tag script di HTML:

HTML β€” Menggunakan ES Modules
<!-- Menggunakan module script -->
<script type="module" src="main.js"></script>

<!-- Inline module -->
<script type="module">
  import { sapaan } from './utils.js';
  console.log(sapaan('Budi'));
</script>

<!-- Perbedaan: module vs classic script -->
<!-- Module: strict mode otomatis, scope terpisah, deferred loading -->
<!-- Classic: global scope, tidak deferred -->
⚠️ Catatan Penting

Ketika menggunakan type="module", beberapa aturan otomatis berlaku: strict mode aktif secara default, setiap modul memiliki scope sendiri (tidak ada variabel global), dan file dimuat secara deferred (setelah HTML selesai diparse). Anda juga tidak bisa memuat modul dari file:// protocol β€” perlu HTTP server lokal.

2. Named Export

Named export memungkinkan Anda mengekspor beberapa nilai dari satu modul. Setiap nilai diberi nama dan harus diimpor dengan nama yang sama. Named export sangat berguna untuk utility functions, konstanta, dan komponen yang saling terkait.

Sintaks Named Export

JavaScript β€” Named Export
// ====== math.js ======

// Cara 1: Export saat deklarasi
export const PI = 3.14159265358979;
export const E = 2.71828182845904;

export function tambah(a, b) {
  return a + b;
}

export function kurang(a, b) {
  return a - b;
}

export function kali(a, b) {
  return a * b;
}

export function bagi(a, b) {
  if (b === 0) throw new Error('Tidak bisa dibagi nol');
  return a / b;
}

// Cara 2: Export di akhir file (lebih rapi untuk file besar)
const akarKuadrat = (x) => Math.sqrt(x);
const pangkat = (base, exp) => Math.pow(base, exp);
const absolut = (x) => Math.abs(x);

export { akarKuadrat, pangkat, absolut };

// Export dengan alias (rename)
export { tambah as add, kurang as subtract };

// Export dengan komentar dokumentasi
/** Menghitung rata-rata dari array angka */
export function rataRata(angka) {
  if (angka.length === 0) return 0;
  return angka.reduce((a, b) => a + b, 0) / angka.length;
}

/** Menghitung median dari array angka */
export function median(angka) {
  const sorted = [...angka].sort((a, b) => a - b);
  const mid = Math.floor(sorted.length / 2);
  return sorted.length % 2 !== 0
    ? sorted[mid]
    : (sorted[mid - 1] + sorted[mid]) / 2;
}

Mengimpor Named Exports

JavaScript β€” Import Named
// Import beberapa fungsi dari math.js
import { tambah, kurang, kali, bagi } from './math.js';

console.log(tambah(10, 5));   // 15
console.log(kurang(10, 5));   // 5
console.log(kali(10, 5));     // 50
console.log(bagi(10, 5));     // 2

// Import dengan alias (rename)
import { tambah as add, kali as multiply } from './math.js';
console.log(add(3, 4));        // 7
console.log(multiply(3, 4));   // 12

// Import semua named exports sebagai namespace
import * as MathLib from './math.js';
console.log(MathLib.tambah(1, 2));     // 3
console.log(MathLib.PI);              // 3.14159...
console.log(MathLib.rataRata([1,2,3])); // 2

Named Export dari Class

JavaScript β€” Export Class
// ====== models/User.js ======
export class User {
  constructor(nama, email, role = 'member') {
    this.nama = nama;
    this.email = email;
    this.role = role;
    this.createdAt = new Date();
  }

  getDisplayInfo() {
    return `${this.nama} (${this.email}) - ${this.role}`;
  }

  isAdmin() {
    return this.role === 'admin';
  }
}

export class AdminUser extends User {
  constructor(nama, email, permissions = []) {
    super(nama, email, 'admin');
    this.permissions = permissions;
  }

  hasPermission(permission) {
    return this.permissions.includes(permission);
  }
}

// ====== app.js ======
import { User, AdminUser } from './models/User.js';

const userBaru = new User('Budi', 'budi@email.com');
const adminBaru = new AdminUser('Sari', 'sari@email.com', ['read', 'write', 'delete']);

console.log(userBaru.getDisplayInfo());
// "Budi (budi@email.com) - member"

console.log(adminBaru.hasPermission('delete')); // true
πŸ’‘ Kapan Menggunakan Named Export?

Gunakan named export ketika: (1) modul mengekspor beberapa hal sekaligus, (2) Anda ingin tree shaking bekerja optimal, (3) nama ekspor penting untuk konteks penggunaan, atau (4) Anda mengekspor utility functions dan konstanta.

3. Default Export

Default export memungkinkan Anda mengekspor satu nilai utama dari setiap modul. Saat mengimpor, Anda bisa memberi nama apa saja β€” tidak harus sesuai dengan nama aslinya. Default export cocok untuk komponen utama, kelas utama, atau fungsi utama dari sebuah modul.

Sintaks Default Export

JavaScript β€” Default Export
// ====== Cara 1: Inline default export ======

// Export class sebagai default
export default class Kalkulator {
  constructor() {
    this.hasil = 0;
  }

  tambah(n) { this.hasil += n; return this; }
  kurang(n) { this.hasil -= n; return this; }
  kali(n)   { this.hasil *= n; return this; }
  reset()   { this.hasil = 0; return this; }
  getHasil() { return this.hasil; }
}

// Export function sebagai default
export default function buatLogger(prefix) {
  return function(message) {
    const waktu = new Date().toLocaleTimeString('id-ID');
    console.log(`[${waktu}] [${prefix}] ${message}`);
  };
}

// Export arrow function sebagai default
export default (items) => items.filter(Boolean).map(item => item.trim());

// ====== Cara 2: Deklarasi terpisah ======
function formatRupiah(angka) {
  return new Intl.NumberFormat('id-ID', {
    style: 'currency',
    currency: 'IDR',
    minimumFractionDigits: 0,
  }).format(angka);
}

export default formatRupiah;

// ====== Cara 3: Default export object/konfigurasi ======
const konfigurasi = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3,
  debug: false,
};

export default konfigurasi;

// ====== Default + Named dalam satu file ======
// models/Produk.js

// Default export β€” satu per file
export default class Produk {
  constructor(nama, harga) {
    this.nama = nama;
    this.harga = harga;
  }
}

// Named exports β€” bisa banyak per file
export const KATEGORI_ELEKTRONIK = 'elektronik';
export const KATEGORI_PAKAIAN = 'pakaian';
export const KATEGORI_MAKANAN = 'makanan';

export function formatHargaProduk(produk) {
  return `${produk.nama}: Rp ${produk.harga.toLocaleString('id-ID')}`;
}

Mengimpor Default Export

JavaScript β€” Import Default
// Import default β€” nama bebas, tidak perlu kurung kurawal
import Kalkulator from './Kalkulator.js';

const calc = new Kalkulator();
calc.tambah(10).kali(2).kurang(5);
console.log(calc.getHasil()); // 15

// Import default dengan nama berbeda (alias)
import FormatRupiah from './formatRupiah.js';
console.log(FormatRupiah(150000)); // "Rp 150.000"

// Import default + named sekaligus
import Produk, { KATEGORI_ELEKTRONIK, formatHargaProduk } from './Produk.js';

const laptop = new Produk('Laptop ASUS', 12000000);
console.log(formatHargaProduk(laptop));
// "Laptop ASUS: Rp 12.000.000"

// Import default sebagai namespace berbeda
import Logger from './logger.js';
const logAplikasi = Logger('APP');
const logDatabase = Logger('DB');
logAplikasi('Aplikasi dimulai');    // [14:30:00] [APP] Aplikasi dimulai
logDatabase('Koneksi terbuka');     // [14:30:00] [DB] Koneksi terbuka

// Import default dan rename semua named exports
import DefaultComp, {
  helperA as utilA,
  helperB as utilB,
  CONSTANT as CONST
} from './myModule.js';
⚠️ Default Export vs Named Export

Hanya boleh ada satu default export per file, tetapi bisa ada banyak named exports. Default export diimpor tanpa kurung kurawal, sedangkan named export wajib menggunakan kurung kurawal { }. Banyak developer menggabungkan keduanya dalam satu file untuk fleksibilitas maksimal.

4. Import Syntax & Variasi

ES Modules menyediakan berbagai cara untuk mengimpor modul. Memahami semua variasi import membantu Anda menulis kode yang lebih bersih dan efisien.

Semua Variasi Import

JavaScript β€” Import Variations
// ====== 1. Named imports ======
import { useState, useEffect, useCallback } from 'react';
import { format, parseISO, differenceInDays } from 'date-fns';

// ====== 2. Default import ======
import axios from 'axios';
import React from 'react';

// ====== 3. Default + Named ======
import React, { useState, useEffect } from 'react';

// ====== 4. Namespace import (semua exports sebagai objek) ======
import * as helpers from './helpers.js';
console.log(helpers.formatDate(new Date()));
console.log(helpers.validateEmail('test@mail.com'));

// ====== 5. Side-effect import (jalankan kode tanpa import values) ======
import './styles/global.css';
import './polyfills.js';
import './analytics.js';

// ====== 6. Import dengan alias ======
import { calculateTotal as hitungTotal } from './utils/calculation.js';
import { User as Pengguna } from './models/User.js';

// ====== 7. Gabungan import ======
import DefaultExport, {
  namedExport1,
  namedExport2,
  namedExport3 as alias3,
} from './complexModule.js';

// ====== 8. Import dari package (node_modules) ======
import lodash from 'lodash';
import { v4 as uuidv4 } from 'uuid';

// ====== 9. Import JSON (Node.js dengan assert) ======
import data from './config.json' assert { type: 'json' };

// ====== 10. Import URL (browser) ======
import { teknik } from 'https://cdn.example.com/lib.js';

Import Assertions & Import Attributes

JavaScript β€” Import Attributes
// Import assertion (proposal lama, beberapa browser)
import data from './data.json' assert { type: 'json' };

// Import attributes (proposal baru β€” diganti keyword 'with')
import data from './data.json' with { type: 'json' };

// Import CSS module (eksperimental)
import styles from './Button.module.css' with { type: 'css' };

// Konsumsi dalam code
console.log(data.nama);
console.log(data.produk);

// Contoh dengan config
import appConfig from './config.json' with { type: 'json' };

function initApp() {
  return {
    apiUrl: appConfig.apiUrl,
    port: appConfig.port || 3000,
    env: appConfig.env || 'development',
  };
}

Path Resolution

JavaScript β€” Path Resolution
// Relative paths β€” diawali dengan ./ atau ../
import { helper } from './utils/helper.js';     // satu level ke bawah
import { config } from '../config/app.js';       // satu level ke atas
import { logger } from '../../lib/logger.js';    // dua level ke atas

// Absolute paths β€” tanpa awalan (biasanya dari node_modules atau alias)
import express from 'express';                    // dari node_modules
import { Button } from '@/components/Button.js'; // alias path

// URL paths (browser)
import { lib } from 'https://cdn.skypack.dev/lodash-es';

// Dynamic path β€” TIDAK BISA karena import bersifat static
// import x from path;           // ❌ Error!
// const x = await import(path); // βœ… Gunakan dynamic import

5. Re-exports & Barrel Files

Re-export memungkinkan Anda mengekspor ulang modul yang diimpor dari modul lain. Teknik ini sangat berguna untuk membuat barrel files β€” file index yang mengumpulkan semua export dari direktori menjadi satu pintu masuk yang rapi.

Teknik Re-export

JavaScript β€” Re-exports
// ====== Re-export named exports ======

// utils/index.js β€” Re-export dari semua file di folder utils
export { formatRupiah, formatTanggal, formatWaktu } from './formatters.js';
export { validateEmail, validatePhone, validatePassword } from './validators.js';
export { debounce, throttle, deepClone } from './helpers.js';
export { default as Konstanta } from './constants.js';

// Re-export dengan alias
export {
  calculateTotal as hitungTotal,
  calculateDiscount as hitungDiskon,
  calculateTax as hitungPajak,
} from './calculations.js';

// Re-export default sebagai named
export { default as ApiClient } from './ApiClient.js';
export { default as AuthService } from './AuthService.js';

// Re-export default sebagai default (jarang tapi mungkin)
export { default } from './mainService.js';

// ====== Re-export semua dari modul ======
// Ekspor semua named exports dari module (kecuali default)
export * from './mathOperations.js';

// Ekspor semua + named default export
export * from './mathOperations.js';
export { default as MathOps } from './mathOperations.js';

Barrel File Pattern

File Structure β€” Barrel Pattern
// Struktur folder:
src/
β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ Button.js        export default class Button { ... }
β”‚   β”œβ”€β”€ Modal.js         export class Modal { ... }
β”‚   β”œβ”€β”€ Navbar.js        export default class Navbar { ... }
β”‚   β”œβ”€β”€ Card.js          export default class Card { ... }
β”‚   └── index.js         ← BARREL FILE
β”œβ”€β”€ utils/
β”‚   β”œβ”€β”€ format.js        export function formatRupiah() { ... }
β”‚   β”œβ”€β”€ validate.js      export function validateEmail() { ... }
β”‚   └── index.js         ← BARREL FILE
└── main.js

// components/index.js β€” Barrel File
export { default as Button } from './Button.js';
export { Modal } from './Modal.js';
export { default as Navbar } from './Navbar.js';
export { default as Card } from './Card.js';

// utils/index.js β€” Barrel File
export * from './format.js';
export * from './validate.js';

// main.js β€” Import dari barrel files (sangat rapi!)
import { Button, Modal, Navbar, Card } from './components/index.js';
import { formatRupiah, validateEmail } from './utils/index.js';
// Atau tanpa /index.js (bundler resolve otomatis)
import { Button, Modal, Navbar, Card } from './components';
import { formatRupiah, validateEmail } from './utils';
πŸ’‘ Barrel File: Kelebihan & Kekurangan

Kelebihan: Import lebih rapi, path lebih pendek, API modul jelas terlihat. Kekurangan: Bisa menimbulkan circular dependency, dapat memperbesar bundle jika barrel mengimpor banyak modul yang tidak semuanya dipakai. Solusinya: gunakan barrel files yang spesifik dan hindari barrel yang terlalu besar.

6. Dynamic Imports & Code Splitting

Dynamic import memungkinkan Anda memuat modul secara on-demand (saat dibutuhkan) menggunakan import() yang mengembalikan Promise. Ini adalah kunci untuk code splitting dan lazy loading β€” teknik yang sangat penting untuk performa aplikasi web besar.

Dasar Dynamic Import

JavaScript β€” Dynamic Import
// Static import β€” dimuat sebelum kode berjalan
import { formatRupiah } from './utils/format.js';

// Dynamic import β€” dimuat saat dibutuhkan
async function tampilkanHarga(harga) {
  const { formatRupiah } = await import('./utils/format.js');
  console.log(formatRupiah(harga));
}

// Dynamic import default export
async function muatKalkulator() {
  const Kalkulator = (await import('./Kalkulator.js')).default;
  const calc = new Kalkulator();
  calc.tambah(10).kali(2);
  return calc.getHasil();
}

// Dynamic import dengan conditional
async function muatLibrary() {
  if (navigator.onLine) {
    const { cloudSync } = await import('./cloudSync.js');
    await cloudSync();
  } else {
    const { offlineStorage } = await import('./offlineStorage.js');
    await offlineStorage();
  }
}

// Dynamic import dengan error handling
async function muatModul(path) {
  try {
    const modul = await import(path);
    return modul;
  } catch (error) {
    console.error(`Gagal memuat modul: ${path}`, error);
    // Fallback ke modul default
    return import('./fallback.js');
  }
}

Dynamic Import dengan React (Lazy Loading)

JSX β€” Lazy Loading Komponen
import { lazy, Suspense, useState } from 'react';

// Lazy load komponen berat β€” hanya dimuat saat di-render
const Dashboard = lazy(() => import('./pages/Dashboard.js'));
const Profil = lazy(() => import('./pages/Profil.js'));
const Pengaturan = lazy(() => import('./pages/Pengaturan.js'));

// Lazy load dengan timeout
const Laporan = lazy(() => {
  return Promise.all([
    import('./pages/Laporan.js'),
    new Promise(resolve => setTimeout(resolve, 300)) // minimum loading time
  ]).then(([module]) => module);
});

// Lazy load dengan named export
const ChartKomponen = lazy(() =>
  import('./components/Chart.js').then(module => ({
    default: module.LineChart
  }))
);

function App() {
  const [halaman, setHalaman] = useState('dashboard');

  const renderHalaman = () => {
    switch (halaman) {
      case 'dashboard':   return <Dashboard />;
      case 'profil':      return <Profil />;
      case 'pengaturan':  return <Pengaturan />;
      case 'laporan':     return <Laporan />;
      default:            return <Dashboard />;
    }
  };

  return (
    <div className="app">
      <nav>
        <button onClick={() => setHalaman('dashboard')}>Dashboard</button>
        <button onClick={() => setHalaman('profil')}>Profil</button>
        <button onClick={() => setHalaman('pengaturan')}>Pengaturan</button>
        <button onClick={() => setHalaman('laporan')}>Laporan</button>
      </nav>

      {/* Suspense menampilkan fallback saat komponen sedang dimuat */}
      <Suspense fallback={<div className="loading">Memuat halaman...</div>}>
        {renderHalaman()}
      </Suspense>
    </div>
  );
}

Praktik Dynamic Import Nyata

JavaScript β€” Praktik Nyata
// 1. Load library berat hanya saat dibutuhkan
async function eksporPDF(data) {
  // jsPDF cukup berat (~300KB), jadi lazy load saja
  const { jsPDF } = await import('jspdf');
  const doc = new jsPDF();

  doc.setFontSize(16);
  doc.text('Laporan Penjualan', 20, 20);

  data.forEach((item, i) => {
    doc.text(`${i + 1}. ${item.nama}: Rp ${item.harga}`, 20, 40 + (i * 10));
  });

  doc.save('laporan.pdf');
}

// 2. Load modul sesuai bahasa pengguna
async function muatTerjemahan(locale) {
  const terjemahan = await import(`./locales/${locale}.json`, {
    with: { type: 'json' }
  });
  return terjemahan.default;
}

// 3. Progressive enhancement
async function inisialisasiFitur() {
  // Load fitur dasar terlebih dahulu
  const { initBasicUI } = await import('./features/basic.js');
  initBasicUI();

  // Lalu muat fitur lanjutan secara background
  const { initAdvancedFeatures } = await import('./features/advanced.js');
  initAdvancedFeatures();
}

// 4. Module preloading (prefetch untuk performa)
function preloadModule(path) {
  const link = document.createElement('link');
  link.rel = 'modulepreload';
  link.href = path;
  document.head.appendChild(link);
}

// Preload modul yang mungkin dibutuhkan nanti
preloadModule('./features/chat.js');
preloadModule('./features/notifications.js');
Diagram: Code Splitting dengan Dynamic Import
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              BUNDLE OUTPUT                             β”‚
β”‚                                                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   Initial Load (kecil & cepat)     β”‚
β”‚  β”‚  main.js     β”‚   β€’ Entry point                     β”‚
β”‚  β”‚  (100 KB)    β”‚   β€’ Route logic                     β”‚
β”‚  β”‚              β”‚   β€’ Core utilities                   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                                      β”‚
β”‚         β”‚                                             β”‚
β”‚    β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”‚
β”‚    β–Ό         β–Ό          β–Ό          β–Ό                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚
β”‚  β”‚Dash- β”‚ β”‚Profilβ”‚ β”‚Settinβ”‚ β”‚ Laporan  β”‚  Lazy Load  β”‚
β”‚  β”‚board β”‚ β”‚      β”‚ β”‚gs    β”‚ β”‚ (+PDF)   β”‚  (on demand)β”‚
β”‚  β”‚(80KB)β”‚ β”‚(40KB)β”‚ β”‚(30KB)β”‚ β”‚ (200KB)  β”‚             β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚
β”‚                                                       β”‚
β”‚  User hanya memuat halaman yang dikunjungi!           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

7. Top-Level Await

Top-level await memungkinkan Anda menggunakan await di luar fungsi async, langsung pada level modul. Fitur ini tersedia di ES2022 dan sangat berguna untuk inisialisasi modul yang membutuhkan operasi asynchronous.

Penggunaan Top-Level Await

JavaScript β€” Top-Level Await
// ====== db.js β€” Inisialisasi database ======

// Top-level await β€” modul menunggu koneksi sebelum diekspor
const connection = await createDatabaseConnection({
  host: 'localhost',
  port: 5432,
  database: 'myapp',
});

export const db = connection;
export const isConnected = connection.status === 'connected';

// Modul yang mengimpor db.js akan menunggu sampai koneksi siap
// import { db, isConnected } from './db.js';
// console.log(isConnected); // true (pasti sudah terkoneksi)

// ====== config.js β€” Muat konfigurasi ======

// Muat config dari file JSON
const config = await fetch('/api/config').then(res => res.json());

// Muat terjemahan berdasarkan bahasa browser
const locale = navigator.language || 'id-ID';
const translations = await import(`./locales/${locale}.json`, {
  with: { type: 'json' }
});

export { config, translations };

// ====== wasm.js β€” Load WebAssembly ======

// Load WASM module secara asynchronous
const wasmModule = await WebAssembly.instantiateStreaming(
  fetch('/lib/image-processor.wasm')
);

export const processImage = wasmModule.instance.exports.processImage;
export const wasmReady = true;

// ====== crypto.js β€” Inisialisasi kriptografi ======

// Generate kunci kriptografi saat modul dimuat
const keyPair = await crypto.subtle.generateKey(
  { name: 'RSA-OAEP', modulusLength: 2048, hash: 'SHA-256' },
  true,
  ['encrypt', 'decrypt']
);

export const publicKey = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
export const privateKey = keyPair.privateKey; // Tidak diekspor dalam produksi!

Top-Level Await dengan Pattern Menarik

JavaScript β€” Advanced TLA Patterns
// ====== Pattern 1: Conditional module loading ======

let storage;

try {
  // Coba load IndexedDB wrapper
  storage = await import('./storage/indexeddb.js');
} catch {
  // Fallback ke localStorage
  storage = await import('./storage/localstorage.js');
}

export default storage;

// ====== Pattern 2: Race condition (ambil yang tercepat) ======

const fastestCDN = await Promise.race([
  fetch('https://cdn1.example.com/api').then(() => 'cdn1'),
  fetch('https://cdn2.example.com/api').then(() => 'cdn2'),
  fetch('https://cdn3.example.com/api').then(() => 'cdn3'),
]);

export const CDN_URL = `https://${fastestCDN}.example.com`;

// ====== Pattern 3: Parallel initialization ======

const [userData, appConfig, featureFlags] = await Promise.all([
  fetch('/api/user').then(r => r.json()),
  fetch('/api/config').then(r => r.json()),
  fetch('/api/features').then(r => r.json()),
]);

export { userData, appConfig, featureFlags };

// ====== Pattern 4: Retry logic ======

async function fetchWithRetry(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return response.json();
    } catch (err) {
      if (i === retries - 1) throw err;
      await new Promise(r => setTimeout(r, 1000 * (i + 1))); // exponential backoff
    }
  }
}

const remoteConfig = await fetchWithRetry('https://api.example.com/config');
export { remoteConfig };
⚠️ Top-Level Await Blocking

Top-level await memblokir eksekusi modul yang mengimpor modul tersebut. Jika modul A menggunakan top-level await dan modul B mengimpor modul A, maka modul B akan menunggu sampai modul A selesai. Ini bisa menyebabkan delay jika tidak dikelola dengan baik. Gunakan dengan bijak β€” idealnya untuk inisialisasi yang benar-benar kritis.

8. Tree Shaking & Dead Code Elimination

Tree shaking adalah teknik optimasi yang dilakukan oleh bundler (Vite, webpack, Rollup) untuk menghapus kode yang tidak digunakan dari bundle akhir. Istilah ini berasal dari konsep "menggoyang pohon" agar daun-daun yang mati (dead code) jatuh dan tersisa hanya yang hidup.

Bagaimana Tree Shaking Bekerja?

JavaScript β€” Tree Shaking Demo
// ====== utils.js β€” Semua fungsi tersedia ======

// βœ… Named exports β€” bisa di-tree shake
export function formatRupiah(angka) {
  return new Intl.NumberFormat('id-ID', {
    style: 'currency',
    currency: 'IDR'
  }).format(angka);
}

export function formatTanggal(date) {
  return new Intl.DateTimeFormat('id-ID', {
    dateStyle: 'long'
  }).format(new Date(date));
}

export function validateEmail(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

export function debounce(fn, ms) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  };
}

export function generateId() {
  return Date.now().toString(36) + Math.random().toString(36).substr(2);
}

// ====== app.js β€” Hanya gunakan beberapa fungsi ======
import { formatRupiah, validateEmail } from './utils.js';

// formatTanggal, debounce, dan generateId TIDAK diimpor
// β†’ bundler bisa menghapusnya dari bundle akhir!

console.log(formatRupiah(150000));          // "Rp 150.000"
console.log(validateEmail('test@mail.com')); // true

// Bundle akhir hanya berisi: formatRupiah dan validateEmail
// Ukuran berkurang signifikan!

Tips Agar Tree Shaking Efektif

JavaScript β€” Tree Shaking Best Practices
// ❌ BURUK: Import seluruh library
import _ from 'lodash';
_.debounce(fn, 300);
// Bundler tidak bisa tree shake lodash karena default export adalah objek monolitik

// βœ… BAIK: Import hanya fungsi yang dibutuhkan
import debounce from 'lodash-es/debounce';
debounce(fn, 300);
// Hanya debounce yang masuk ke bundle

// βœ… BAIK: Library yang mendukung tree shaking
import { debounce } from 'lodash-es'; // lodash-es mendukung ESM
import { format } from 'date-fns';    // date-fns mendukung tree shaking
import { motion } from 'framer-motion'; // framer-motion mendukung ESM

// ❌ BURUK: Side effects di dalam modul (menghambat tree shaking)
// utils-bad.js
export const PI = 3.14;
console.log('Modul dimuat!'); // Side effect! Bundler ragu untuk menghapus
window.globalConfig = {};     // Side effect global!

// βœ… BAIK: Pisahkan side effects
// utils-pure.js β€” tanpa side effects, aman di-tree shake
export const PI = 3.14;

// init.js β€” side effects terpisah
console.log('Modul dimuat!');
import './init.js'; // Explicitly imported untuk side effects

// package.json β€” tandai file yang memiliki side effects
// {
//   "sideEffects": false           ← semua file aman di-tree shake
//   "sideEffects": ["*.css"]       ← CSS punya side effects
//   "sideEffects": ["./src/init.js"] ← hanya init.js yang punya side effects
// }
Diagram: Tree Shaking Process
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚            TREE SHAKING PROCESS                       β”‚
β”‚                                                       β”‚
β”‚  SOURCE CODE (utils.js)          SOURCE (app.js)      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚ export formatRupiah  β”‚ ◄──import──  βœ… dipakai    β”‚
β”‚  β”‚ export formatTanggal β”‚         β”‚  ❌ tidak    β”‚    β”‚
β”‚  β”‚ export validateEmail  β”‚ ◄──import──  βœ… dipakai    β”‚
β”‚  β”‚ export debounce       β”‚         β”‚  ❌ tidak    β”‚    β”‚
β”‚  β”‚ export generateId     β”‚         β”‚  ❌ tidak    β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                                                       β”‚
β”‚  BUNDLER (Vite/webpack/Rollup)                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  1. Static analysis: deteksi export/import       β”‚  β”‚
β”‚  β”‚  2. Mark: export yang tidak dipakai = "unused"   β”‚  β”‚
β”‚  β”‚  3. Eliminate: hapus unused code                 β”‚  β”‚
β”‚  β”‚  4. Minify: kompresi kode tersisa                β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                                       β”‚
β”‚  BUNDLE OUTPUT                                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                              β”‚
β”‚  β”‚ βœ… formatRupiah     β”‚  Hanya yang dipakai          β”‚
β”‚  β”‚ βœ… validateEmail     β”‚  ~200 bytes (bukan 2KB)     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

9. Best Practices ES Modules

Berikut adalah panduan dan best practices untuk menggunakan ES Modules secara efektif dalam proyek JavaScript modern.

Organisasi File & Struktur Modul

File Structure β€” Best Practices
src/
β”œβ”€β”€ features/                    ← Berdasarkan fitur, bukan tipe file
β”‚   β”œβ”€β”€ auth/
β”‚   β”‚   β”œβ”€β”€ AuthContext.js        ← Context/state
β”‚   β”‚   β”œβ”€β”€ LoginForm.js          ← Komponen
β”‚   β”‚   β”œβ”€β”€ useAuth.js            ← Custom hook
β”‚   β”‚   β”œβ”€β”€ authApi.js            ← API calls
β”‚   β”‚   └── index.js              ← Barrel file
β”‚   β”œβ”€β”€ dashboard/
β”‚   β”‚   β”œβ”€β”€ DashboardPage.js
β”‚   β”‚   β”œβ”€β”€ DashboardCharts.js
β”‚   β”‚   └── index.js
β”‚   └── profile/
β”‚       β”œβ”€β”€ ProfilePage.js
β”‚       β”œβ”€β”€ ProfileForm.js
β”‚       └── index.js
β”œβ”€β”€ shared/                      ← Kode yang dipakai di banyak fitur
β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”œβ”€β”€ Button.js
β”‚   β”‚   β”œβ”€β”€ Modal.js
β”‚   β”‚   └── index.js
β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”œβ”€β”€ useLocalStorage.js
β”‚   β”‚   β”œβ”€β”€ useDebounce.js
β”‚   β”‚   └── index.js
β”‚   └── utils/
β”‚       β”œβ”€β”€ formatters.js
β”‚       β”œβ”€β”€ validators.js
β”‚       └── index.js
β”œβ”€β”€ app.js                       ← Entry point
└── routes.js                    ← Route definitions

Aturan Penting ES Modules

JavaScript β€” Aturan & Pola Umum
// ====== 1. SELALU gunakan file extension (.js) ======
// Beberapa bundler membutuhkan eksplicit extension
import { helper } from './utils/helper.js';  // βœ…
import { helper } from './utils/helper';      // ❌ bisa error di Deno/native ESM

// ====== 2. Hindari circular dependencies ======
// a.js mengimpor b.js, b.js mengimpor a.js β†’ masalah!
// Solusi: pindahkan shared code ke modul ketiga (c.js)

// ====== 3. Satu ekspor per file (Single Responsibility) ======
// βœ… Bagus: file fokus pada satu hal
// UserService.js β€” hanya tentang User Service
export class UserService {
  async getUser(id) { /* ... */ }
  async updateUser(id, data) { /* ... */ }
  async deleteUser(id) { /* ... */ }
}

// ❌ Kurang baik: file menangani banyak hal berbeda
// utils.js β€” terlalu banyak ekspor berbeda
export function formatRupiah() { /* ... */ }
export function validateEmail() { /* ... */ }
export function sendNotification() { /* ... */ }
export class DatabaseService { /* ... */ }
export const API_URL = '...';

// ====== 4. Gunakan constants terpisah ======
// constants/routes.js
export const ROUTES = {
  HOME: '/',
  LOGIN: '/login',
  DASHBOARD: '/dashboard',
  PROFILE: '/profile',
  SETTINGS: '/settings',
};

export const API_ENDPOINTS = {
  USERS: '/api/users',
  PRODUCTS: '/api/products',
  ORDERS: '/api/orders',
};

// ====== 5. Type exports (untuk TypeScript/JSDoc) ======
/** @typedef {{ id: number, nama: string, email: string }} User */

// Export type sebagai value (untuk JSDoc runtime checking)
export const UserType = Object.freeze({
  id: 'number',
  nama: 'string',
  email: 'string',
});

Anti-Pattern yang Harus Dihindari

JavaScript β€” Anti-Patterns
// ❌ 1. Mutable exports β€” bisa menyebabkan bug tak terduga
export let counter = 0;
export function increment() { counter++; }
// Importer mendapat binding langsung, bukan copy!

// βœ… Solusi: Gunakan getter pattern
let _counter = 0;
export function getCounter() { return _counter; }
export function increment() { _counter++; }

// ❌ 2. Import dengan side effects yang tidak disadari
import './analytics'; // Apa yang dilakukan modul ini?

// βœ… Solusi: Jelaskan mengapa Anda mengimpor side-effect module
// Load analytics untuk tracking page views
import './analytics';

// ❌ 3. Deep import dari internal package
import helper from 'some-package/lib/internal/utils/helper.js';

// βœ… Solusi: Gunakan public API
import { helper } from 'some-package';

// ❌ 4. Barrel file yang terlalu besar
// components/index.js β€” re-export 100+ komponen!
export { Button } from './Button';
export { Modal } from './Modal';
// ... 98 lagi

// βœ… Solusi: Sub-barrel files
// components/forms/index.js β€” hanya form components
// components/layout/index.js β€” hanya layout components

// ❌ 5. Mix CommonJS dan ESM secara acak
const fs = require('fs');         // CommonJS
import path from 'path';          // ESM
// Di Node.js, ini bisa menyebabkan error!

// βœ… Solusi: Pilih satu, konsisten
import fs from 'node:fs/promises'; // ESM sepanjang jalan
import path from 'node:path';

10. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang ES Modules:

Pertanyaan 1: Apa yang membedakan named export dari default export?

a) Named export bisa banyak per file, default export hanya satu per file
b) Default export lebih cepat daripada named export
c) Named export hanya untuk fungsi, default export hanya untuk class
d) Tidak ada perbedaan, hanya sintaks berbeda

Pertanyaan 2: Apa keuntungan utama dynamic import (import())?

a) Kode berjalan lebih cepat
b) Memungkinkan code splitting dan lazy loading modul
c) Menghilangkan kebutuhan akan bundler
d) Menggantikan static import sepenuhnya

Pertanyaan 3: Apa itu tree shaking dalam konteks ES Modules?

a) Proses menghapus komentar dari kode
b) Proses mengompresi file menjadi format binary
c) Teknik menghapus kode yang tidak digunakan dari bundle
d) Teknik untuk mengacak kode agar tidak bisa dibaca

Pertanyaan 4: Apa yang terjadi saat top-level await digunakan dalam sebuah modul?

a) Modul akan error karena await hanya bisa di dalam async function
b) Modul menunggu operasi selesai dan memblokir modul pengimpor
c) Modul langsung diekspor tanpa menunggu
d) Browser akan me-refresh halaman otomatis

Pertanyaan 5: Bagaimana cara mengimpor semua named exports dari modul sebagai satu objek?

a) import all from './module.js'
b) import { * } from './module.js'
c) import * as nama from './module.js'
d) import everything './module.js'
πŸ” Zoom
100%
🎨 Tema