Svelte #
Svelte adalah compiler, bukan framework runtime. Kode Svelte dikompilasi menjadi JavaScript vanilla yang sangat kecil dan cepat, tanpa virtual DOM. Svelte 5 membawa runes — system reactivity yang lebih eksplisit dan powerful. SvelteKit, meta-framework resmi, menambahkan routing, SSR/SSG, dan adapter untuk berbagai platform.
Untuk local development, SvelteKit + Vite memberikan HMR yang sangat cepat. Docker memastikan environment konsisten, dan output build (static atau Node) bisa di-serve dengan nginx atau Node server.
Artikel ini membahas setup Svelte 5 dan SvelteKit dengan Docker Compose, termasuk runes, stores, routing, dan strategi deployment.
Prasyarat #
Pastikan sudah terinstall:
- Docker dan Docker Compose versi terbaru
- Node.js 20+ (opsional)
Project SvelteKit (bootstrap dengan npx sv create):
my-svelte-app/
├── src/
│ ├── app.html
│ ├── app.d.ts
│ ├── lib/
│ │ ├── components/
│ │ ├── stores/
│ │ └── utils/
│ └── routes/
│ ├── +page.svelte
│ ├── +layout.svelte
│ └── api/
├── static/
├── package.json
├── svelte.config.js
├── vite.config.ts
├── Dockerfile
└── docker-compose.yml
Mengapa Svelte Berbeda #
flowchart LR
A[Source .svelte] --> B[Svelte Compiler]
B --> C[Vanilla JS]
C --> D[Browser]
style A fill:#ff3e00,color:#fff
style B fill:#ff3e00,color:#fff
style C fill:#40b3ff,color:#fff
style D fill:#40b3ff,color:#fff
Svelte mengompilasi komponen menjadi JavaScript imperatif yang langsung memanipulasi DOM. Hasilnya:
| Aspek | Svelte | React/Vue |
|---|---|---|
| Runtime | ~5KB (runes) | 40-130KB |
| Bundle size | Sangat kecil | Lebih besar |
| Reactivity | Compiler-level | Virtual DOM / Proxy |
| Learning curve | Landai | Lebih tinggi |
Dockerfile Multi-Stage #
SvelteKit build dengan adapter menghasilkan output berbeda tergantung target:
- adapter-static — HTML statis untuk nginx
- adapter-node — Node server untuk SSR/ISR
- adapter-auto — auto-detect platform (Vercel, Netlify, dll)
# syntax=docker/dockerfile:1.6
# ---- Stage 1: Install dependencies ----
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
# ---- Stage 2: Build SvelteKit ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NODE_ENV=production
RUN npm run build
# ---- Stage 3: Production dengan adapter-node ----
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=3000
RUN addgroup --system --gid 1001 svelte
RUN adduser --system --uid 1001 svelte
# Copy output SvelteKit
COPY --from=builder --chown=svelte:svelte /app/build ./build
COPY --from=builder --chown=svelte:svelte /app/package.json ./package.json
# Install hanya production dependencies
RUN npm ci --omit=dev && npm cache clean --force
USER svelte
EXPOSE 3000
CMD ["node", "build"]
svelte.config.js untuk adapter-node:
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
out: 'build',
precompress: false,
envPrefix: ''
}),
},
};
export default config;
Static Export dengan adapter-static #
Untuk blog atau landing page static:
// svelte.config.js
import adapter from '@sveltejs/adapter-static';
export default {
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: 'index.html', // SPA mode
precompress: false,
strict: true
}),
},
};
# Stage terakhir untuk static
FROM nginx:1.27-alpine
COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx.conf untuk SvelteKit static:
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Cache SvelteKit assets
location /_app/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA fallback untuk client-side routing
location / {
try_files $uri $uri/ /index.html;
}
}
docker-compose.yml untuk Development #
# docker-compose.yml
services:
web:
build:
context: .
dockerfile: Dockerfile.dev
image: my-svelte-app:dev
container_name: svelte-dev
command: npm run dev -- --host 0.0.0.0
ports:
- "5173:5173"
volumes:
- ./:/app
- /app/node_modules
- /app/.svelte-kit
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://app:dev@db:5432/myapp
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=dev
- POSTGRES_DB=myapp
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:
Dockerfile.dev:
FROM node:20-alpine
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev"]
Svelte 5 Runes #
Svelte 5 membawa runes — system reactivity yang eksplisit. Berbeda dengan reactivity otomatis Svelte 4 yang bergantung pada let di top-level.
$state untuk Reactive Variable
#
<!-- src/lib/components/Counter.svelte -->
<script lang="ts">
let count = $state(0);
let name = $state('Pengguna');
function increment() {
count++;
}
</script>
<div>
<p>Halo, {name}!</p>
<p>Count: {count}</p>
<input bind:value={name} />
<button onclick={increment}>Tambah</button>
</div>
$derived untuk Computed Value
#
<script lang="ts">
let count = $state(0);
let doubleCount = $derived(count * 2);
let isEven = $derived(count % 2 === 0);
</script>
<p>Count: {count} (double: {doubleCount})</p>
<p>Status: {isEven ? 'Genap' : 'Ganjil'}</p>
$effect untuk Side Effect
#
<script lang="ts">
import { onMount } from 'svelte';
let count = $state(0);
// Effect: jalan otomatis saat count berubah
$effect(() => {
console.log(`Count berubah ke: ${count}`);
document.title = `Count: ${count}`;
});
// Cleanup function
$effect(() => {
const interval = setInterval(() => {
count++;
}, 1000);
return () => clearInterval(interval);
});
</script>
<p>Count: {count}</p>
Kapan pakai$effectvsonMount? Pakai$effectketika kamu butuh re-run saat state berubah. PakaionMountketika logikanya hanya perlu jalan sekali saat komponen mount.
$props untuk Component Props
#
<!-- src/lib/components/UserCard.svelte -->
<script lang="ts">
interface User {
id: number;
name: string;
email: string;
}
interface Props {
user: User;
showEmail?: boolean;
onDelete?: (id: number) => void;
}
let { user, showEmail = true, onDelete }: Props = $props();
function handleDelete() {
onDelete?.(user.id);
}
</script>
<div class="user-card">
<h3>{user.name}</h3>
{#if showEmail}
<p>{user.email}</p>
{/if}
{#if onDelete}
<button onclick={handleDelete}>Hapus</button>
{/if}
</div>
<style>
.user-card {
padding: 1rem;
border: 1px solid #ccc;
}
</style>
Pakai:
<script lang="ts">
import UserCard from '$lib/components/UserCard.svelte';
let users = $state([
{ id: 1, name: 'Andi', email: '[email protected]' },
{ id: 2, name: 'Budi', email: '[email protected]' },
]);
function handleDelete(id: number) {
users = users.filter(u => u.id !== id);
}
</script>
{#each users as user (user.id)}
<UserCard {user} onDelete={handleDelete} />
{/each}
Stores untuk State Global #
SvelteKit menyediakan writable store dari svelte/store untuk state global.
// src/lib/stores/cart.ts
import { writable, derived, get } from 'svelte/store';
import { browser } from '$app/environment';
interface CartItem {
id: number;
name: string;
price: number;
qty: number;
}
function createCartStore() {
// Persist ke localStorage di browser
const initial: CartItem[] = browser
? JSON.parse(localStorage.getItem('cart') ?? '[]')
: [];
const { subscribe, set, update } = writable<CartItem[]>(initial);
// Auto-save ke localStorage
if (browser) {
subscribe((items) => {
localStorage.setItem('cart', JSON.stringify(items));
});
}
return {
subscribe,
add: (item: Omit<CartItem, 'qty'>) => {
update((items) => {
const existing = items.find(i => i.id === item.id);
if (existing) {
return items.map(i =>
i.id === item.id ? { ...i, qty: i.qty + 1 } : i
);
}
return [...items, { ...item, qty: 1 }];
});
},
remove: (id: number) => {
update((items) => items.filter(i => i.id !== id));
},
clear: () => set([]),
};
}
export const cart = createCartStore();
// Derived store
export const cartTotal = derived(cart, ($cart) =>
$cart.reduce((sum, i) => sum + i.price * i.qty, 0)
);
export const cartCount = derived(cart, ($cart) =>
$cart.reduce((sum, i) => sum + i.qty, 0)
);
Pakai di komponen dengan auto-subscription $:
<script lang="ts">
import { cart, cartTotal, cartCount } from '$lib/stores/cart';
</script>
<div>
<p>Items: {$cartCount} — Total: Rp {$cartTotal.toLocaleString('id-ID')}</p>
<button onclick={() => cart.add({ id: 1, name: 'Produk', price: 50000 })}>
Tambah
</button>
<button onclick={() => cart.clear()}>Kosongkan</button>
</div>
Prefix$pada store mengaktifkan auto-subscription. Tanpa$, kamu harussubscribemanual. Ini syntactic sugar yang powerful dari Svelte.
Routing dengan SvelteKit #
SvelteKit pakai file-based routing di src/routes/:
src/routes/
├── +page.svelte # /
├── +layout.svelte # Root layout
├── about/
│ └── +page.svelte # /about
├── products/
│ ├── +page.svelte # /products
│ ├── +page.ts # Load function
│ └── [id]/
│ ├── +page.svelte # /products/:id
│ └── +page.ts # Dynamic load
├── api/
│ └── users/
│ └── +server.ts # /api/users endpoint
└── +error.svelte # Error page
Load function (server + client):
// src/routes/products/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch }) => {
const res = await fetch('/api/products');
const products = await res.json();
return { products };
};
<!-- src/routes/products/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<h1>Produk</h1>
<ul>
{#each data.products as product (product.id)}
<li>
<a href="/products/{product.id}">{product.name}</a>
<span>Rp {product.price.toLocaleString('id-ID')}</span>
</li>
{/each}
</ul>
Server-only load:
// src/routes/products/[id]/+page.server.ts
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/db';
export const load: PageServerLoad = async ({ params }) => {
const product = await db.product.findUnique({
where: { id: Number(params.id) },
});
if (!product) {
throw error(404, 'Produk tidak ditemukan');
}
return { product };
};
Form actions (server-side mutation):
// src/routes/products/new/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { db } from '$lib/server/db';
export const actions: Actions = {
default: async ({ request }) => {
const formData = await request.formData();
const name = formData.get('name') as string;
const price = Number(formData.get('price'));
if (!name || isNaN(price)) {
return fail(400, { error: 'Nama dan harga wajib diisi' });
}
await db.product.create({ data: { name, price } });
throw redirect(303, '/products');
},
};
<!-- src/routes/products/new/+page.svelte -->
<script lang="ts">
import type { ActionData } from './$types';
let { form }: { form: ActionData } = $props();
</script>
<h1>Produk Baru</h1>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<form method="POST">
<input name="name" placeholder="Nama produk" required />
<input name="price" type="number" placeholder="Harga" required />
<button type="submit">Simpan</button>
</form>
API endpoint:
// src/routes/api/users/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { db } from '$lib/server/db';
export const GET: RequestHandler = async () => {
const users = await db.user.findMany();
return json(users);
};
export const POST: RequestHandler = async ({ request }) => {
const body = await request.json();
const user = await db.user.create({ data: body });
return json(user, { status: 201 });
};
Hot Reload dengan Vite #
SvelteKit + Vite HMR bekerja di semua level:
- Perubahan
.svelte— HMR instan, state dipertahankan - Perubahan
+page.ts/+page.server.ts— Reload halaman - Perubahan
+layout.svelte— Reload semua child - Perubahan
+server.ts— API endpoint di-restart
Untuk Docker, pastikan bind mount source code dan anonymous volume untuk .svelte-kit:
volumes:
- ./:/app
- /app/node_modules
- /app/.svelte-kit
Build dan Run #
# Development
docker compose up --build
# Akses: http://localhost:5173
# Production build (adapter-node)
docker build -t my-svelte-app:prod -f Dockerfile .
docker run -p 3000:3000 my-svelte-app:prod
# Production build (adapter-static)
docker build -t my-svelte-app:static -f Dockerfile.static .
docker run -p 8080:80 my-svelte-app:static
# Stop
docker compose down
Best Practice #
1. Pilih Adapter yang Tepat #
| Adapter | Pakai untuk |
|---|---|
adapter-static |
Blog, landing page, dokumentasi |
adapter-node |
SSR, ISR, dynamic content |
adapter-auto |
Deploy ke Vercel/Netlify/Cloudflare |
2. Pakai Runes untuk Kode Baru #
Svelte 5 runes lebih eksplisit dan powerful. Untuk project baru, langsung pakai $state, $derived, $effect, $props. Untuk project lama, migrasi bertahap.
3. Pisahkan Server vs Universal Code #
Kode dengan import ke Node-only modules (seperti pg, fs) harus di +page.server.ts atau $lib/server/. Kode di +page.ts jalan di server DAN client.
// src/lib/server/db.ts — server only
import { Pool } from 'pg';
export const pool = new Pool({ ... });
<script lang="ts">
// ❌ JANGAN import server module di +page.svelte
// import { pool } from '$lib/server/db';
// ✅ Pakai load function
let { data } = $props();
</script>
4. Vite host: '0.0.0.0'
#
Wajib di vite.config.ts atau flag --host 0.0.0.0. Tanpa ini, dev server tidak bisa diakses dari host.
5. Anonymous Volume untuk .svelte-kit
#
volumes:
- /app/.svelte-kit
Tanpa ini, type generation dan cache build konflik dengan host.
6. Form Actions untuk Mutation #
Untuk form submission, pakai form actions di +page.server.ts daripada fetch API manual. Lebih simpel, otomatis handle CSRF, dan progressive enhancement ready.
7. Stores untuk State Lintas Halaman #
Cart, user auth, theme preferences — pakai store di $lib/stores/. Auto-subscribe dengan prefix $ di template.
8. $effect untuk Cleanup
#
Selalu return cleanup function dari $effect untuk prevent memory leak:
$effect(() => {
const handler = () => console.log('resize');
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
});
Troubleshooting #
HMR Tidak Bekerja #
Pastikan host: '0.0.0.0', bind mount source code, dan anonymous volume .svelte-kit.
“Cannot find module ‘$app/environment’” #
Install @sveltejs/kit dan pastikan svelte.config.js valid. Restart container.
Build Output Kecil tapi Production Error #
Cek +page.server.ts — mungkin ada kode server yang tidak sengaja di-bundle. Pastikan server-only module di-import dari $lib/server/.
Adapter-node Port Conflict #
lsof -i :3000
Ubah PORT environment variable atau port mapping Compose.
Svelte 5 Component Tidak Reactive #
Pakai runes ($state, $derived) bukan let biasa. Di Svelte 5, let tidak otomatis reactive.
Ringkasan #
- Svelte ideal untuk SPA (adapter-static) atau SSR (adapter-node). Bundle kecil, performa tinggi.
- Dockerfile multi-stage berbeda per adapter: adapter-node pakai
node build(Node server), adapter-static pakai nginx.- Svelte 5 runes (
$state,$derived,$effect,$props) adalah reactivity modern — lebih eksplisit dariletdi Svelte 4.- Stores dengan
writable+deriveddi$lib/stores/. Auto-subscribe dengan prefix$di template.- SvelteKit routing dengan file-based routing di
src/routes/.+page.svelteuntuk UI,+page.ts/+page.server.tsuntuk data loading.- Form actions di
+page.server.tsuntuk mutation. Auto-handle CSRF, progressive enhancement, no JS required.- API endpoints dengan
+server.tsdan HTTP method exports (GET,POST, dll).- Adapter-node untuk SSR, adapter-static untuk SPA, adapter-auto untuk platform khusus.
- Anonymous volume untuk
node_modulesdan.svelte-kitmencegah konflik dengan host.- Server-only code di
$lib/server/atau+page.server.ts— tidak boleh di-bundle ke client.- Hot reload Vite bekerja untuk
.svelte,+page.ts,+layout.svelte. State dipertahankan untuk komponen.- Pakai $effect dengan cleanup untuk side effect yang perlu unsubscribe (event listener, interval).
- Dockerfile dev sederhana (Vite + npm), Dockerfile prod multi-stage (Node build + nginx/node runner).
- Troubleshooting: HMR mati (cek host + volume), module not found (restart container), reactivity error (pakai runes).
- Bundle size Svelte lebih kecil dari React/Vue karena compiler-level optimization.
- TypeScript dengan
lang="ts"di script tag. Type inference untuk props, state, dan load functions.