Service Worker dan PWA: Bikin Aplikasi Web Offline-First di 2026

Developer 30 Mei 2026 · OTPZap Team

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:

  1. Register: main thread call navigator.serviceWorker.register('/sw.js')
  2. Install: SW di-download dan di-install. Saat ini bisa cache static asset.
  3. Activate: SW activated. Old SW (kalau ada) di-replace.
  4. Idle: SW dorman, terminate after some time.
  5. 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:

  1. Manifest file: manifest.json dengan app name, icon, theme color, display mode
  2. Service Worker registered
  3. HTTPS (atau localhost untuk dev)
  4. 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:

TIDAK, native app diperlukan kalau:

Limitations di iOS yang Masih Ada

Apple historically restrict PWA capability di iOS. Improvement di iOS 17/18, tapi masih ada gap:

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.