PWA #

Progressive Web Application adalah kumpulan teknologi yang memungkinkan web application berperilaku seperti aplikasi native: bisa diinstall di home screen, bekerja offline, menerima push notification, dan memuat konten lebih cepat dari server dengan memanfaatkan cache yang cerdas. Tidak ada satu teknologi tunggal yang disebut “PWA” — yang ada adalah tiga pilar yang bekerja bersama: Service Worker, Web App Manifest, dan HTTPS. Memahami cara kerja setiap pilar ini, terutama Service Worker dan strategi caching-nya, adalah kunci untuk membangun PWA yang benar-benar bermanfaat dan bukan sekadar menambahkan manifest file dan menyebutnya PWA. Artikel ini membahas setiap pilar secara mendalam, strategi caching yang tepat untuk berbagai jenis konten, dan kapan PWA layak dipertimbangkan.

Tiga Pilar PWA #

flowchart TD
    subgraph Pillars["Tiga Pilar PWA"]
        SW["Service Worker\nProxy JavaScript yang berjalan\ndi background thread\n→ Offline support\n→ Cache control\n→ Push notification\n→ Background sync"]
        
        Manifest["Web App Manifest\nFile JSON yang mendefinisikan\nidentitas dan tampilan app\n→ Nama dan ikon\n→ Start URL\n→ Display mode\n→ Theme color"]
        
        HTTPS["HTTPS\nProtokol aman wajib\nuntuk semua PWA\n→ Service Worker hanya\n   bekerja via HTTPS\n→ Mencegah man-in-\n   the-middle attack"]
    end

    subgraph Capabilities["Kapabilitas yang Dihasilkan"]
        Offline["Offline Support"]
        Install["Installable"]
        Push["Push Notification"]
        Fast["Load Cepat"]
    end

    SW --> Offline
    SW --> Push
    SW --> Fast
    Manifest --> Install
    HTTPS --> SW

Service Worker — Jantung PWA #

Service Worker adalah JavaScript file yang berjalan di background thread terpisah dari main thread halaman. Ia bertindak sebagai proxy antara aplikasi dan network — setiap request yang dibuat aplikasi bisa dicegat, dimodifikasi, atau dijawab dari cache oleh Service Worker.

Lifecycle Service Worker:

1. Registration — aplikasi mendaftarkan service worker
   navigator.serviceWorker.register('/sw.js')

2. Installation — browser download dan install sw.js
   → Event 'install' dipanggil di service worker
   → Kesempatan untuk pre-cache asset penting

3. Activation — service worker menjadi aktif, mengontrol halaman
   → Event 'activate' dipanggil
   → Kesempatan untuk cleanup cache lama

4. Fetch Interception — service worker mencegat semua request
   → Event 'fetch' dipanggil untuk setiap request
   → Bisa return dari cache atau ke network

Penting: Service Worker hanya bekerja di HTTPS (kecuali localhost)
         Service Worker berjalan di background — tidak punya akses ke DOM
sequenceDiagram
    participant Page as Halaman Web
    participant SW as Service Worker
    participant Cache as Cache Storage
    participant Net as Network

    Page->>SW: fetch('/api/products')
    Note over SW: SW mencegat request

    SW->>Cache: Cek apakah ada di cache
    
    alt Ada di cache (Cache Hit)
        Cache-->>SW: Return cached response
        SW-->>Page: Response dari cache (sangat cepat!)
        SW->>Net: Fetch di background untuk update cache
    else Tidak di cache (Cache Miss)
        SW->>Net: Fetch ke network
        Net-->>SW: Response dari server
        SW->>Cache: Simpan response ke cache
        SW-->>Page: Return response
    end

Strategi Caching Service Worker #

Ini adalah bagian yang paling kritis dalam membangun PWA. Strategi caching yang salah bisa menyebabkan user mendapat konten yang outdated atau tidak ada konten sama sekali saat offline.

// sw.js — Service Worker dengan multiple caching strategies

const CACHE_NAME = 'app-v1'
const STATIC_CACHE = 'static-v1'
const API_CACHE = 'api-v1'

// === INSTALL: Pre-cache asset kritis ===
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(STATIC_CACHE).then((cache) => {
      return cache.addAll([
        '/',
        '/index.html',
        '/main.bundle.js',
        '/app.css',
        '/icons/icon-192.png',
        // Asset yang WAJIB ada untuk app shell berfungsi
      ])
    })
  )
})

// === ACTIVATE: Cleanup cache lama ===
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames
          .filter((name) => name !== STATIC_CACHE && name !== API_CACHE)
          .map((name) => caches.delete(name))  // hapus cache versi lama
      )
    })
  )
})

// === FETCH: Pilih strategi berdasarkan jenis request ===
self.addEventListener('fetch', (event) => {
  const { request } = event
  const url = new URL(request.url)

  // Static asset → Cache First
  if (isStaticAsset(url)) {
    event.respondWith(cacheFirst(request, STATIC_CACHE))
  }
  // API data → Network First
  else if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(request, API_CACHE))
  }
  // HTML pages → Stale While Revalidate
  else if (request.headers.get('accept')?.includes('text/html')) {
    event.respondWith(staleWhileRevalidate(request, STATIC_CACHE))
  }
})

Strategi 1: Cache First #

Prioritaskan cache, gunakan network hanya sebagai fallback. Ideal untuk asset yang jarang berubah.

async function cacheFirst(request, cacheName) {
  const cachedResponse = await caches.match(request)
  if (cachedResponse) {
    return cachedResponse  // return dari cache — sangat cepat
  }
  // Tidak ada di cache, ambil dari network
  const networkResponse = await fetch(request)
  const cache = await caches.open(cacheName)
  cache.put(request, networkResponse.clone())  // simpan untuk next time
  return networkResponse
}

// Cocok untuk:
// ✓ JavaScript dan CSS dengan content hash (app.4f3a2c.js)
// ✓ Font files
// ✓ Gambar yang jarang berubah
// ✗ Tidak cocok untuk API data atau HTML — user akan dapat konten stale

Strategi 2: Network First #

Prioritaskan network, gunakan cache sebagai fallback saat offline. Ideal untuk konten yang sering berubah.

async function networkFirst(request, cacheName) {
  try {
    const networkResponse = await fetch(request)
    // Simpan response ke cache untuk fallback offline
    const cache = await caches.open(cacheName)
    cache.put(request, networkResponse.clone())
    return networkResponse
  } catch {
    // Network gagal (offline) — coba cache
    const cachedResponse = await caches.match(request)
    if (cachedResponse) {
      return cachedResponse
    }
    // Tidak ada di cache juga — return offline page
    return caches.match('/offline.html')
  }
}

// Cocok untuk:
// ✓ API endpoints (data yang sering berubah)
// ✓ Halaman HTML yang butuh konten terbaru
// ✗ Tidak optimal untuk asset statis — network overhead tidak perlu

Strategi 3: Stale While Revalidate #

Return cache segera (cepat), update cache di background. Terbaik untuk konten yang boleh sedikit outdated.

async function staleWhileRevalidate(request, cacheName) {
  const cache = await caches.open(cacheName)
  const cachedResponse = await cache.match(request)

  // Update cache di background (tidak menunggu)
  const fetchPromise = fetch(request).then((networkResponse) => {
    cache.put(request, networkResponse.clone())
    return networkResponse
  })

  // Return cache segera jika ada, atau tunggu network
  return cachedResponse || fetchPromise
}

// Cocok untuk:
// ✓ Asset yang update berkala tapi tidak harus real-time (konten berita, avatar user)
// ✓ HTML halaman yang kontennya berubah tapi tidak kritis untuk selalu fresh
// ✓ Data yang toleran terhadap slight staleness

Web App Manifest — Identitas PWA #

Web App Manifest adalah file JSON yang memberitahu browser bagaimana aplikasi harus berperilaku saat diinstall di perangkat.

// /manifest.json
{
  "name": "Toko Online App",
  "short_name": "Toko",
  "description": "Belanja online mudah dan cepat",
  "start_url": "/",
  "display": "standalone",
  "orientation": "portrait",
  "background_color": "#ffffff",
  "theme_color": "#2C3E50",
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ],
  "screenshots": [
    {
      "src": "/screenshots/home.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide"
    }
  ],
  "shortcuts": [
    {
      "name": "Keranjang",
      "url": "/cart",
      "icons": [{ "src": "/icons/cart.png", "sizes": "96x96" }]
    }
  ]
}
Opsi display yang tersedia:

"standalone":  Terlihat seperti native app — tidak ada browser UI (address bar, dll)
               → Pengalaman paling "native" — direkomendasikan untuk kebanyakan PWA

"minimal-ui":  Address bar minimal (hanya back/forward + URL)
               → Compromise antara web dan native

"browser":     Tampilan browser normal — sama seperti tab biasa
               → Tidak ada bedanya dengan website biasa, kurang direkomendasikan

"fullscreen":  Penuh layar, tidak ada UI browser sama sekali
               → Untuk game atau aplikasi media yang butuh layar penuh

Offline Experience — UX yang Bermakna #

Offline support bukan hanya tentang “tidak crash saat offline” — ini tentang memberikan pengalaman yang bermakna bahkan tanpa koneksi.

Level offline experience dari yang paling buruk ke yang paling baik:

Level 0 — Crash / Error:
  "No internet connection" dari browser
  → Tidak ada penanganan sama sekali

Level 1 — Offline Page:
  Halaman "Kamu sedang offline" yang generik
  → Lebih baik dari error browser, tapi tidak informatif
  → Minimal yang harus diimplementasikan

Level 2 — Konten yang Di-cache:
  User bisa melihat konten yang sudah pernah dikunjungi
  → "Kamu melihat versi yang tersimpan dari 2 jam lalu"
  → Sangat berguna untuk aplikasi yang read-heavy

Level 3 — Full Offline Functionality:
  User bisa membuat perubahan saat offline
  → Perubahan disinkronisasi saat koneksi kembali (Background Sync)
  → Pengalaman yang paling mendekati native app
// Halaman offline yang informatif — /offline.html atau komponen offline
function OfflinePage() {
  const [lastOnline, setLastOnline] = useState(null)
  const [isBackOnline, setIsBackOnline] = useState(false)

  useEffect(() => {
    const handleOnline = () => {
      setIsBackOnline(true)
      setTimeout(() => window.location.reload(), 1500)
    }
    window.addEventListener('online', handleOnline)
    return () => window.removeEventListener('online', handleOnline)
  }, [])

  if (isBackOnline) {
    return <div>Koneksi kembali! Memuat ulang halaman...</div>
  }

  return (
    <div className="offline-page">
      <WifiOffIcon />
      <h1>Tidak Ada Koneksi Internet</h1>
      <p>Kamu sedang offline. Beberapa fitur mungkin tidak tersedia.</p>
      {lastOnline && (
        <p>Konten terakhir diperbarui: {formatRelativeTime(lastOnline)}</p>
      )}
      <button onClick={() => window.location.reload()}>
        Coba Lagi
      </button>
    </div>
  )
}

Push Notification #

Push Notification memungkinkan server mengirim pesan ke user bahkan saat browser tertutup. Ini adalah salah satu fitur yang paling membedakan PWA dari website biasa.

sequenceDiagram
    participant U as User
    participant App as PWA
    participant SW as Service Worker
    participant PS as Push Server (VAPID)
    participant Backend as Backend

    U->>App: Klik "Izinkan Notifikasi"
    App->>PS: Subscribe (kirim VAPID public key)
    PS-->>App: PushSubscription object
    App->>Backend: Simpan subscription (endpoint + keys)

    Note over Backend: Saat ada event yang perlu notifikasi
    Backend->>PS: Kirim pesan dengan private key
    PS->>SW: Deliver push message ke browser
    SW->>U: Tampilkan notifikasi (bahkan jika browser ditutup!)

    U->>SW: Klik notifikasi
    SW->>App: Buka halaman yang relevan
// Meminta izin notifikasi
async function requestNotificationPermission() {
  const permission = await Notification.requestPermission()

  if (permission !== 'granted') {
    console.log('Notifikasi ditolak user')
    return null
  }

  // Subscribe ke push service
  const registration = await navigator.serviceWorker.ready
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,  // wajib true — tidak boleh silent push
    applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
  })

  // Kirim subscription ke backend untuk disimpan
  await fetch('/api/push/subscribe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(subscription)
  })

  return subscription
}

// Di Service Worker — handle push event
self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? {}

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icons/icon-192.png',
      badge: '/icons/badge-72.png',
      data: { url: data.actionUrl },
      actions: [
        { action: 'open', title: 'Buka', icon: '/icons/open.png' },
        { action: 'dismiss', title: 'Tutup' }
      ]
    })
  )
})

// Handle klik notifikasi
self.addEventListener('notificationclick', (event) => {
  event.notification.close()
  if (event.action === 'open' || !event.action) {
    event.waitUntil(
      clients.openWindow(event.notification.data.url)
    )
  }
})
Jangan spam user dengan push notification. Notification permission adalah salah satu permission yang paling sering diblock oleh user karena situs yang abuse fitur ini. Best practice: tanya izin hanya setelah user melakukan aksi yang menunjukkan intent (misalnya klik “aktifkan notifikasi” yang user-initiated), bukan segera setelah halaman dibuka. User yang di-spam akan revoke permission dan tidak bisa diminta lagi tanpa user action yang eksplisit.

Background Sync — Offline Actions #

Background Sync memungkinkan aksi yang dilakukan saat offline untuk disinkronisasi saat koneksi kembali, tanpa user perlu membuka aplikasi lagi.

// Di aplikasi — daftarkan sync saat submit form saat offline
async function submitOrder(orderData) {
  if (!navigator.onLine) {
    // Simpan ke IndexedDB untuk di-sync nanti
    await saveToIndexedDB('pending-orders', orderData)

    // Daftarkan background sync
    const registration = await navigator.serviceWorker.ready
    await registration.sync.register('sync-orders')

    showToast('Order disimpan. Akan dikirim saat online.')
    return
  }

  // Online — submit langsung
  await fetch('/api/orders', {
    method: 'POST',
    body: JSON.stringify(orderData)
  })
}

// Di Service Worker — handle sync event
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-orders') {
    event.waitUntil(syncPendingOrders())
  }
})

async function syncPendingOrders() {
  const pendingOrders = await getFromIndexedDB('pending-orders')

  for (const order of pendingOrders) {
    try {
      await fetch('/api/orders', {
        method: 'POST',
        body: JSON.stringify(order)
      })
      await deleteFromIndexedDB('pending-orders', order.id)
    } catch (error) {
      // Jika masih gagal, sync akan dicoba lagi nanti
      console.error('Sync failed for order:', order.id)
      throw error  // re-throw agar browser tahu sync belum berhasil
    }
  }
}

Perbedaan PWA dan SPA #

Ini adalah kebingungan yang sangat umum — banyak yang mengira keduanya sama.

SPA (Single Page Application):
  → Arsitektur rendering — satu HTML document, client-side routing
  → Tidak punya offline support secara default
  → Tidak bisa diinstall
  → Tidak punya push notification
  → Semua SPA adalah web app biasa dari perspektif OS

PWA (Progressive Web Application):
  → Kumpulan teknologi untuk membuat web lebih "native"
  → Bisa offline (via Service Worker + Cache)
  → Bisa diinstall di home screen / desktop
  → Bisa kirim push notification
  → Terlihat seperti app di OS user

Hubungan keduanya:
  → SPA bisa menjadi PWA dengan menambahkan Service Worker + Manifest + HTTPS
  → PWA bisa menggunakan arsitektur SPA atau MPA atau SSR
  → SPA tidak otomatis menjadi PWA
  → PWA bukan pengganti SPA — mereka adalah lapisan yang berbeda

Analogi:
  SPA = jenis bangunan (rumah satu lantai)
  PWA = fitur bangunan (AC, alarm, panel surya)
  → Rumah satu lantai bisa punya atau tidak punya fitur-fitur itu
  → Fitur-fitur itu bisa dipasang di rumah satu atau dua lantai

Installability — Add to Home Screen #

Agar PWA bisa diinstall, semua kriteria berikut harus terpenuhi:

Kriteria installability di Chrome:

✓ Served via HTTPS (atau localhost untuk development)
✓ Web App Manifest ada dengan field yang diperlukan:
  - name atau short_name
  - icons (minimal 192x192 dan 512x512)
  - start_url
  - display: 'standalone', 'fullscreen', atau 'minimal-ui'
✓ Service Worker terdaftar dengan fetch event handler
✓ Belum diinstall sebelumnya di perangkat ini

Saat kriteria terpenuhi:
  → Chrome menampilkan prompt "Install App" atau banner
  → User bisa memilih install

Best practice untuk install prompt:
  // Tangkap dan simpan event, tampilkan saat user menunjukkan intent
  let deferredPrompt = null

  window.addEventListener('beforeinstallprompt', (e) => {
    e.preventDefault()  // cegah prompt otomatis
    deferredPrompt = e  // simpan untuk nanti
  })

  // Tampilkan saat user klik tombol "Install App"
  async function promptInstall() {
    if (!deferredPrompt) return
    deferredPrompt.prompt()
    const { outcome } = await deferredPrompt.userChoice
    if (outcome === 'accepted') {
      // User install — bisa track analytics
    }
    deferredPrompt = null
  }

Anti-Pattern PWA yang Harus Dihindari #

Cache Semua Request Tanpa Strategi #

// ✗ Anti-pattern: cache semua request
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request).then((r) => {
        const clone = r.clone()
        caches.open('all-cache').then((c) => c.put(event.request, clone))
        return r
      })
    })
  )
})
// Masalah:
// → Cache token autentikasi yang expired
// → Cache response error (404, 500) yang akan terus dikembalikan
// → Cache data sensitif yang tidak seharusnya tersimpan
// → Cache tumbuh tanpa batas

// ✓ Solusi: Gunakan strategi yang tepat per jenis request, dengan filter yang jelas
self.addEventListener('fetch', (event) => {
  const { request } = event
  
  // Jangan cache POST, PUT, DELETE
  if (request.method !== 'GET') return
  
  // Jangan cache request autentikasi
  if (request.url.includes('/api/auth')) return
  
  // Pilih strategi berdasarkan jenis
  if (isStaticAsset(request.url)) {
    event.respondWith(cacheFirst(request))
  } else if (request.url.includes('/api/')) {
    event.respondWith(networkFirst(request))
  }
})

Cache yang Tidak Di-versioning #

// ✗ Anti-pattern: nama cache statis tanpa versi
const CACHE = 'my-cache'
// Saat deploy baru, asset lama masih di-cache
// User tidak mendapat update sampai cache kadaluarsa sendiri

// ✓ Solusi: Version cache dan cleanup di activate
const CACHE_VERSION = 'v2'  // increment setiap deploy
const STATIC_CACHE = `static-${CACHE_VERSION}`

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((names) =>
      Promise.all(
        names
          .filter((name) => !name.endsWith(CACHE_VERSION))
          .map((name) => caches.delete(name))  // hapus versi lama
      )
    )
  )
})

Meminta Push Permission Segera #

// ✗ Anti-pattern: minta permission langsung saat halaman dibuka
document.addEventListener('DOMContentLoaded', () => {
  Notification.requestPermission()  // 70-80% user langsung block ini!
})

// ✓ Solusi: Tanya hanya setelah user menunjukkan interest
function ProfilePage() {
  return (
    <div>
      <h2>Preferensi Notifikasi</h2>
      <p>Dapatkan update tentang order dan promosi terbaru</p>
      <button onClick={requestNotificationPermission}>
        Aktifkan Notifikasi
      </button>
    </div>
  )
}

Kapan Menggunakan PWA #

PWA sangat tepat untuk:
  ✓ Aplikasi yang sering digunakan secara berkala
     (news app, todo, productivity tools)
  ✓ Aplikasi yang butuh akses offline
     (aplikasi di area dengan koneksi tidak stabil)
  ✓ Ingin reach mobile user tanpa biaya publish ke App Store
     (terutama di emerging markets di mana storage terbatas)
  ✓ Aplikasi yang butuh push notification
     (reminder, update order, berita breaking)
  ✓ Aplikasi e-commerce yang butuh performa tinggi dan engagement

PWA kurang tepat atau tidak ada manfaatnya jika:
  ✗ Konten yang dikunjungi sekali saja (landing page marketing)
  ✗ Butuh akses ke hardware yang tidak tersedia di web
     (Bluetooth peripheral, USB, NFC yang kompleks)
  ✗ Tim belum punya pengalaman dengan Service Worker
     → Service Worker yang salah implementasi lebih buruk dari tidak ada
  ✗ Aplikasi yang koneksinya selalu stabil dan cepat
     → Overhead Service Worker tidak sepadan

Checklist PWA #

SERVICE WORKER:
  □ Service Worker terdaftar di halaman utama
  □ Install event meng-cache asset kritis (app shell)
  □ Activate event membersihkan cache lama
  □ Fetch event menggunakan strategi yang tepat per jenis request
  □ Request POST/PUT/DELETE tidak di-cache
  □ Cache di-versioning (cleanup saat deploy baru)
  □ Fallback ke offline page jika tidak ada cache

WEB APP MANIFEST:
  □ manifest.json ada dan valid
  □ Ikon tersedia minimal 192x192 dan 512x512
  □ display: 'standalone'
  □ theme_color dan background_color dikonfigurasi
  □ start_url dikonfigurasi dengan benar
  □ <link rel="manifest"> ada di setiap halaman

HTTPS:
  □ Seluruh aplikasi served via HTTPS
  □ HTTP redirect ke HTTPS
  □ HSTS header dikonfigurasi

INSTALLABILITY:
  □ Semua kriteria installability terpenuhi (dapat di-verify dengan Lighthouse)
  □ Install prompt ditampilkan pada waktu yang tepat (user-initiated)
  □ Handling untuk setelah user install (post-install UX)

OFFLINE:
  □ App shell tersedia offline
  □ Offline page ada dan informatif
  □ Cache di-update saat online (stale-while-revalidate atau background update)
  □ Perubahan saat offline di-sync saat kembali online (Background Sync)

PUSH NOTIFICATION:
  □ Permission diminta pada waktu yang tepat (tidak saat page load)
  □ Notification konten relevan dan bermakna
  □ Notification klik mengarah ke halaman yang relevan
  □ Unsubscribe tersedia dan mudah ditemukan

TESTING:
  □ Ditest di Chrome DevTools (Application tab)
  □ Lighthouse PWA audit score dicheck
  □ Ditest dalam kondisi offline (Network throttling)
  □ Ditest di device mobile yang berbeda

Ringkasan #

  • PWA bukan satu teknologi — ia adalah tiga pilar — Service Worker (offline, caching, push), Web App Manifest (installability), dan HTTPS (security, prerequisite untuk Service Worker).
  • Pilih strategi caching yang tepat per jenis konten — Cache First untuk static asset dengan content hash, Network First untuk API data, Stale While Revalidate untuk konten yang boleh sedikit outdated.
  • Jangan cache semua request tanpa filter — POST/PUT/DELETE tidak boleh di-cache. Request autentikasi tidak boleh di-cache. Response error tidak boleh di-cache. Cache yang salah lebih buruk dari tidak ada cache.
  • Versioning cache wajib — tanpa versioning, user akan terus mendapat asset lama setelah deploy baru. Increment versi di nama cache dan cleanup di activate event.
  • Offline experience harus bermakna — minimal ada halaman offline yang informatif. Lebih baik lagi: tampilkan konten yang sudah di-cache dengan indikasi kapan terakhir diupdate.
  • Jangan spam push notification — minta permission hanya saat user menunjukkan interest, bukan saat halaman pertama dibuka. User yang di-spam akan block semua notifikasi dan tidak bisa diminta lagi.
  • Background Sync untuk aksi offline — simpan aksi ke IndexedDB saat offline, sinkronisasi saat koneksi kembali via Background Sync API, bahkan tanpa user membuka app.
  • Install prompt harus user-initiated — tangkap event beforeinstallprompt, simpan, dan tampilkan hanya saat user menunjukkan intent untuk menginstall (bukan secara otomatis).
  • SPA dan PWA adalah konsep yang berbeda — SPA adalah arsitektur rendering, PWA adalah kumpulan kapabilitas. SPA bisa menjadi PWA, tapi SPA tidak otomatis PWA.
  • Gunakan Lighthouse untuk validasi — Chrome DevTools Lighthouse memberikan PWA audit yang komprehensif: mana kriteria yang terpenuhi dan apa yang perlu diperbaiki.

← Sebelumnya: SPA   Berikutnya: Suspense →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact