필사 모드: PWA & Service Workers 2026 — Workbox 7 / vite-pwa / Capacitor / TWA / iOS Web Push / Storage Buckets Deep Dive
EnglishPrologue — "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.**
| Area | 2019 status | 2026 status |
| --------------------- | --------------------------------- | ------------------------------------------------------------------ |
| iOS Web Push | No | Yes (iOS 16.4+) |
| iOS Home Screen WebApp| Limited but worked | Works, after one removal-and-restore crisis |
| Push API | Chrome / Firefox / Edge only | All major browsers |
| File System | Download only | File System Access API (Chrome/Edge) |
| Storage | Single origin pool | Storage Buckets allow partitioning |
| Page transitions | Smooth in SPAs only | View Transitions cross-document also works for MPAs |
| Prerender | Only rel=prefetch | Speculation Rules |
| Framework integration | Hand-rolled | vite-pwa / next-pwa / Astro PWA |
| Store distribution | Hard or impossible | PWABuilder / 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.
| Date | Event |
| ------------- | ---------------------------------------------------- |
| Mar 2023 | iOS 16.4 — Web Push officially supported |
| Early Feb 2024| iOS 17.4 beta — EU PWA removal announced |
| Mid Feb 2024 | Mozilla / OWA / EU Commission protests begin |
| Feb 27, 2024 | Apple announces restoration |
| Mar 5, 2024 | iOS 17.4 stable — PWAs restored (WebKit only) |
| 2025 | iOS 18 series — PWA stable, Web Push expansion |
| 2026 current | iOS 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
// 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.
| Strategy | When |
| -------------------- | --------------------------------------------------------------- |
| CacheFirst | Images, fonts, hashed JS — nearly immutable static assets |
| NetworkFirst | API, news feeds — freshness matters |
| StaleWhileRevalidate | HTML pages, CSS — show instantly, refresh in background |
| CacheOnly | Offline-only assets |
| NetworkOnly | POST, 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
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.
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
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.
| Plugin | Purpose |
| ----------------------------------- | ----------------------------------- |
| @capacitor/camera | Camera, gallery |
| @capacitor/filesystem | File system |
| @capacitor/push-notifications | APN / FCM |
| @capacitor/geolocation | Location |
| @capacitor/preferences | Key-value store |
| @capacitor/share | OS share sheet |
| @capacitor/biometric | Face ID / fingerprint |
| @capacitor/in-app-browser | In-app browser |
| @capacitor/local-notifications | Local notifications |
| @capacitor/app | App lifecycle |
The call pattern resembles standard JS APIs closely.
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)
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.**
{
"prerender": [
{ "where": { "href_matches": "/product/*" }, "eagerness": "moderate" }
],
"prefetch": [
{ "where": { "href_matches": "/category/*" } }
]
}
`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
| Category | PWA Fit | Why |
| ---------------------- | ------------- | ------------------------------------------------------------- |
| Media / Blog | Excellent | SW cache, offline reading, Web Push |
| E-commerce | Good | Catalog prerender, View Transitions, light install |
| SaaS Dashboard | Excellent | Desktop PWA install, shortcuts, fullscreen |
| Chat / Messenger | OK | Push works, but native alerts and VoIP are limited |
| Casual Games | Good | Canvas, WebGL, WebGPU, Storage Buckets |
| 3A / Real-time Games | Poor | Graphics ceiling, memory ceiling, App Store marketing |
| Tools (audio / video) | Excellent | File System Access, Web MIDI, Audio API |
| Payments / Fintech | OK | Security model, OS auth, biometrics — partial fit |
| Conference / Event app | Excellent | Short-term use, avoids install friction |
| Photo / Drawing tools | Good | File 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
- [W3C — Service Workers 1](https://www.w3.org/TR/service-workers/)
- [W3C — Web App Manifest](https://www.w3.org/TR/appmanifest/)
- [W3C — Push API](https://www.w3.org/TR/push-api/)
- [MDN — Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)
- [MDN — Progressive Web Apps](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps)
- [Workbox official](https://developer.chrome.com/docs/workbox)
- [vite-pwa official](https://vite-pwa-org.netlify.app/)
- [@ducanh2912/next-pwa GitHub](https://github.com/DuCanhGH/next-pwa)
- [Serwist (next-pwa successor)](https://serwist.pages.dev/)
- [@vite-pwa/astro](https://vite-pwa-org.netlify.app/frameworks/astro)
- [Capacitor official](https://capacitorjs.com/)
- [PWABuilder official](https://www.pwabuilder.com/)
- [Bubblewrap CLI (TWA)](https://github.com/GoogleChromeLabs/bubblewrap)
- [Android — Trusted Web Activities](https://developer.chrome.com/docs/android/trusted-web-activity)
- [WebKit Blog — Web Push for Web Apps on iOS](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/)
- [Open Web Advocacy — PWA in iOS 17.4 timeline](https://open-web-advocacy.org/)
- [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API)
- [browser-fs-access (polyfill)](https://github.com/GoogleChromeLabs/browser-fs-access)
- [Storage Buckets API](https://developer.chrome.com/docs/web-platform/storage-buckets)
- [View Transitions API — cross-document](https://developer.chrome.com/docs/web-platform/view-transitions/cross-document)
- [Speculation Rules API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API)
- [Web Bluetooth API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API)
- [WebUSB API](https://developer.mozilla.org/en-US/docs/Web/API/WebUSB_API)
- [Web Share API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API)
- [Background Sync API](https://developer.mozilla.org/en-US/docs/Web/API/Background_Synchronization_API)
- [Periodic Background Sync](https://developer.chrome.com/docs/capabilities/periodic-background-sync)
- [Chrome Status — PWA features](https://chromestatus.com/features#pwa)
- [Web.dev — Learn PWA](https://web.dev/learn/pwa/)
- [Mercari Engineering Blog](https://engineering.mercari.com/en/)
- [LINE Engineering Blog](https://engineering.linecorp.com/en)
현재 단락 (1/568)
Around 2019, the most common refrain at conference stages in Korea and Japan was "PWA is dead." The ...