1. Pengenalan Service Workers
Service Worker adalah script JavaScript yang berjalan di background, terpisah dari halaman web. Service Worker bertindak sebagai proxy antara browser, jaringan, dan cache. Ini memungkinkan web app Anda berfungsi offline, mengirim push notifications, dan menjadi Progressive Web App (PWA).
Service Worker vs Web Worker
| Fitur | Web Worker | Service Worker |
|---|---|---|
| Lifecycle | Hidup selama tab aktif | Independen dari tab (hidup terus) |
| Scope | Satu halaman | Seluruh origin (semua halaman) |
| Proxy Network | Tidak | Ya β bisa intercept semua fetch |
| Push Notification | Tidak | Ya |
| Cache API | Bisa akses | Bisa akses dan kontrol |
| Akses DOM | Tidak | Tidak |
Browser (Tab) βββββββΊ Service Worker βββββββΊ Network
β β
βΌ β
Cache β
β β
βββββ Response βββββββ
Alur normal:
1. Browser meminta resource (halaman, gambar, API)
2. Service Worker menangkap request (fetch event)
3. Service Worker memutuskan:
- Kirim dari cache? (cepat, offline)
- Kirim dari network? (data terbaru)
- Gabungan keduanya? (stale-while-revalidate)
4. Response dikembalikan ke browser
Syarat Service Worker
HTTPS β Service Worker hanya berfungsi di HTTPS (atau localhost untuk development). File service worker harus berada di root domain agar scope-nya mencakup seluruh halaman. Service Worker tidak bisa mengakses DOM.
2. Lifecycle Service Worker
Service Worker memiliki lifecycle yang unik dan terdiri dari 3 tahap utama: Register β Install β Activate.
ββββββββββββ βββββββββββββ ββββββββββββββββ βββββββββββββ
β Register βββββΊβ Install βββββΊβ Waiting βββββΊβ Activate β
β β β β β (jika ada SW β β β
β navigator β β Pre-cache β β lama aktif) β β Cleanup β
β .service β β assets β β β β cache lama β
β Worker β β β β β β β
β .register β β skipWaitingβ β clients β β Controllingβ
ββββββββββββ βββββββββββββ β .claim() β β semua tab β
ββββββββββββββββ βββββββββββββ
β
βββββββββΌββββββββ
β Fetch Event β
β Activate Eventβ
β (idle/terminated)β
ββββββββββββββββββ
// ===== sw.js β Service Worker File =====
// EVENT 1: INSTALL β Dipanggil saat pertama kali diinstal
self.addEventListener('install', (event) => {
console.log('[SW] Installing...');
// event.waitUntil() β tunggu sampai promise selesai
// Jika gagal, install dianggap gagal
event.waitUntil(
caches.open('v1-cache').then((cache) => {
console.log('[SW] Pre-caching assets...');
// Pre-cache file-file penting
return cache.addAll([
'/',
'/index.html',
'/css/style.css',
'/js/app.js',
'/images/logo.png',
'/offline.html'
]);
})
);
// skipWaiting() β langsung aktif tanpa menunggu tab ditutup
self.skipWaiting();
});
// EVENT 2: ACTIVATE β Dipanggil setelah install, sebelum fetch
self.addEventListener('activate', (event) => {
console.log('[SW] Activating...');
event.waitUntil(
// Hapus cache lama yang tidak digunakan
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter(name => name !== 'v1-cache')
.map(name => {
console.log('[SW] Menghapus cache lama:', name);
return caches.delete(name);
})
);
})
);
// Klaim semua klien (tab) yang sudah terbuka
self.clients.claim();
});
// EVENT 3: FETCH β Menangkap semua network request
self.addEventListener('fetch', (event) => {
console.log('[SW] Fetch:', event.request.url);
// Intercept request dan kirim response custom
event.respondWith(
fetch(event.request).catch(() => {
// Jika offline, kirim halaman offline
return caches.match('/offline.html');
})
);
});
3. Registrasi & Instalasi
// ===== app.js β Registrasi di halaman web =====
// Feature detection
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/' // Scope: area yang dicakup SW
});
console.log('SW registered:', registration.scope);
// Cek update
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
console.log('SW update ditemukan!');
newWorker.addEventListener('statechange', () => {
console.log('SW state:', newWorker.state);
// states: installing β installed β activating β activated
if (newWorker.state === 'activated') {
// Tampilkan notifikasi "Update tersedia"
showUpdateNotification();
}
});
});
} catch (error) {
console.error('SW registration failed:', error);
}
});
}
// Cek apakah ada update service worker
async function checkForUpdates() {
if ('serviceWorker' in navigator) {
const reg = await navigator.serviceWorker.getRegistration();
if (reg) {
await reg.update();
}
}
}
// Cek update setiap 1 jam
setInterval(checkForUpdates, 60 * 60 * 1000);
Saat Anda mengubah file sw.js, browser akan mendeteksi perubahan dan menginstal versi baru. Namun, versi baru tidak langsung aktif β ia masuk ke status waiting. Gunakan self.skipWaiting() untuk langsung mengaktifkan, atau biarkan user memilih untuk "Refresh untuk update".
4. Fetch Event & Intersep
Event fetch di-trigger setiap kali halaman meminta resource β HTML, CSS, JS, gambar, API call, semuanya. Anda bisa menangkap request ini dan memutuskan bagaimana meresponsnya.
// ===== Berbagai cara menangani fetch event =====
// 1. Passthrough β biarkan request ke network (default)
self.addEventListener('fetch', (event) => {
// Tidak melakukan apa-apa β request ke network biasa
});
// 2. Custom Response β buat response sendiri
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/time')) {
event.respondWith(
new Response(
JSON.stringify({ time: new Date().toISOString() }),
{
headers: { 'Content-Type': 'application/json' }
}
)
);
}
});
// 3. Redirect β arahkan ke URL lain
self.addEventListener('fetch', (event) => {
if (event.request.url.endsWith('/old-page')) {
event.respondWith(
Response.redirect('/new-page', 301)
);
}
});
// 4. Modify Response β ubah response dari network
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request).then(async (response) => {
// Clone response (karena response hanya bisa dibaca sekali)
const clone = response.clone();
const data = await clone.json();
// Tambahkan metadata
data._cached = false;
data._timestamp = Date.now();
return new Response(JSON.stringify(data), {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
})
);
}
});
5. Caching Strategies
Caching strategy menentukan bagaimana Service Worker menangani request: apakah mengambil dari cache, network, atau kombinasi keduanya.
Cache API
// ===== Cache API =====
// Cache API tersedia di Service Worker dan window context
// Buka cache bernama 'v1'
const cache = await caches.open('v1');
// Tambahkan satu item
await cache.add('/page/about.html');
// Tambahkan banyak item sekaligus
await cache.addAll([
'/css/style.css',
'/js/app.js',
'/images/logo.png'
]);
// Simpan request-response pair
const response = await fetch('/api/data');
await cache.put('/api/data', response.clone());
// Ambil dari cache
const cachedResponse = await cache.match('/css/style.css');
if (cachedResponse) {
console.log('Ditemukan di cache!');
}
// Hapus item dari cache
await cache.delete('/old-file.js');
// List semua cache names
const cacheNames = await caches.keys();
console.log('Semua cache:', cacheNames);
// Hapus cache tertentu
await caches.delete('v1');
// Cek apakah request ada di cache
const request = new Request('/page/about.html');
const matched = await caches.match(request); // Cek semua cache
console.log(matched ? 'Ada!' : 'Tidak ada');
6. Cache-First & Network-First
Strategy 1: Cache-Only
// ===== Cache-Only: Selalu dari cache, tidak ada network =====
// Cocok untuk: pre-cached assets yang tidak berubah
self.addEventListener('fetch', (event) => {
event.respondWith(caches.match(event.request));
});
Strategy 2: Network-Only
// ===== Network-Only: Selalu dari network =====
// Cocok untuk: POST requests, realtime data
self.addEventListener('fetch', (event) => {
event.respondWith(fetch(event.request));
});
Strategy 3: Cache-First (with Network Fallback)
// ===== Cache-First: Cek cache dulu, kalau tidak ada ke network =====
// Cocok untuk: static assets (gambar, CSS, JS, font)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse; // β
Dari cache
}
// Tidak ada di cache β fetch dari network
return fetch(event.request).then((networkResponse) => {
// Simpan ke cache untuk next time
const responseClone = networkResponse.clone();
caches.open('v1').then((cache) => {
cache.put(event.request, responseClone);
});
return networkResponse;
});
})
);
});
Strategy 4: Network-First (with Cache Fallback)
// ===== Network-First: Coba network dulu, fallback ke cache =====
// Cocok untuk: halaman HTML, data yang sering berubah
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((networkResponse) => {
// Update cache dengan data terbaru
const responseClone = networkResponse.clone();
caches.open('v1').then((cache) => {
cache.put(event.request, responseClone);
});
return networkResponse;
})
.catch(() => {
// Network gagal β ambil dari cache
return caches.match(event.request);
})
);
});
Strategy 5: Stale-While-Revalidate
// ===== Stale-While-Revalidate: Cache dulu, update di background =====
// Cocok untuk: API data yang perlu cepat tapi juga terbaru
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open('v1').then((cache) => {
return cache.match(event.request).then((cachedResponse) => {
// Fetch dari network SELALU (untuk update cache)
const fetchPromise = fetch(event.request).then((networkResponse) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
// Kembalikan cache langsung jika ada, atau tunggu network
return cachedResponse || fetchPromise;
});
})
);
});
Ringkasan Strategi
| Strategy | Cepat? | Fresh? | Offline? | Cocok untuk |
|---|---|---|---|---|
| Cache-Only | β | β | β | Pre-cached static |
| Network-Only | β | β | β | POST, realtime |
| Cache-First | β | β | β | Static assets |
| Network-First | β | β | β | HTML, API data |
| Stale-While-Revalidate | β | β * | β | API data, feed |
7. Push Notifications
Service Worker memungkinkan web app mengirim push notifications bahkan ketika browser ditutup. Ini sangat mirip dengan notifikasi dari aplikasi native.
// ===== 1. Minta Izin Notifikasi =====
async function requestNotificationPermission() {
// Cek support
if (!('Notification' in window)) {
console.log('Browser tidak support notifikasi');
return false;
}
// Cek permission
if (Notification.permission === 'granted') {
return true;
}
if (Notification.permission !== 'denied') {
const permission = await Notification.requestPermission();
return permission === 'granted';
}
return false;
}
// ===== 2. Subscribe ke Push =====
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready;
// VAPID key β di-generate dari server
const vapidPublicKey = 'BEl62iUYgUivxIkv69yVi...';
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
});
console.log('Push subscription:', JSON.stringify(subscription));
// Kirim subscription ke server
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
}
// Helper: convert VAPID key
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)));
}
// ===== 3. Handle Push Event di Service Worker =====
self.addEventListener('push', (event) => {
let data = { title: 'Notifikasi Baru', body: 'Ada info baru!' };
if (event.data) {
data = event.data.json();
}
const options = {
body: data.body,
icon: '/images/icon-192.png',
badge: '/images/badge-72.png',
image: data.image,
vibrate: [100, 50, 100],
data: { url: data.url || '/' },
actions: [
{ action: 'open', title: 'Buka', icon: '/images/open.png' },
{ action: 'close', title: 'Tutup', icon: '/images/close.png' }
]
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
// ===== 4. Handle Notification Click =====
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const action = event.action;
const url = event.notification.data.url;
if (action === 'close') return;
event.waitUntil(
clients.matchAll({ type: 'window' }).then((windowClients) => {
// Cek apakah sudah ada window yang terbuka
for (const client of windowClients) {
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
// Buka window baru
return clients.openWindow(url);
})
);
});
8. Background Sync
Background Sync memungkinkan Anda menunda operasi (seperti mengirim form) sampai koneksi internet tersedia kembali β bahkan jika user menutup tab!
// ===== Background Sync =====
// 1. Di halaman web β daftarkan sync
async function submitForm(data) {
try {
// Coba kirim langsung
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(data)
});
showSuccess('Data berhasil dikirim!');
} catch (error) {
// Gagal (offline?) β simpan ke IndexedDB dan daftarkan sync
await saveToIndexedDB('pending-submissions', data);
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-submissions');
showInfo('Data akan dikirim saat online kembali');
}
}
// 2. Di Service Worker β handle sync event
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-submissions') {
event.waitUntil(syncSubmissions());
}
});
async function syncSubmissions() {
const db = await openDB();
const submissions = await db.getAll('pending-submissions');
for (const submission of submissions) {
try {
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(submission)
});
// Hapus dari IndexedDB setelah berhasil
await db.delete('pending-submissions', submission.id);
console.log('[SW] Sync berhasil:', submission.id);
} catch (error) {
console.log('[SW] Sync gagal, akan dicoba lagi:', error);
throw error; // Throw agar sync dicoba lagi nanti
}
}
}
// Periodic Background Sync (Chrome only)
self.addEventListener('periodicsync', (event) => {
if (event.tag === 'update-content') {
event.waitUntil(updateContentFromServer());
}
});
async function updateContentFromServer() {
const cache = await caches.open('v1');
const response = await fetch('/api/latest-content');
await cache.put('/api/latest-content', response);
console.log('[SW] Content updated via periodic sync');
}
9. Membangun PWA Lengkap
Progressive Web App (PWA) membutuhkan 3 komponen: Service Worker, Web App Manifest, dan HTTPS.
{
"name": "BeebaneLabs Tutorial",
"short_name": "Beebane",
"description": "Portal tutorial teknologi Indonesia",
"start_url": "/",
"display": "standalone",
"background_color": "#08090a",
"theme_color": "#08090a",
"orientation": "portrait-primary",
"icons": [
{
"src": "/images/icon-72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/images/icon-96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/images/icon-128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/images/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": ["education", "technology"],
"lang": "id",
"dir": "ltr"
}
<!-- Tambahkan di <head> halaman Anda --> <link rel="manifest" href="/manifest.json"> <!-- Theme color untuk browser chrome --> <meta name="theme-color" content="#08090a"> <!-- iOS support --> <link rel="apple-touch-icon" href="/images/icon-192.png"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-title" content="BeebaneLabs"> <!-- Windows --> <meta name="msapplication-TileColor" content="#08090a"> <meta name="msapplication-TileImage" content="/images/icon-144.png">
Install Prompt (Add to Home Screen)
// ===== Handle Install Prompt =====
let deferredPrompt;
// Tangkap event 'beforeinstallprompt'
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault(); // Cegah auto-prompt
deferredPrompt = event;
// Tampilkan tombol "Install" custom
const installBtn = document.getElementById('installBtn');
installBtn.style.display = 'block';
installBtn.addEventListener('click', async () => {
installBtn.style.display = 'none';
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log('User choice:', outcome); // 'accepted' atau 'dismissed'
deferredPrompt = null;
});
});
// Deteksi saat app berhasil di-install
window.addEventListener('appinstalled', () => {
console.log('PWA berhasil di-install!');
deferredPrompt = null;
});
// Deteksi apakah app sedang berjalan sebagai PWA
function isRunningAsPWA() {
return window.matchMedia('(display-mode: standalone)').matches
|| window.navigator.standalone === true;
}
10. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut: