Web Dev 14 Sep 2026 10 views 0 komentar

Membuat Progressive Web App PWA dengan Vanilla JavaScript - Panduan Lengkap Pemula

Membuat Progressive Web App PWA dengan Vanilla JavaScript - Panduan Lengkap Pemula

Pernah mau bikin aplikasi yang bisa di-install di HP tanpa perlu Play Store? Atau pengen bikin web yang tetap jalan walau offline? Itu yang saya alami bulan lalu. Saya bikin web app sederhana buat catat keuangan, tapi pas mode jalan di HP, langsung blank page. Frustrating banget. Terus saya inget kata teman: "Bikin PWA aja, gan."

PWA atau Progressive Web App itu konsep web yang nggak cuma jalan di browser, tapi bisa install di homescreen, jalan offline, dan kirim push notification. Kayak native app, tapi dibikin dengan HTML, CSS, JS. Gak perlu Swift, gak perlu Kotlin. Cuma butuh vanilla JavaScript yang kamu sudah tau.

Dalam tutorial ini, saya mau ajak kamu bikin PWA dari nol. Bukan pake framework apa-apa. Cuma HTML, CSS, JavaScript murni. Kita bikin app "Notes" sederhana yang bisa di-install di HP dan jalan offline. Siap? Yuk.

Apa Itu PWA dan Kenapa Kamu Harus Peduli?

PWA bukan framework atau library. Itu pertama yang harus jelas. PWA itu kombinasi tiga teknologi web yang sudah ada: Service Worker, Web App Manifest, dan HTTPS. Kalau kamu punya ketiganya, web kamu sudah bisa disebut PWA.

Keuntungannya apa? Coba lihat ini:

  • Install di homescreen - User bisa "install" web app tanpa App Store. Tinggal klik "Add to Home Screen" di browser, muncul icon di homescreen, langsung kebuka kayak native app
  • Offline support - Service worker cache halaman dan asset, jadi pas gak ada internet, app tetap jalan. Bikin jauh lebih baik
  • Push notification - Bisa kirim notifikasi kayak native app. Bagus banget buat re-engagement
  • Fast loading - Karena asset di-cache, loading jadi cepet banget. Dari 3 detik jadi 0.3 detik
  • Cross-platform - Satu codebase jalan di Android, iOS, Windows, Mac. Gak perlu bikin 3 versi berbeda

Buat saya pribadi, alasan utama pakai PWA itu simpel: gak perlu review App Store. Mau update? Deploy aja. Selesai. User dapet versi terbaru langsung tanpa tunggu approval dari Apple atau Google.

Step 1: Bikin Web App Dasar

Sebelum jadi PWA, kita butuh web app yang berfungsi normal. Bikin folder project dulu:

mkdir pwa-notes
cd pwa-notes
mkdir css js

Trus bikin file index.html kayak gini:

<!DOCTYPE html>
<html lang="id">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Notes App - PWA Demo</title>
 <link rel="stylesheet" href="css/style.css">
 <link rel="manifest" href="manifest.json">
 <meta name="theme-color" content="#6c5ce7">
 <link rel="apple-touch-icon" href="icon-192.png">
</head>
<body>
 <div class="app">
 <header>
 <h1>Notes</h1>
 </header>
 <main>
 <form id="noteForm">
 <input type="text" id="noteInput" placeholder="Tulis catatan..." required>
 <button type="submit">Tambah</button>
 </form>
 <ul id="noteList"></ul>
 </main>
 </div>
 <script src="js/app.js"></script>
</body>
</html>

Perhatikan dua baris penting di <head>: manifest.json dan theme-color. Itu yang bikin browser tahu kalau web ini adalah PWA. Tanpa keduanya, gak akan ada prompt "Install App" di browser.

Sekarang bikin CSS sederhana di css/style.css:

* {
 margin: 0;
 padding: 0;
 box-sizing: border-box;
}

body {
 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
 background: #f0f0f5;
 color: #2d3436;
}

.app {
 max-width: 500px;
 margin: 0 auto;
 min-height: 100vh;
 display: flex;
 flex-direction: column;
}

header {
 background: #6c5ce7;
 color: white;
 padding: 20px;
 text-align: center;
 position: sticky;
 top: 0;
 z-index: 100;
}

#noteForm {
 display: flex;
 gap: 10px;
 padding: 15px;
}

#noteInput {
 flex: 1;
 padding: 12px;
 border: 2px solid #ddd;
 border-radius: 8px;
 font-size: 16px;
}

button {
 padding: 12px 20px;
 background: #6c5ce7;
 color: white;
 border: none;
 border-radius: 8px;
 cursor: pointer;
 font-size: 16px;
}

button:hover {
 background: #5a4bd1;
}

#noteList {
 list-style: none;
 padding: 0 15px;
}

#noteList li {
 background: white;
 padding: 15px;
 margin-bottom: 10px;
 border-radius: 8px;
 box-shadow: 0 2px 4px rgba(0,0,0,0.05);
 display: flex;
 justify-content: space-between;
 align-items: center;
 animation: slideIn 0.3s ease;
}

@keyframes slideIn {
 from { opacity: 0; transform: translateY(-10px); }
 to { opacity: 1; transform: translateY(0); }
}

.delete-btn {
 background: #e74c3c;
 padding: 6px 12px;
 font-size: 14px;
}

Desainnya minimalis. Sticky header biar header selalu keliatan pas scroll. Animation pas tambah note biar terasa "hidup". Kamu bisa customize warnanya sesuai selera.

Step 2: Bikin Web App Manifest

Manifest itu file JSON yang kasih tau browser "apa identitas app ini". Nama, icon, warna tema, orientation, semua di-set di sini. Bikin file manifest.json di root folder:

{
 "name": "Notes App - PWA Demo",
 "short_name": "Notes",
 "description": "Catatan sederhana yang jalan offline",
 "start_url": "/",
 "display": "standalone",
 "background_color": "#f0f0f5",
 "theme_color": "#6c5ce7",
 "orientation": "portrait",
 "icons": [
 {
 "src": "icon-192.png",
 "sizes": "192x192",
 "type": "image/png",
 "purpose": "any maskable"
 },
 {
 "src": "icon-512.png",
 "sizes": "512x512",
 "type": "image/png",
 "purpose": "any maskable"
 }
 ]
}

Field yang wajib kamu perhatiin:

  • name - Nama lengkap app. Muncul di splash screen pas pertama buka
  • short_name - Nama pendek di homescreen. Maksimal 12 karakter biar gak kepotong
  • display - standalone berarti tanpa address bar. Kayak native app. Bisa juga fullscreen (tanpa status bar) atau minimal-ui (dengan address bar mini)
  • start_url - URL yang kebuka pas user klik icon di homescreen
  • icons - Butuh minimal 192px dan 512px. purpose: "any maskable" biar icon bisa di-adapt ke shape apa aja (Android adaptive icon)

Untuk bikin icon, gak perlu Photoshop. Saya biasa pake RealFaviconGenerator atau tool online lain. Upload satu image 512x512, generate semua size. Praktis.

Step 3: Bikin Service Worker

Ini bagian paling penting. Service worker itu JavaScript yang jalan di background, terpisah dari halaman web. Dia bisa intercept network request, cache response, dan serve dari cache pas offline. Anggap aja kayak proxy lokal di browser.

Bikin file sw.js di root folder:

// Nama cache dan versi
const CACHE_NAME = 'notes-pwa-v1';
const ASSETS = [
 '/',
 '/index.html',
 '/css/style.css',
 '/js/app.js',
 '/manifest.json',
 '/icon-192.png',
 '/icon-512.png'
];

// Install: cache semua asset awal
self.addEventListener('install', (event) => {
 event.waitUntil(
 caches.open(CACHE_NAME)
 .then(cache => cache.addAll(ASSETS))
 .then(() => self.skipWaiting())
 );
});

// Activate: hapus cache lama
self.addEventListener('activate', (event) => {
 event.waitUntil(
 caches.keys().then(keys => {
 return Promise.all(
 keys
 .filter(key => key !== CACHE_NAME)
 .map(key => caches.delete(key))
 );
 })
 );
 self.clients.claim();
});

// Fetch: strategy cache-first untuk asset, network-first untuk API
self.addEventListener('fetch', (event) => {
 const { request } = event;
 
 // Skip non-GET requests
 if (request.method !== 'GET') return;
 
 event.respondWith(
 caches.match(request).then(cachedResponse => {
 // Return cached version if available
 if (cachedResponse) {
 return cachedResponse;
 }
 // Otherwise fetch from network
 return fetch(request).then(response => {
 // Cache new responses (only valid ones)
 if (!response || response.status !== 200) {
 return response;
 }
 const responseToCache = response.clone();
 caches.open(CACHE_NAME)
 .then(cache => cache.put(request, responseToCache));
 return response;
 });
 })
 );
});

Tiga event utama di service worker:

Install - Jalan sekali pas pertama kali register. Di sini kita pre-cache semua asset awal (HTML, CSS, JS, icon). skipWaiting() bikin service worker baru langsung aktif tanpa nunggu yang lama selesai.

Activate - Jalan pas service worker baru aktif. Di sini kita bersihin cache versi lama. Kalau kamu update app, ganti CACHE_NAME ke v2, cache lama otomatis kehapus.

Fetch - Jalan setiap kali browser mau request sesuatu. Strategy di atas itu cache-first - cek cache dulu, kalau ada langsung return. Kalau gak ada, fetch dari network, cache hasilnya buat next time. Sederhana tapi efektif buat static assets.

Penting: service worker cuma jalan di HTTPS. Kalau development lokal pake http://localhost, itu exception. Tapi production wajib HTTPS. Kalau belum punya SSL, pakai Let's Encrypt gratis.

Step 4: Register Service Worker

Service worker gak otomatis aktif. Harus di-register dari halaman. Tambahin ini di js/app.js:

// Register service worker
if ('serviceWorker' in navigator) {
 window.addEventListener('load', () => {
 navigator.serviceWorker.register('/sw.js')
 .then(reg => console.log('SW registered', reg.scope))
 .catch(err => console.error('SW failed', err));
 });
}

// === App Logic: Notes dengan localStorage ===
const noteForm = document.getElementById('noteForm');
const noteInput = document.getElementById('noteInput');
const noteList = document.getElementById('noteList');

// Load notes dari localStorage
function loadNotes() {
 const notes = JSON.parse(localStorage.getItem('notes') || '[]');
 noteList.innerHTML = '';
 notes.forEach((note, index) => {
 const li = document.createElement('li');
 li.innerHTML = `
 <span>${escapeHtml(note)}</span>
 <button class="delete-btn" onclick="deleteNote(${index})">Hapus</button>
 `;
 noteList.appendChild(li);
 });
}

// Tambah note
noteForm.addEventListener('submit', (e) => {
 e.preventDefault();
 const notes = JSON.parse(localStorage.getItem('notes') || '[]');
 notes.push(noteInput.value);
 localStorage.setItem('notes', JSON.stringify(notes));
 noteInput.value = '';
 loadNotes();
});

// Hapus note
window.deleteNote = (index) => {
 const notes = JSON.parse(localStorage.getItem('notes') || '[]');
 notes.splice(index, 1);
 localStorage.setItem('notes', JSON.stringify(notes));
 loadNotes();
};

// Prevent XSS
function escapeHtml(text) {
 const div = document.createElement('div');
 div.textContent = text;
 return div.innerHTML;
}

// Init
loadNotes();

Beberapa hal yang penting di kode di atas:

Pertama, if ('serviceWorker' in navigator) cek apakah browser support service worker. Jangan register kalau gak support, bakal error. Graceful degradation.

Kedua, window.addEventListener('load', ...) - register SW pas halaman selesai load. Kalau register terlalu awal, bisa ganggu first render. Performance best practice.

Ketiga, data disimpan di localStorage bukan database. Karena ini demo offline-first, localStorage cukup. Untuk app production, pakai IndexedDB yang bisa simpan data lebih banyak dan query lebih powerful. Library kayak localForage bikin IndexedDB gampang dipake kayak localStorage.

Keempat, escapeHtml() function. JANGAN pernah skip ini. Tanpa escaping, user bisa inject script lewat note-nya. Itu XSS vulnerability. Selalu escape user input sebelum render ke DOM.

Step 5: Test di Browser

Sekarang waktunya test. Kamu butuh server lokal karena SW gak jalan di file:// protocol. Pakai Python http server paling gampang:

cd pwa-notes
python3 -m http.server 8080

Buka http://localhost:8080 di Chrome. Buka DevTools (F12), ke tab Application:

  • Di Manifest - harus keliatan manifest.json kebaca, nama app, icon, theme color
  • Di Service Workers - harus ada status "activated and is running"
  • Di Cache Storage - harus ada notes-pwa-v1 dengan semua asset di-list
  • Di Storage - harus ada localStorage dengan key "notes"

Untuk test offline: centang checkbox "Offline" di Application tab. Refresh halaman. Harus tetap kebuka! Itulah kekuatan service worker.

Untuk test install prompt: buka di HP Android Chrome, tap menu (titik tiga), harus ada opsi "Install app". Atau di desktop Chrome, address bar bakal muncul icon install (+). Klik itu, app bakal jadi window standalone.

Step 6: Deploy ke Production

Untuk deploy PWA, kamu butuh hosting yang support HTTPS. Banyak opsi gratis:

  • Vercel - Gratis, auto HTTPS, CDN global. Tinggal npx vercel di folder project
  • Netlify - Juga gratis, drag-and-drop folder, auto HTTPS
  • GitHub Pages - Gratis untuk public repo, HTTPS included
  • VPS dengan Nginx - Kalau mau full control. Pakai Let's Encrypt buat SSL gratis

Kalau pakai VPS, pastikan nginx config serve file static dengan header yang benar:

location / {
 try_files $uri $uri/ /index.html;
}

# Manifest dan SW harus serve dengan correct MIME type
location = /manifest.json {
 add_header Content-Type application/manifest+json;
}

location = /sw.js {
 add_header Cache-Control "no-cache";
}

# Icon: cache lama karena jarang berubah
location ~* \.(png|ico|svg)$ {
 expires 30d;
 add_header Cache-Control "public, immutable";
}

Pitfall yang sering bikin pusing: sw.js harus selalu di-serve fresh. Kalau di-cache, update gak akan keambil. Set Cache-Control: no-cache khusus buat file ini. Sedangkan icon dan asset static bisa di-cache lama (30 hari+) karena jarang berubah.

Step 7: Add Push Notification (Bonus)

Bagian ini opsional tapi bagus buat engagement. Push notification butuh VAPID key pair. Generate pake web-push library:

npm install web-push -g
web-push generate-vapid-keys

Simpan public key di client side. Bikin subscription pas user click "Enable Notifications":

async function subscribePush() {
 const permission = await Notification.requestPermission();
 if (permission !== 'granted') return;
 
 const reg = await navigator.serviceWorker.ready;
 const subscription = await reg.pushManager.subscribe({
 userVisibleOnly: true,
 applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_KEY')
 });
 
 // Kirim subscription ke server buat disimpan
 await fetch('/api/subscribe', {
 method: 'POST',
 body: JSON.stringify(subscription),
 headers: { 'Content-Type': 'application/json' }
 });
}

function urlBase64ToUint8Array(base64String) {
 const padding = '='.repeat((4 - base64String.length % 4) % 4);
 const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
 const rawData = atob(base64);
 const output = new Uint8Array(rawData.length);
 for (let i = 0; i < rawData.length; i++) {
 output[i] = rawData.charCodeAt(i);
 }
 return output;
}

Di server side, pakai web-push library buat kirim notifikasi:

// Node.js server
const webpush = require('web-push');

webpush.setVapidDetails(
 'mailto:[email protected]',
 'YOUR_PUBLIC_KEY',
 'YOUR_PRIVATE_KEY'
);

// Kirim push ke subscriber
webpush.sendNotification(subscription, JSON.stringify({
 title: 'Notes App',
 body: 'Jangan lupa catat pengeluaran hari ini!'
}));

Server butuh endpoint buat simpan subscription dan kirim push. Bisa pakai Express.js, Flask, atau apapun. Yang penting library web-push ada di server.

Troubleshooting: Masalah yang Sering Terjadi

"Service worker gak activate" - Buka DevTools Application tab, lihat error message. Biasanya karena syntax error di sw.js. Atau path file salah. SW registrasi return error di console.

"Icon gak muncul di homescreen" - Pastikan icon ukuran 192px dan 512px. Format PNG. Path di manifest.json benar. purpose: "any maskable" wajib ada biar Android adaptive icon jalan.

"App gak bisa install" - Chrome butuh: valid manifest, registered service worker, HTTPS, dan minimal satu fetch event handler. Kalau salah satu gak ada, install prompt gak muncul.

"Offline tetap blank page" - Cek apakah / (root URL) ada di cache list. Service worker harus cache index.html DAN route /. Kadang keduanya dianggap beda oleh cache API.

"Update gak keambil" - Service worker cache diri-nya sendiri. Pas update SW, browser ambil versi baru di background, tapi gak langsung aktif. User harus tutup semua tab dan buka lagi. Atau pakai skipWaiting() dan clients.claim() kayak di kode di atas buat force update.

Kesimpulan

PWA itu powerful. Kamu bisa bikin app yang installable, offline-ready, dan push-enabled cuma dengan vanilla JavaScript. Tanpa framework, tanpa App Store review, tanpa biaya deploy. Cuma butuh service worker, manifest, dan HTTPS.

Yang penting diingat: service worker itu powerful tapi gampang bikin bingung. Cache strategy yang salah bikin user liat versi lama web kamu. Selalu pakai version di CACHE_NAME dan update pas ada perubahan. Test offline mode setiap kali update.

Untuk project production, kamu mungkin mau pakai Workbox (library dari Google) yang bikin service worker lebih gampang di-manage. Tapi prinsipnya sama kayak yang kita bahas di sini.

Sudah pernah bikin PWA? Atau masih ragu mau mulai? Kasih tau di komentar, saya bantu kalau ada yang stuck. Tutorial ini cuma basic, masih banyak yang bisa di-explore kayak background sync, periodic sync, dan file system access API. Mantap!


Bagikan artikel ini:

Komentar (0)

Belum ada komentar. Jadilah yang pertama memberikan tanggapan!

Tinggalkan Komentar