VueアプリをPWA(Progressive Web Apps)対応すると、オフラインでも動く、ホーム画面に追加できる、再訪時が高速といったメリットが得られます。本記事では、Vue 3 + Viteを前提に、最短の導入手順(vite-plugin-pwa)と、細かく制御したい場合の手書きService Workerの2パターンで解説します。さらに、キャッシュ戦略、オフラインフォールバック、アップデート検知まで実装例を示します。
最短ルート:vite-plugin-pwaでPWA対応
npm i -D vite-plugin-pwa
vite.config.tsにPWAプラグインを設定します。これだけでビルド時にマニフェストとService Workerが生成され、静的アセットは自動キャッシュされます。
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
vue(),
VitePWA({
registerType: 'autoUpdate', // SW更新を自動適用
includeAssets: ['favicon.svg', 'robots.txt'],
manifest: {
name: 'My Vue PWA',
short_name: 'VuePWA',
start_url: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#42b883',
icons: [
{ src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },
{ src: 'pwa-maskable.png', sizes: '512x512', type: 'image/png', purpose: 'maskable any' }
]
},
workbox: {
navigateFallback: '/offline.html', // オフライン時のフォールバック
runtimeCaching: [
{
urlPattern: ({ request }) => request.destination === 'image',
handler: 'CacheFirst',
options: {
cacheName: 'images',
expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 7 } // 7日
}
},
{
urlPattern: /^https:\/\/api\.example\.com\//,
handler: 'StaleWhileRevalidate',
options: { cacheName: 'api-cache', networkTimeoutSeconds: 3 }
}
]
}
})
]
})
main.tsで登録します(プラグインが自動でSWを生成・登録)。
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
// PWA:自動登録モード(vite-plugin-pwaが仕込みを行う)
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').catch(console.error)
})
}
オフライン用ページ(public/offline.html)を用意しておくと、ネットワークが切れた時に自動で表示されます。
<!-- public/offline.html -->
<!doctype html>
<html lang="ja">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>オフライン</title></head>
<body><h1>オフラインです</h1><p>接続が回復したら自動で復旧します。</p></body>
</html>
より細かく制御:手書きService Worker + Workbox
キャッシュ戦略を自前で設計したい場合は、Workboxを使ってpublic/sw.js
を作成します。
// public/sw.js
/* global workbox */
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.6.0/workbox-sw.js')
// 事前キャッシュ(プレキャッシュ)
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST || [])
// ルーティング:ページはNetworkFirst(オフライン時はfallback)
workbox.routing.registerRoute(
({ request }) => request.mode === 'navigate',
new workbox.strategies.NetworkFirst({
cacheName: 'pages',
networkTimeoutSeconds: 4,
plugins: [new workbox.broadcastUpdate.BroadcastUpdatePlugin()]
})
)
// 画像はCacheFirst
workbox.routing.registerRoute(
({ request }) => request.destination === 'image',
new workbox.strategies.CacheFirst({
cacheName: 'images',
plugins: [
new workbox.expiration.ExpirationPlugin({ maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 14 })
]
})
)
// APIはStaleWhileRevalidate
workbox.routing.registerRoute(
({ url }) => url.origin === 'https://api.example.com',
new workbox.strategies.StaleWhileRevalidate({ cacheName: 'api' })
)
// オフラインフォールバック
const FALLBACK_URL = '/offline.html'
workbox.routing.setCatchHandler(async ({ event }) => {
if (event.request.destination === 'document') return caches.match(FALLBACK_URL)
return Response.error()
})
// 更新の即時適用(skipWaiting + clientsClaim)
self.addEventListener('install', () => self.skipWaiting())
self.addEventListener('activate', (e) => {
e.waitUntil(self.clients.claim())
})
index.htmlでSWを登録します。
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
})
}
</script>
キャッシュ戦略の設計指針
静的アセット(JS/CSS/フォント/ロゴ)はCacheFirstで長期キャッシュ+ハッシュ付きファイル名。HTMLはNetworkFirstで最新優先、オフライン時はキャッシュを返す。APIはStaleWhileRevalidateで体感を軽くしつつ背後更新。画像はCacheFirst+期限で肥大化を抑制。この基本の組み合わせでほとんどのアプリはカバーできます。
アップデートの扱い(更新検知と再読み込み)
SW更新は即時適用か、次回アクセスで適用かを選べます。即時適用する場合、skipWaiting()
とclientsClaim()
を使い、UI上で「新しいバージョンがあります。更新しますか?」を出して再読み込みします。
// src/pwaUpdate.ts(簡易実装)
export function listenPwaUpdate() {
if (!('serviceWorker' in navigator)) return
navigator.serviceWorker.addEventListener('controllerchange', () => {
// 新SWが制御を開始したタイミングでリロード
window.location.reload()
})
}
開発・検証のコツ
Chrome DevToolsのApplication > Service Workersで「Update on reload」を有効化するとデバッグが捗ります。Offlineチェックで通信を切断し、オフラインフォールバックが効くかを確認しましょう。キャッシュはApplication > Cache Storageで確認・削除できます。
よくある落とし穴と対策
- APIのPOST/PUT/DELETEは基本的にキャッシュしない(読み取り専用のGETのみ対象)。
- 古いキャッシュが残る場合はキャッシュ名にバージョンを付けて段階的に破棄。
- SPAのルーティングで404になるときは、navigateFallback(例:
/index.html
or/offline.html
)を設定。 - iOS SafariはPWA周りの挙動が独特。ホーム追加や容量制限を考慮し、manifestとアイコンを適切に準備。
まとめ
VueアプリのPWA化は、vite-plugin-pwaで最短導入しつつ、必要に応じてWorkboxで戦略を微調整するのが実用的です。静的アセットはCacheFirst、HTMLはNetworkFirst、APIはStaleWhileRevalidate、オフライン時はフォールバック。この定石を押さえれば、オフラインでも壊れにくく、再訪が高速なプロダクション品質の体験を提供できます。