Kenapa ini penting
Bayangkan kamu pindahan rumah. Kamu bisa saja membawa barang satu per satu dengan motor, bolak-balik ratusan kali. Atau kamu bisa mengemasnya ke dalam kardus berlabel, membuang barang yang tidak terpakai, membungkus yang rapuh, lalu mengangkut semuanya dengan satu truk. Bundler adalah jasa pindahan untuk kodemu: ia mengumpulkan puluhan sampai ratusan file JavaScript dan CSS, membuang yang tidak terpakai, memadatkannya, memberi label versi, lalu menyiapkannya untuk dikirim ke browser pengguna seefisien mungkin.
Selama Fase 2 kamu mungkin menulis <script src="app.js"> langsung di HTML. Itu cukup untuk proyek kecil. Begitu proyek tumbuh, masalah muncul: urutan <script> harus tepat, paket dari npm (misalnya pustaka tanggal) tidak bisa langsung di-import dengan namanya, setiap file berarti satu permintaan jaringan, kode tidak diminifikasi, dan browser terus memakai versi cache lama setelah kamu deploy. Build tool seperti Vite menyelesaikan semua itu, ditambah dev server yang langsung memperbarui halaman saat kamu menyimpan file. Vite juga fondasi yang akan kamu pakai di Fase 3 untuk React, dan menjadi dasar Vitest yang sudah kamu pakai di modul Testing.
Konsep inti
ES Modules: bahasa yang dipahami bundler
ES Modules (ESM) adalah sistem modul standar JavaScript: export untuk mengekspor, import untuk mengimpor. Browser modern mendukungnya langsung lewat <script type="module">, yang juga otomatis berperilaku defer.
// export bernama (bisa banyak per file)
export function formatRupiah(angka) { /* ... */ }
// export default (satu per file)
export default function setupForm() { /* ... */ }
// import
import setupForm from './form.js'; // default
import { formatRupiah } from './utils.js'; // bernama
import * as utils from './utils.js'; // semua sebagai object
const { grafik } = await import('./grafik.js'); // dinamis: dimuat saat dibutuhkan
Tanpa bundler, browser butuh path lengkap ('./utils.js'). Dengan bundler, kamu bisa menulis import dayjs from 'dayjs' (bare import) dan bundler mencarinya di node_modules. Karena ESM bersifat statis (bisa dianalisis tanpa dijalankan), bundler bisa melakukan tree-shaking: membuang export yang tidak pernah di-import.
Apa saja yang dilakukan bundler saat build?
| Langkah | Apa yang terjadi | Manfaat |
|---|---|---|
| Resolve & bundle | Mengikuti semua import, menggabungkan modul jadi sedikit file |
Lebih sedikit permintaan jaringan |
| Tree-shaking | Membuang kode yang tidak dipakai | File lebih kecil |
| Transform | Mengubah sintaks sesuai target browser, memproses CSS/SCSS | Kompatibilitas |
| Minify | Menghapus spasi, komentar, memendekkan nama variabel | File jauh lebih kecil |
| Code splitting | import() dinamis menjadi file terpisah yang dimuat belakangan |
Halaman awal lebih ringan (bagus untuk LCP & INP) |
| Hashing | Nama file berisi hash isi: index-DTMPt3vS.js |
Cache setahun aman (modul Performa) |
| Aset | Gambar kecil di bawah 4 KiB di-inline sebagai base64, sisanya diberi hash | Lebih sedikit permintaan |
Vite: dev cepat, build optimal
Vite (dibaca "vit", bahasa Prancis untuk "cepat") versi 8.3 membutuhkan Node.js 20.19+ atau 22.12+. Ia bekerja dalam dua mode:
- Dev (
npm run dev): server lokal dihttp://localhost:5173. Vite tidak membundel semuanya di awal. Ia menyajikan file sebagai ES modules asli dan hanya memproses file yang diminta browser, sehingga server menyala dalam hitungan milidetik. HMR (Hot Module Replacement) memperbarui bagian yang berubah tanpa me-reload seluruh halaman. - Build (
npm run build): membuat versi produksi di folderdist/. Vite 8 memakai Rolldown sebagai bundler produksi. Target bawaannya adalah browser yang masuk Baseline Widely Available (Chrome/Edge 111, Firefox 114, Safari 16.4). Untuk browser lebih lama tersedia@vitejs/plugin-legacy. - Preview (
npm run preview): menyajikan isidist/secara lokal dihttp://localhost:4173, untuk mengecek hasil build sebelum deploy. Ini bukan server produksi.
Variabel environment
Vite membaca file .env, .env.development, .env.production di root proyek. Hanya variabel berawalan VITE_ yang diekspos ke kode klien lewat import.meta.env. Vite juga menyediakan import.meta.env.MODE ('development'/'production'), DEV, dan PROD (boolean). Saat build, Vite mengganti ekspresi tersebut dengan nilai literal. Kami membuktikannya: console.log(import.meta.env.VITE_API_URL) di hasil build menjadi console.log(`https://api.contoh.id`), sementara variabel tanpa awalan VITE_ menjadi void 0 (undefined). Karena nilainya tertanam di file publik, VITE_ hanya untuk konfigurasi yang boleh dilihat publik (lihat modul Keamanan). Selalu masukkan .env ke .gitignore, dan sediakan .env.example tanpa nilai rahasia.
PostCSS dan Autoprefixer
PostCSS adalah alat untuk mengubah CSS lewat plugin, mirip bundler untuk CSS. Plugin paling terkenal adalah Autoprefixer, yang menambahkan vendor prefix (-webkit-, -moz-) berdasarkan daftar browser target dari Browserslist (file .browserslistrc atau kunci browserslist di package.json). Vite otomatis membaca postcss.config.js kalau file itu ada.
Satu hal menarik dari pengujian kami di Vite 8: Autoprefixer menambahkan -webkit-user-select, tetapi minifier CSS bawaan Vite (Lightning CSS) menyesuaikan prefix dengan build.target/build.cssTarget, lalu membuang prefix yang dianggap tidak dibutuhkan target bawaan. Saat build dijalankan dengan --target safari14, prefix itu tetap ada. Pelajarannya: samakan target browser di Browserslist dan di konfigurasi build, lalu periksa file hasil build, jangan berasumsi.
Contoh kode
Membuat proyek dan melihat strukturnya:
npm create vite@latest newsletter -- --template vanilla
cd newsletter
npm install
npm run dev
# → VITE v8.3.5 ready in 185 ms
# → ➜ Local: http://localhost:5173/
newsletter/
├── index.html ← titik masuk (bukan di public/, beda dengan alat lama)
├── package.json ← "type": "module" + script dev/build/preview
├── public/ ← file disalin apa adanya (favicon, robots.txt)
└── src/
├── main.js ← di-import dari index.html lewat <script type="module">
└── style.css ← bisa di-import dari JS: import './style.css'
Kode aplikasi yang memakai import CSS, environment, dan code splitting:
// src/main.js
import './style.css'; // Vite memproses & menyuntikkan CSS
import { setupForm } from './form.js';
setupForm(document.querySelector('#app'));
console.log('API:', import.meta.env.VITE_API_URL);
console.log('Rahasia:', import.meta.env.RAHASIA_DB);
console.log('Mode:', import.meta.env.MODE, 'DEV?', import.meta.env.DEV);
// Code splitting: modul berat hanya diunduh saat tombol diklik
document.querySelector('#lihat-statistik')?.addEventListener('click', async () => {
const { tampilkanGrafik } = await import('./grafik.js'); // jadi file terpisah
tampilkanGrafik();
});
# .env (jangan di-commit!)
VITE_API_URL=https://api.contoh.id
RAHASIA_DB=jangan-bocor
Build dan periksa hasilnya:
npm run build
# → vite v8.3.5 building client environment for production...
# → ✓ 7 modules transformed.
# → dist/index.html 0.60 kB │ gzip: 0.35 kB
# → dist/assets/index-CzUM7UuI.css 0.64 kB │ gzip: 0.39 kB
# → dist/assets/index-DTMPt3vS.js 1.85 kB │ gzip: 0.93 kB
# → ✓ built in 592ms
# Buktikan apa yang tertanam di bundle publik:
grep -o 'console.log([^)]*)' dist/assets/*.js
# → console.log(`API:`,`https://api.contoh.id`) ← VITE_ ikut terbawa!
# → console.log(`Rahasia:`,void 0) ← tanpa VITE_: tidak terekspos
# → console.log(`Mode:`,`production`,`DEV?`,!1)
npm run preview
# → ➜ Local: http://localhost:4173/
Perhatikan dist/index.html hasil build: Vite otomatis mengganti <script src="/src/main.js"> dengan file ber-hash dan menambahkan <link rel="stylesheet">:
<script type="module" crossorigin src="/assets/index-DTMPt3vS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CzUM7UuI.css">
Konfigurasi PostCSS + Autoprefixer, dan konfigurasi Vite gabungan (build + test):
npm install -D postcss autoprefixer
printf 'last 2 versions\n> 0.5%%\nnot dead\n' > .browserslistrc
// postcss.config.js, otomatis dibaca Vite
import autoprefixer from 'autoprefixer';
export default {
plugins: [autoprefixer()],
};
// vite.config.js
import { defineConfig } from 'vitest/config';
export default defineConfig({
build: {
// Samakan dengan target browser-mu. Bawaan: 'baseline-widely-available'
// cssTarget: ['safari14'],
sourcemap: true, // file .map untuk debugging error produksi
},
test: {
environment: 'jsdom',
include: ['src/**/*.test.js'],
setupFiles: ['./vitest.setup.js'],
},
});
# Bandingkan hasil CSS untuk target berbeda
npx vite build --minify false && grep -o "[-a-z]*user-select[^;}]*" dist/assets/*.css
# → -webkit-user-select: none
# → -moz-user-select: none
# → user-select: none
npx vite build && grep -o "[-a-z]*user-select[^;}]*" dist/assets/*.css
# → user-select:none (Lightning CSS membuang prefix untuk target bawaan)
npx vite build --target safari14 && grep -o "[-a-z]*user-select[^;}]*" dist/assets/*.css
# → -webkit-user-select:none
# → user-select:none
Akhirnya, package.json proyek lengkap dari seluruh materi ini:
{
"name": "newsletter",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"e2e": "playwright test",
"lint": "eslint . && stylelint \"src/**/*.css\"",
"format": "prettier . --write",
"prepare": "husky"
}
}
Kesalahan umum
- Membuka
index.htmllangsung dengan klik dua kali (file://). Module script dan import tidak jalan darifile://. Selalu pakainpm run devataunpm run preview. - Men-deploy folder proyek, bukan
dist/. Browser lalu meminta/src/main.jsmentah dan bare import gagal. Yang di-deploy adalah isidist/hasilnpm run build. - Menaruh rahasia di
VITE_.... Nilainya tertanam di bundle publik. Rahasia hanya di server. - Menaruh file yang di-import di
public/. Isipublic/tidak diproses (tidak di-hash, tidak diminifikasi). Gambar dan CSS yang di-import dari kode taruh disrc/.public/untuk file yang butuh nama tetap sepertirobots.txtdanfavicon.svg. - Mengira
vite previewadalah server produksi. Itu hanya untuk mengecek hasil build secara lokal. Di produksi, pakai hosting statis (Netlify, Vercel, Cloudflare Pages) atau server seperti contoh di modul Keamanan.
Latihan
- Mudah, Jelaskan kenapa nama file hasil build seperti
index-DTMPt3vS.jsberubah setiap kali isinya berubah, dan apa hubungannya dengan header cache. - Sedang, Buat file
.env.developmentberisiVITE_API_URL=http://localhost:3000dan.env.productionberisiVITE_API_URL=https://api.contoh.id. Tulis kode yang memakai URL itu untukfetch('/produk'), lalu jelaskan nilai mana yang dipakai saatnpm run devdan saatnpm run build. - Menantang, Buat modul
src/grafik.jsyang mengeksportampilkanGrafik(), muat denganimport()dinamis saat tombol diklik, lalu buktikan lewat outputnpm run buildbahwa modul itu menjadi file terpisah.
Lihat solusi
1. Hash dihitung dari isi file. Isi berubah, hash berubah, nama file berubah. Karena itu file di /assets/ aman diberi Cache-Control: public, max-age=31536000, immutable: browser tidak akan memakai versi lama, karena index.html baru (yang no-cache) menunjuk ke nama file baru.
2.
// src/api.js
const BASE = import.meta.env.VITE_API_URL;
export async function ambilProduk() {
const res = await fetch(`${BASE}/produk`);
if (!res.ok) throw new Error(`Gagal memuat produk: ${res.status}`);
return res.json();
}
// npm run dev → mode development → http://localhost:3000/produk
// npm run build → mode production → https://api.contoh.id/produk (tertanam di bundle)
3.
// src/grafik.js
export function tampilkanGrafik() {
const el = document.createElement('p');
el.textContent = 'Grafik dimuat!';
document.querySelector('#app').append(el);
}
// src/main.js (tambahan)
const tombol = document.createElement('button');
tombol.type = 'button';
tombol.textContent = 'Lihat statistik';
tombol.addEventListener('click', async () => {
const { tampilkanGrafik } = await import('./grafik.js');
tampilkanGrafik();
});
document.querySelector('#app').append(tombol);
npm run build
# → dist/assets/grafik-BDfR925C.js 0.14 kB ← file terpisah (chunk)
# → dist/assets/index-Cbslhm8Y.js …
Di tab Network DevTools, grafik-*.js baru diunduh saat tombol diklik.
Ringkasan
- Bundler mengikuti
import, menggabungkan, tree-shaking, minify, code splitting, dan memberi hash pada nama file. - ES Modules (
import/export,import()dinamis) adalah dasar kerja bundler modern. - Vite 8.3:
devdi port 5173 dengan HMR,buildkedist/memakai Rolldown,previewdi port 4173 untuk mengecek hasil build. - Hanya variabel
VITE_yang masuk ke kode klien, dan nilainya tertanam di bundle publik. - PostCSS + Autoprefixer menambah vendor prefix berdasarkan Browserslist. Samakan dengan target build dan periksa hasil
dist/. - Deploy isi
dist/, bukan foldersrc/.