Service Worker dan PWA: Bikin Aplikasi Web Offline-First di 2026
Tahun 2026, gap antara web app dan native app makin tipis. Twitter (X) sebagian besar pakai PWA. Spotify Web rasa kayak app native. Notion Desktop sebenarnya PWA wrapper. Bahkan iOS akhirnya kasih PWA proper home screen support.
Kunci di balik semua ini: Service Worker. Tapi banyak developer web belum pakai Service Worker karena dianggap kompleks. Padahal di 2026, tools dan pattern udah matang. Setup PWA proper jadi much easier.
Artikel ini panduan praktis Service Worker dan PWA dari perspective developer 2026: kapan worth invest, common pattern, dan limitations yang masih ada.
Apa Itu Service Worker
Service Worker adalah JavaScript yang jalan di background, terpisah dari main thread browser tab. Dia intercept network request dan bisa respond dengan data cache, atau modify response. Dia juga handle push notification dan background sync.
Lifecycle Service Worker:
- Register: main thread call
navigator.serviceWorker.register('/sw.js') - Install: SW di-download dan di-install. Saat ini bisa cache static asset.
- Activate: SW activated. Old SW (kalau ada) di-replace.
- Idle: SW dorman, terminate after some time.
- Wakeup: SW di-bangunin saat ada event (fetch, push, sync).
Penting: SW hidup independent dari tab. Bahkan kalau tab close, SW masih bisa receive push notification dan trigger background sync.
Mengapa Service Worker Game-Changer
1. Offline Capability
Cache static asset dan data API. User offline tetap bisa akses sebagian besar feature. Critical untuk mobile user di Indonesia di mana koneksi unreliable.
2. Performance Boost
Asset di cache lokal = load instant di repeat visit. Network request optional, ngga critical path. App rasa lebih responsif.
3. Push Notification
Web push notification jalan via Service Worker. User bisa di-engage walau ngga buka tab. App-like engagement.
4. Background Sync
User submit form saat offline, queue di IndexedDB. Saat online, background sync send data tanpa user open app. Reliability messaging-style.
5. Update Mechanism
SW handle versioning. Update detect, install di background, activate saat user reload. Better user experience daripada force refresh.
Setup Service Worker: Minimal Working Example
Ini barebone SW yang cache static asset:
// sw.js
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/logo.png'
];
// Install: cache static asset
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(STATIC_ASSETS);
})
);
self.skipWaiting(); // jadi active immediate
});
// Activate: cleanup old cache
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((names) => {
return Promise.all(
names.filter(n => n !== CACHE_NAME)
.map(n => caches.delete(n))
);
})
);
self.clients.claim();
});
// Fetch: cache-first for static, network-first for API
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.pathname.startsWith('/api/')) {
// Network first untuk API
event.respondWith(
fetch(event.request).catch(() => caches.match(event.request))
);
} else {
// Cache first untuk static
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request);
})
);
}
});
Register di main thread:
// app.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered'))
.catch(err => console.error('SW failed:', err));
}
Caching Strategies: Pilih yang Tepat
Pattern caching umum:
1. Cache First
Cek cache dulu, kalau ada serve dari cache. Kalau ngga ada, fetch dari network dan cache.
Use case: static asset (CSS, JS, image). Asset yang ngga sering berubah.
2. Network First
Coba fetch network dulu. Kalau berhasil, cache dan serve. Kalau gagal (offline), fallback ke cache.
Use case: API response yang freshness penting. News feed, dashboard data.
3. Stale While Revalidate
Serve dari cache immediate (stale OK). Sambil itu, fetch network update di background. Next request dapat data fresh.
Use case: balanced freshness vs speed. User profile, frequently-accessed data.
4. Network Only
Always fetch dari network. No cache.
Use case: critical data yang harus realtime (banking transaction, payment).
5. Cache Only
Always serve dari cache. No network.
Use case: pre-cached static asset, app shell.
Pakai Workbox: Less Boilerplate
Workbox (oleh Google) adalah library yang abstract caching strategy. Konfigurasi declarative, auto-generate SW.
// workbox-config.js
module.exports = {
globDirectory: 'dist/',
globPatterns: ['**/*.{html,js,css,png,jpg}'],
swDest: 'dist/sw.js',
runtimeCaching: [{
urlPattern: /^https:\/\/api\.yoursite\.com/,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: { maxAgeSeconds: 60 * 5 } // 5 menit
}
}, {
urlPattern: /\.(png|jpg|jpeg|svg)$/,
handler: 'CacheFirst',
options: {
cacheName: 'image-cache',
expiration: { maxAgeSeconds: 60 * 60 * 24 * 30 } // 30 hari
}
}]
};
Build via workbox-cli generate optimized SW. Untuk project medium ke besar, ini saving banyak waktu vs hand-roll.
Push Notification Implementation
Push notification web butuh 3 component:
1. Subscribe User
const reg = await navigator.serviceWorker.ready;
const subscription = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VAPID_PUBLIC_KEY
});
// Send subscription ke server
await fetch('/api/subscribe', {
method: 'POST',
body: JSON.stringify(subscription)
});
2. Server Send Push
// Node.js example pakai web-push library
const webPush = require('web-push');
webPush.setVapidDetails(
'mailto:[email protected]',
VAPID_PUBLIC_KEY,
VAPID_PRIVATE_KEY
);
await webPush.sendNotification(subscription, JSON.stringify({
title: 'New message',
body: 'You have unread messages',
icon: '/icon.png',
url: '/inbox'
}));
3. SW Handle Push Event
self.addEventListener('push', (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon,
data: { url: data.url }
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
});
Use case: chat app, marketplace order update, kabar penting. Important: jangan over-use, user bisa block notification dari domain lo permanent.
Background Sync
User submit form saat offline. Naive: kasih error. Better: queue di IndexedDB, sync saat online.
// Trigger sync
const reg = await navigator.serviceWorker.ready;
await reg.sync.register('sync-form-submission');
// Di SW
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-form-submission') {
event.waitUntil(syncQueuedForms());
}
});
async function syncQueuedForms() {
const db = await openDB('queue', 1);
const queue = await db.getAll('forms');
for (const form of queue) {
try {
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(form.data)
});
await db.delete('forms', form.id);
} catch (err) {
// Retry next sync
console.error('Sync failed', err);
}
}
}
Browser otomatis trigger sync saat online. Reliable mechanism untuk eventual consistency.
PWA: Web App Yang Bisa Di-Install
PWA = Progressive Web App. Web app yang fulfill criteria specific bisa di-"install" sebagai app di home screen.
Requirement:
- Manifest file:
manifest.jsondengan app name, icon, theme color, display mode - Service Worker registered
- HTTPS (atau localhost untuk dev)
- Icon di-define multiple size
Contoh manifest:
{
"name": "OTPZap",
"short_name": "OTPZap",
"start_url": "/",
"display": "standalone",
"background_color": "#0c0e16",
"theme_color": "#a566ff",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
Browser detect criteria, kasih "Install app" prompt. User install, app appear di home screen, jalan dalam window standalone (no browser UI).
Kapan PWA Bisa Replace Native App
YES, PWA cukup kalau:
- App primarily content delivery (news, blog, e-commerce, dashboard)
- Functionality bisa dicapai dengan web API (push notification, geolocation, camera basic, payment)
- Ngga butuh deep OS integration
- Cross-platform tanpa native team
TIDAK, native app diperlukan kalau:
- Heavy AR / VR / 3D processing
- Bluetooth advanced (BLE pairing, custom protocol)
- Background process panjang (periodic sync, location tracking)
- Native widget di home screen
- Tight integration ke OS feature (Siri shortcuts, App Clip, Apple Watch)
- iOS-specific feature yang Apple block dari web (NFC payment, etc)
Limitations di iOS yang Masih Ada
Apple historically restrict PWA capability di iOS. Improvement di iOS 17/18, tapi masih ada gap:
- Storage limit per origin (50MB-1GB tergantung available space)
- Push notification: support, tapi user must "Add to Home Screen" dulu
- Background sync: limited
- Bluetooth, NFC, USB: tetap ngga support web
- App Clip equivalent: ngga ada
Untuk iOS-heavy user base, native app via Capacitor atau React Native masih jadi opsi yang lebih lengkap.
Penutup
Service Worker dan PWA udah lebih matang di 2026 dibanding 5 tahun lalu. Library kayak Workbox abstract complexity. Browser support universal. iOS akhirnya support proper.
Untuk web app baru, PWA harus jadi default consideration. Effort tambahan minimal kalau pakai Workbox. Benefit: offline capability, faster repeat visit, install-able. Lo dapat 80% functionality native app dengan 20% effort.
Untuk app yang udah live, evaluate one feature at a time. Mulai dengan static asset cache (impact paling besar untuk speed). Tambah API caching kalau make sense. Push notification kalau use case fit.
Yang ngga boleh: bikin PWA "untuk fancy". Lo butuh use case nyata yang user dapat benefit. Otherwise, complexity tambahan ngga worth-it.