Skip to content
Published on

PWA & Service Workers 2026 — Workbox 7 / vite-pwa / Capacitor / TWA / iOS Web Push / Storage Buckets Deep Dive

Authors

Prologue — "PWA is dead" is the second-wrongest prediction of 2026

Around 2019, the most common refrain at conference stages in Korea and Japan was "PWA is dead." The reasons were simple — iOS blocked Web Push, the Home Screen UX was terrible, Service Worker was a debugging hell, and React Native plus Flutter were taking over the market.

By May 2026, that prediction has been proven wrong a second time.

  • iOS 16.4 (March 2023) finally enabled Web Push as a first-class feature. It goes through APNs, but it is standard Push API.
  • iOS 17.4 (February 2024) — facing the EU's DMA (Digital Markets Act) enforcement, Apple briefly removed Home Screen Web Apps in the EU, and then, after a unified backlash from developers, the EU Commission, and the press, restored them within three weeks.
  • Workbox 7 (Google, 2023~2025) became the standard for cache strategy, leaving almost no reason to write Service Worker by hand.
  • vite-pwa / next-pwa / Astro PWA provide framework integration.
  • Capacitor 6 / PWABuilder 3 / TWA finally answer the "I still want to be in the store" desire.
  • Storage Buckets API (Chrome 122, February 2024) treats storage as partitioned units.
  • View Transitions cross-document (Chrome 126, June 2024) makes even MPAs feel like SPAs.
  • Speculation Rules prerender the next page so clicks feel like 0 ms.

This piece maps that landscape — the political drama, code, and Korean/Japanese case studies — in one breath.


Chapter 1 · The 2026 PWA Map — Revival or Another Plateau

First, a picture. The word "PWA" actually points to five things combined.

[Web Page]
   |
   v
1. Manifest (web app manifest)  -- icon, name, theme, display:standalone
   |
   v
2. Service Worker               -- background thread, cache, intercept
   |
   v
3. HTTPS                        -- prerequisite for Service Worker
   |
   v
4. Installable                  -- add to home screen, beforeinstallprompt
   |
   v
5. Capable APIs                 -- Push, Notification, Bluetooth, FS Access, ...

All five = a PWA. Three = an "Installable Web." Two = just a website.

What changed in 2026 is that 1, 2, and 3 are barely different from 2019. The big drama happened in 4 and 5.

Area2019 status2026 status
iOS Web PushNoYes (iOS 16.4+)
iOS Home Screen WebAppLimited but workedWorks, after one removal-and-restore crisis
Push APIChrome / Firefox / Edge onlyAll major browsers
File SystemDownload onlyFile System Access API (Chrome/Edge)
StorageSingle origin poolStorage Buckets allow partitioning
Page transitionsSmooth in SPAs onlyView Transitions cross-document also works for MPAs
PrerenderOnly rel=prefetchSpeculation Rules
Framework integrationHand-rolledvite-pwa / next-pwa / Astro PWA
Store distributionHard or impossiblePWABuilder / Capacitor / TWA

Revival? Half-yes. The era of "App Store or nothing" has ended. But React Native, Flutter, and Capacitor still own the spaces they took. PWA in 2026 has become "one of the options," and that is the healthiest position it has ever held.


Chapter 2 · iOS Web Push (iOS 16.4+) — Finally Here

The single disqualifier that killed PWA momentum was the absence of iOS Web Push. iOS 16.4 (March 2023) ended that.

The conditions are explicit.

  1. The page must be added to the Home Screen (it does not work from the browser tab).
  2. The user must explicitly call Notification.requestPermission() and receive granted.
  3. The site must be HTTPS.
  4. The manifest must declare display: standalone or display: fullscreen.

The code is plain standard Push API.

// 1) Request permission — must be inside a user gesture (click)
button.addEventListener('click', async () => {
  const permission = await Notification.requestPermission()
  if (permission !== 'granted') return

  const reg = await navigator.serviceWorker.ready
  const subscription = await reg.pushManager.subscribe({
    userVisibleOnly: true, // iOS requires this to be true
    applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
  })

  // Send subscription to server
  await fetch('/api/push/subscribe', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(subscription),
  })
})

function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
  const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
  const raw = atob(base64)
  return Uint8Array.from([...raw].map((c) => c.charCodeAt(0)))
}

The Service Worker side is also stock-standard.

// sw.js
self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? { title: 'New', body: '' }
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icons/icon-192.png',
      badge: '/icons/badge-72.png',
      data: { url: data.url },
    })
  )
})

self.addEventListener('notificationclick', (event) => {
  event.notification.close()
  event.waitUntil(clients.openWindow(event.notification.data.url))
})

iOS gotchas.

  • Routed through APNs. Apple translates the standard Web Push protocol into APN messages. Your backend code is unchanged, but average delivery latency is 2~5 seconds slower than Android.
  • userVisibleOnly: true required. Silent push (invisible to the user) is impossible on iOS.
  • Permission must come from a user gesture. Triggering it on page-load is auto-rejected.
  • Notification permission UX is buried deep in OS Settings. If a user denies once, they have to manually reverse it in the system Settings app.

In practice you get 95% of Android Chrome's quality. That is exactly why Kakao, LINE, and Mercari all adopted it.


Chapter 3 · iOS 17.4 PWA Removal → Restoration (Feb 2024) — An EU Political Drama

In February 2024, Apple added a shocking change to iOS 17.4 beta — EU users' Home Screen Web Apps would behave like ordinary browser shortcuts. No fullscreen, no Service Worker, no Push, no separate storage.

The reason was DMA (Digital Markets Act). The EU's DMA forces "gatekeeper" Apple to allow third-party browser engines. Apple's argument was "if non-Safari engines can host PWAs, we'd need to build a security and privacy model for every engine, and that's not on our roadmap."

The industry reaction.

  • Mozilla, Vivaldi, and Open Web Advocacy filed formal letters with the EU Commission.
  • Developer communities in the UK, EU, and US erupted, calling it "a DMA workaround."
  • The EU Commission officially announced "we will investigate."

Three weeks later, Apple reversed the decision. In the iOS 17.4 stable release (March 5, 2024), PWAs continued to work in the EU. With one caveat — "WebKit only" — PWAs from non-WebKit engines are still impossible.

DateEvent
Mar 2023iOS 16.4 — Web Push officially supported
Early Feb 2024iOS 17.4 beta — EU PWA removal announced
Mid Feb 2024Mozilla / OWA / EU Commission protests begin
Feb 27, 2024Apple announces restoration
Mar 5, 2024iOS 17.4 stable — PWAs restored (WebKit only)
2025iOS 18 series — PWA stable, Web Push expansion
2026 currentiOS 18.x — PWA + Web Push are baseline behavior

The drama taught the PWA camp two things.

  1. Politically fragile. A single platform decision can erase half the global user base.
  2. Still survives. If enough developers get angry, decisions get reversed.

Chapter 4 · Service Worker 2026 — What Actually Changed

The Service Worker spec itself has barely moved since 2014. What changed are the surrounding ecosystem and best practices.

The core lifecycle is worth remembering.

register
   |
   v
parse install   --(skipWaiting?)--> activate
   |                                    |
   |                                    v
   |                                  fetch / push / sync / message
   |                                    |
   |                                    v
   |                                  ... new version install
   |                                    |
   |                                    v
   |                              waiting (controlled by old)
   |                                    |
   +-- update found ---------------------+

Best practices in 2026.

  1. Be careful with skipWaiting(). Activating immediately can let new SW collide with already-open clients that talk to the old SW. The standard pattern is to show a toast saying "an update is available, reload" to the user.
  2. Keep fetch handlers short. Heavy synchronous work in the handler slows every network request.
  3. Set timeouts yourself. fetch() has no default timeout. Use AbortController.
  4. Version explicitly. Keep a CACHE_VERSION constant and clear stale caches on activate.

The smallest Service Worker.

// sw.js
const CACHE_VERSION = 'v2026-05-16'
const PRECACHE = `precache-${CACHE_VERSION}`
const RUNTIME = `runtime-${CACHE_VERSION}`

const PRECACHE_URLS = ['/', '/offline.html', '/styles.css', '/app.js']

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches
      .open(PRECACHE)
      .then((cache) => cache.addAll(PRECACHE_URLS))
      .then(() => self.skipWaiting())
  )
})

self.addEventListener('activate', (event) => {
  const valid = new Set([PRECACHE, RUNTIME])
  event.waitUntil(
    caches
      .keys()
      .then((keys) => Promise.all(keys.filter((k) => !valid.has(k)).map((k) => caches.delete(k))))
      .then(() => self.clients.claim())
  )
})

self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url)
  if (event.request.method !== 'GET') return
  if (url.origin !== self.location.origin) return

  // Network first, fall back to cache
  event.respondWith(
    fetch(event.request)
      .then((res) => {
        const copy = res.clone()
        caches.open(RUNTIME).then((cache) => cache.put(event.request, copy))
        return res
      })
      .catch(() => caches.match(event.request).then((r) => r || caches.match('/offline.html')))
  )
})

If this feels long, that's normal. In 2026, almost everyone writes this with Workbox.


Chapter 5 · Workbox 7 — The Standard Library

Workbox is Google's Service Worker library. Workbox 7 (released January 2024, stabilized through 7.x) is the de facto standard.

Core modules.

  • workbox-routing — routing. registerRoute(matcher, handler).
  • workbox-strategies — five cache strategies. CacheFirst, NetworkFirst, StaleWhileRevalidate, CacheOnly, NetworkOnly.
  • workbox-precaching — build-time manifest-driven precache.
  • workbox-expiration — TTL and LRU.
  • workbox-background-sync — wrapper around Background Sync API.
  • workbox-broadcast-update — notifies the main thread when a new response arrives.
  • workbox-window — main-thread helpers for managing the SW.

A five-line Workbox SW.

// sw.js
import { precacheAndRoute } from 'workbox-precaching'
import { registerRoute } from 'workbox-routing'
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'

// Manifest injected at build time (auto-populated by vite-pwa / next-pwa)
precacheAndRoute(self.__WB_MANIFEST)

// Images — cache first, 30 days, max 60 entries
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 })],
  })
)

// API — network first, 3-second timeout, fall back to cache
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({ cacheName: 'api', networkTimeoutSeconds: 3 })
)

// Pages — Stale-While-Revalidate (return cache immediately, refresh in background)
registerRoute(
  ({ request }) => request.mode === 'navigate',
  new StaleWhileRevalidate({ cacheName: 'pages' })
)

Those five lines bake in a week's worth of thinking. Hand-rolled, it would be over 100 lines.

Knowing when to use each strategy is half of PWA design.

StrategyWhen
CacheFirstImages, fonts, hashed JS — nearly immutable static assets
NetworkFirstAPI, news feeds — freshness matters
StaleWhileRevalidateHTML pages, CSS — show instantly, refresh in background
CacheOnlyOffline-only assets
NetworkOnlyPOST, payments, logs — requests you must never cache

Chapter 6 · vite-pwa / next-pwa / Astro PWA — Framework Integration

The era of touching sw.js by hand is over. In 2026, the build system handles it.

vite-pwa

@vite-pwa/vite (the fastest-growing PWA plugin of 2024~2025) is the de facto Vite standard.

// vite.config.ts
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'

export default defineConfig({
  plugins: [
    VitePWA({
      registerType: 'autoUpdate', // auto-update on new SW
      injectRegister: 'auto',
      workbox: {
        globPatterns: ['**/*.{js,css,html,svg,png,ico,woff2}'],
        runtimeCaching: [
          {
            urlPattern: /^https:\/\/api\.example\.com\//,
            handler: 'NetworkFirst',
            options: { cacheName: 'api', networkTimeoutSeconds: 3 },
          },
        ],
      },
      manifest: {
        name: 'My App',
        short_name: 'MyApp',
        theme_color: '#000000',
        background_color: '#ffffff',
        display: 'standalone',
        icons: [
          { src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
          { src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' },
          {
            src: '/icons/maskable-512.png',
            sizes: '512x512',
            type: 'image/png',
            purpose: 'maskable',
          },
        ],
      },
    }),
  ],
})

That is the whole config. vite build produces sw.js, manifest.webmanifest, and injects the manifest automatically.

React-side registration.

import { registerSW } from 'virtual:pwa-register'

const updateSW = registerSW({
  onNeedRefresh() {
    if (confirm('A new version is available. Reload?')) updateSW(true)
  },
  onOfflineReady() {
    console.log('Offline ready')
  },
})

next-pwa

The Next.js story is more complicated. App Router (13.4+) introduced RSC, which collides with SW in surprising ways, and the original next-pwa (shadowwalker, 2018~) maintainer stepped back in late 2024. The community migrated to @ducanh2912/next-pwa. As of May 2026, the most-used packages are @ducanh2912/next-pwa and @serwist/next.

// next.config.js
const withPWA = require('@ducanh2912/next-pwa').default({
  dest: 'public',
  cacheOnFrontEndNav: true, // cache App Router navigation
  aggressiveFrontEndNavCaching: true,
  reloadOnOnline: true,
  swcMinify: true,
  workboxOptions: {
    disableDevLogs: true,
  },
})

module.exports = withPWA({
  reactStrictMode: true,
})

App Router gotcha — caching RSC payload (?_rsc=) requests freezes content forever in a stale state. Recent @ducanh2912/next-pwa versions avoid this automatically, but if you write your own Workbox routes, route any RSC payload pattern to NetworkOnly.

Astro PWA

@vite-pwa/astro — Astro produces static output, which works extremely well with PWA.

// astro.config.mjs
import AstroPWA from '@vite-pwa/astro'

export default {
  integrations: [
    AstroPWA({
      registerType: 'autoUpdate',
      manifest: { /* ... */ },
      workbox: { /* ... */ },
    }),
  ],
}

Works best for blogs, docs, and landing pages. The fewer dynamic routes, the simpler the PWA — a general rule with this as one instance.


Chapter 7 · Capacitor — From the Ionic Team

Capacitor is the answer to "I want to wrap my web app in a native shell and ship to the App Store." Built by the Ionic team. As of May 2026, Capacitor 6 (major release April 2024, stabilized through 6.x) is the stable line.

# Initialize a Capacitor project (bolted onto an existing web app)
npm i @capacitor/core @capacitor/cli
npx cap init my-app com.example.myapp --web-dir=dist

npm i @capacitor/ios @capacitor/android
npx cap add ios
npx cap add android

# Build and sync
npm run build
npx cap sync
npx cap open ios   # opens Xcode
npx cap open android  # opens Android Studio

The Capacitor plugin ecosystem.

PluginPurpose
@capacitor/cameraCamera, gallery
@capacitor/filesystemFile system
@capacitor/push-notificationsAPN / FCM
@capacitor/geolocationLocation
@capacitor/preferencesKey-value store
@capacitor/shareOS share sheet
@capacitor/biometricFace ID / fingerprint
@capacitor/in-app-browserIn-app browser
@capacitor/local-notificationsLocal notifications
@capacitor/appApp lifecycle

The call pattern resembles standard JS APIs closely.

import { Camera, CameraResultType } from '@capacitor/camera'

const photo = await Camera.getPhoto({
  quality: 90,
  allowEditing: false,
  resultType: CameraResultType.Uri,
})
imgElement.src = photo.webPath

Capacitor pros.

  • One web codebase covers iOS, Android, and web.
  • Native APIs only when you need them via plugins — everything else is plain web.
  • Unlike React Native, rendering is a WebView, so all web technologies work as-is.

Cons.

  • Marginally slower than React Native due to the WebView layer.
  • WKWebView on iOS, Chrome Custom Tabs on Android — subtle differences become debugging traps.
  • App Store review — "this is just a webview wrapper" is a common rejection reason. Use at least one native feature.

Chapter 8 · PWABuilder (Microsoft) — Store Wrapping

Microsoft's PWABuilder is a wizard that builds packages for every store from just one PWA URL. Plug a URL into https://www.pwabuilder.com and you get scores plus downloads for each store.

  • Microsoft Store (MSIX) — Windows
  • Google Play (Android, TWA-based).aab file
  • App Store (iOS, Capacitor-based) — Xcode project
  • Meta Quest Store — VR app
  • Samsung Galaxy Store — Android variant

There is also a CLI.

npm i -g @pwabuilder/cli

# Generate an Android package (TWA)
pwabuilder package --type android --url https://example.com

# Generate an iOS package
pwabuilder package --type ios --url https://example.com

# Generate a Windows package
pwabuilder package --type windows --url https://example.com

Drop the generated Android package into Play Console and you are done. You only have to sign it yourself.

The real value of PWABuilder.

  • Manifest score. PWABuilder grades manifest, SW, security, icons, and metadata. "Getting to 100" is itself a great checklist.
  • Quality gate. Google Play already accepts PWA packages, but a Microsoft-verified package brings additional trust.

Chapter 9 · TWA (Trusted Web Activities) — Android Chrome

TWA extends Chrome Custom Tabs. "Take my domain's PWA, render it fullscreen, hide Chrome UI, and make it feel like a native app."

How it works.

  1. Bake the PWA URL into an Android app (.apk or .aab).
  2. Android renders that URL through Chrome, but hides the Chrome UI.
  3. Digital Asset Links prove the domain trusts this app — that's what "trusted" means.

Place assetlinks.json at /.well-known/assetlinks.json on your domain.

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.myapp",
      "sha256_cert_fingerprints": [
        "AA:BB:CC:..."
      ]
    }
  }
]

Bubblewrap CLI (Google's official tool) makes a TWA package in one line.

npm i -g @bubblewrap/cli

bubblewrap init --manifest https://example.com/manifest.webmanifest
bubblewrap build
# → produces app-release-signed.aab

TWA pros.

  • It is the real Chrome engine — 100% compatibility.
  • Service Worker, Push, File System Access, every Chrome feature works.
  • Updates are immediate — refresh the web, no app re-release required.

Gotchas.

  • No equivalent on iOS. On iOS you have to use Capacitor or the PWABuilder iOS path.
  • If the user disables Chrome, TWA does not run (falls back to system WebView).

Chapter 10 · File System Access API — Chrome/Edge Only

An API that lets web apps read and write the user's local files directly. Photoshop on Web, Figma, and VS Code Web all use it.

// Open a file
const [handle] = await window.showOpenFilePicker({
  types: [{ description: 'Text', accept: { 'text/plain': ['.txt', '.md'] } }],
})
const file = await handle.getFile()
const text = await file.text()

// Write back to the same file (no extra permission)
const writable = await handle.createWritable()
await writable.write(text + '\nedited')
await writable.close()

// Whole directory
const dirHandle = await window.showDirectoryPicker()
for await (const [name, entry] of dirHandle.entries()) {
  console.log(name, entry.kind) // 'file' or 'directory'
}

Support as of May 2026.

  • Chrome / Edge / Opera: shipped
  • Firefox: not supported (Mozilla has voiced security-model concerns)
  • Safari: not supported

Fallback pattern — substitute <input type="file"> plus a Blob URL. Figma and VS Code Web run in fallback mode on Firefox.

// Polyfill library — browser-fs-access (Google)
import { fileOpen, fileSave } from 'browser-fs-access'

const blob = await fileOpen({ extensions: ['.txt'] })
// Works everywhere. File System Access on Chrome, input/Blob fallback elsewhere.

Chapter 11 · More Capable APIs — Push / Background Sync / Periodic Sync / Bluetooth / USB

Push API + Notifications

Already covered in Chapter 2. The point — in 2026, this works in every major browser, including iOS.

Background Sync API

User actions performed offline (form submits, message sends) automatically retry when the network returns.

// Main page
const reg = await navigator.serviceWorker.ready
await reg.sync.register('send-message')

// sw.js
self.addEventListener('sync', (event) => {
  if (event.tag === 'send-message') {
    event.waitUntil(flushPendingMessages())
  }
})

Support: Chrome / Edge / Opera / Samsung Internet. Firefox and Safari not supported.

Periodic Background Sync

Refresh data in the background on a periodic basis (minimum 24 hours).

const status = await navigator.permissions.query({ name: 'periodic-background-sync' })
if (status.state === 'granted') {
  await reg.periodicSync.register('refresh-feed', { minInterval: 24 * 60 * 60 * 1000 })
}

Support: Chrome and Edge only. Works only in installed PWAs.

Web Bluetooth API

Talk directly to Bluetooth LE devices.

const device = await navigator.bluetooth.requestDevice({
  filters: [{ services: ['heart_rate'] }],
})
const server = await device.gatt.connect()
const service = await server.getPrimaryService('heart_rate')
const characteristic = await service.getCharacteristic('heart_rate_measurement')
characteristic.addEventListener('characteristicvaluechanged', (event) => {
  console.log('HR:', event.target.value.getUint8(1))
})
await characteristic.startNotifications()

Support: Chrome / Edge / Opera. Firefox and Safari not supported.

WebUSB API

Direct USB device access. Arduino flashing, payment terminals, industrial equipment control.

const device = await navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
await device.open()
await device.selectConfiguration(1)
await device.claimInterface(0)
const result = await device.transferIn(1, 64)

Support: Chrome / Edge / Opera. Firefox and Safari not supported.

Web Share API

Trigger the OS share sheet. As of 2026 it works in every major browser (including iOS Safari).

await navigator.share({
  title: 'My PWA',
  text: 'Take a look',
  url: 'https://example.com',
})

Chapter 12 · Storage Buckets API (Chrome 122, Feb 2024) — Partitioned Storage

Traditional web storage (IndexedDB, Cache, localStorage) all lived in a single origin pool. Hitting "Clear cookies and site data" wiped everything together, and quota was managed as one lump.

Storage Buckets slice that into buckets.

// Create a bucket
const userDataBucket = await navigator.storageBuckets.open('user-data', {
  durability: 'strict', // never lose this
  persisted: true, // persist permission
  quota: 100 * 1024 * 1024, // 100MB
})

// Cache / IDB / SW per bucket
const cache = await userDataBucket.caches.open('user-files')
const idb = await userDataBucket.indexedDB.open('users', 1)
const sw = await userDataBucket.getDirectory() // OPFS

// Temporary bucket — wiped together when the user forgets a password
const tempBucket = await navigator.storageBuckets.open('temp-uploads', {
  durability: 'relaxed',
  persisted: false,
})

// Delete just this bucket — not the whole origin
await navigator.storageBuckets.delete('temp-uploads')

Why it matters.

  • Selective deletion — game save files persist while caches are cleared.
  • Selective persistence — mark only payment receipts with persisted: true.
  • Per-bucket quota — useful for multi-tenant pages within a single domain (email, calendar, notes all under one origin, for example).

Support: Chrome / Edge 122+ (Feb 2024). Firefox and Safari are working on it.


Chapter 13 · View Transitions cross-document (Chrome 126, Jun 2024) — SPA-like MPA

An API that smooths page transitions. It started as a SPA-only feature (same-document routing) but Chrome 126 (June 2024) added cross-document support (between different pages).

Enabling it is one line.

@view-transition {
  navigation: auto;
}

That's all. The browser automatically fades transitions between pages on the same origin.

Advanced — give the same view-transition-name to elements on two pages and that element morphs across the navigation.

/* product-list.html */
.product-card img {
  view-transition-name: product-image;
}

/* product-detail.html */
.product-detail img {
  view-transition-name: product-image;
}

Clicking a list thumbnail smoothly enlarges that image into the detail page's hero. The same motion you've seen in Instagram or Mercari apps.

Why this is part of the PWA revival — once "MPA (server-rendered) can feel as smooth as SPA" is proven, half of the UX advantage SPAs claimed collapses. PWA is, at heart, a model that fits MPA just as well.

Support: Chrome 126+, Edge, Opera. Safari and Firefox are in progress.


Chapter 14 · Speculation Rules — Prerender Future Pages

A beefed-up version of <link rel="prefetch">. It receives HTML, JS, and CSS of the page the user is likely to navigate to next, and prerenders it in a background tab.

<script type="speculationrules">
{
  "prerender": [
    { "where": { "href_matches": "/product/*" }, "eagerness": "moderate" }
  ],
  "prefetch": [
    { "where": { "href_matches": "/category/*" } }
  ]
}
</script>

eagerness options.

  • immediate — prerender as soon as discovered.
  • eager — when the user hovers a link.
  • moderate — when hover exceeds 200 ms.
  • conservative — right after mousedown.

Navigating to a prerendered page shows it instantly (LCP 0 ms). That's the trick behind why next-card clicks on Kakao, LINE, and Mercari catalog pages feel "0 ms."

Gotchas.

  • A prerendered page actually runs SW and JS in the background. Analytics will fire false hits. The standard pattern is to check document.prerendering and defer reporting while prerendering.
  • Mobile allows only one concurrent prerender. Don't be greedy.

Support: Chrome / Edge. Safari and Firefox don't support it but ignore the script gracefully, so it's safe to ship without a polyfill.


Chapter 15 · Korean / Japanese PWA Case Studies — Kakao, Naver, Mercari, Pixiv

Korea

  • Kakao Mobile Web — Kakao Talk channel pages and parts of the Kakao Pay flow lean heavily on PWA patterns (installable manifest, SW cache, Push). Parts of the m.kakao.com domain support add-to-home-screen in standalone mode.
  • Naver Webtoon — the mobile web version (comic.naver.com) uses SW to cache images. Open one episode, and the next page is already prefetched.
  • Coupang — the mobile web uses Service Worker for image and JS caching. The App Banner pushes hard toward the native app.
  • Daangn Market — limited web presence, but mobile-web item detail pages use SW caching.

The reason Korean PWA adoption is comparatively weak is clear. Because they live inside super-app webviews like KakaoTalk and Naver App, the user-facing boundary of "a separate app" blurs. You cannot install a PWA from inside the Kakao in-app browser.

Japan

  • Mercari — the mobile web runs as a PWA. jp.mercari.com supports manifest, SW, and Push. App Store credibility is high in Japan, so native apps dominate, but Mercari has a surprisingly large web-only user segment (light buyers).
  • Pixiv Sketch — a live-drawing platform. Runs as a PWA in mobile browsers. While drawing, work is preserved without breaks even offline.
  • Yahoo! Japan — many pages (especially news) use aggressive SW caching. That's why page transitions feel so fast.
  • Pixiv (main illustration site) — pixiv.net itself is not a full PWA, but it uses Web Push (like and comment notifications).

PWA works surprisingly well in Japan, and a key reason is that with iOS having over 60% market share, iOS Web Push being unlocked had a huge impact. LINE has its own messenger infrastructure and doesn't really need Web Push, but for smaller services, just having access to a push channel was significant.


Chapter 16 · Who Should Choose PWA — A Decision Matrix

CategoryPWA FitWhy
Media / BlogExcellentSW cache, offline reading, Web Push
E-commerceGoodCatalog prerender, View Transitions, light install
SaaS DashboardExcellentDesktop PWA install, shortcuts, fullscreen
Chat / MessengerOKPush works, but native alerts and VoIP are limited
Casual GamesGoodCanvas, WebGL, WebGPU, Storage Buckets
3A / Real-time GamesPoorGraphics ceiling, memory ceiling, App Store marketing
Tools (audio / video)ExcellentFile System Access, Web MIDI, Audio API
Payments / FintechOKSecurity model, OS auth, biometrics — partial fit
Conference / Event appExcellentShort-term use, avoids install friction
Photo / Drawing toolsGoodFile System Access, Canvas, OPFS

Checklist — 5 questions before picking PWA.

  1. Will users use this daily or occasionally? Daily often favors native. Occasional favors PWA.
  2. Is push notification mandatory? iOS supports it now. But for "guaranteed delivery timing" you still want native.
  3. Do you need device APIs like camera / Bluetooth / USB? Chrome / Android — PWA is enough. iOS — use Capacitor.
  4. Is app-store discoverability central to your marketing? Then PWABuilder + TWA, or Capacitor.
  5. Is offline truly required, or "nice to have"? "Truly required" almost always points to PWA.

A one-line heuristic — mobile-first + media/tool + occasional use = PWA. Daily + deep device use = native or Capacitor.


Epilogue — The Second Revival

PWA in May 2026 is the same word as PWA in 2019, but not the same tech stack.

  • The spec is nearly identical — Service Worker, Manifest, Cache API.
  • The tools are completely different — from hand-written SW to Workbox 7, framework integration, build-time manifests.
  • Permissions exploded — iOS Web Push, File System Access, Bluetooth, USB, Storage Buckets.
  • UX no longer mimics SPA — View Transitions cross-document and Speculation Rules let MPAs mimic SPAs.
  • Distribution paths multiplied — PWABuilder, TWA, Capacitor.

The biggest change is that the "PWA vs. native" dichotomy is dead. The 2026 answer is "the lightest path the user wants." For some users that's a PWA, for others it's a Capacitor shell, for others it's true native. And all three can branch from the same web codebase.

The iOS political drama showed once that platforms are fickle. But web standards don't stop. The next round in 2027 — Storage Foundation, Bluetooth on iOS, RTC over Web Push — will be another revival.

"PWA did not die. We just held the memorial too early."

— PWA & Service Workers 2026, fin.


References