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 |
|---|---|
| Modularitas | Kode terorganisir dalam file terpisah, mudah dikelola dan dipelihara |
| Encapsulation | Setiap modul memiliki scope sendiri, variabel tidak bocor ke global |
| Tree Shaking | Bundler bisa menghapus kode yang tidak digunakan untuk mengurangi ukuran bundle |
| Static Analysis | Struktur import/export bisa dianalisis saat compile time |
| Browser Native | Didukung langsung oleh browser modern tanpa perlu bundler |
| Async Loading | Dynamic import memungkinkan lazy loading modul secara asynchronous |
ES Modules vs CommonJS vs AMD
| Aspek | ES Modules | CommonJS | AMD |
|---|---|---|---|
| Sintaks | import/export | require/module.exports | define/require |
| Loading | Static & Async | Synchronous | Asynchronous |
| Environment | Browser & Node.js | Node.js (server) | Browser |
| Tree Shaking | β Ya | β Tidak | β Tidak |
| Top-level Await | β Ya | β Tidak | β Tidak |
| Strict Mode | Otomatis | Manual | Manual |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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:
<!-- 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 -->
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
// ====== 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
// 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
// ====== 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
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
// ====== 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
// 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';
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
// ====== 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
// 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
// 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
// ====== 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
// 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';
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
// 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)
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
// 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');
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 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
// ====== 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
// ====== 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 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?
// ====== 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
// β 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
// }
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 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
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
// ====== 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
// β 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: