progressive-web-app について
プログレッシブウェブアプリの構築を扱うスキルで、マニフェスト、サービスワーカー、キャッシュ戦略、オフライン対応を説明します。インストール可能なWebアプリや不安定なネットワークへの対応を目指す開発者向けです。
主な機能
- 01Web App Manifestの設定
- 02サービスワーカーの登録と管理
- 03アプリシェルの事前キャッシュ
- 04リソース別キャッシュ戦略の選択
- 05オフラインページとキャッシュ更新の処理
- 06ブラウザーごとのインストール差異の説明
活用シーン
- 01Webアプリへのオフラインアクセス追加
- 02モバイルサイトのインストール対応
- 03静的・動的リソースのキャッシュ設計
- 04サービスワーカーのデプロイ問題の調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ sickn33/ agentic-awesome-skills --skill progressive-web-appインストール後、Claude Code で/progressive-web-appを入力して読み込まれたか確認します。
詳細
プログレッシブウェブアプリ(PWA)
概要
プログレッシブウェブアプリは、最新のブラウザー機能を活用して、信頼性の低いネットワークでも高速で信頼性が高く、インストール可能なエクスペリエンスを提供するウェブアプリケーションです。一般的な構成要素は次のとおりです(インストール要件はブラウザーによって異なります)。
- HTTPS — 本番環境でサービスワーカーを登録するには必須です(開発時は localhost の場合を除きます)。
- ウェブアプリマニフェスト(
manifest.json)— アプリをインストール可能にし、デバイスのホーム画面での外観を定義します。 - サービスワーカー(
sw.js)— ネットワークリクエストを傍受し、キャッシュを管理して、オフライン機能を実現するバックグラウンドスクリプトです。
このスキルを使用する場面
- ユーザーがウェブアプリをオフラインや信頼性の低いネットワークでも動作させたい場合に使用します。
- ユーザーがアプリをホーム画面にインストールできるようにする、モバイルファーストのウェブプロジェクトを構築する場合に使用します。
- ユーザーがキャッシュ戦略、サービスワーカー、ウェブアプリのパフォーマンスや回復性の向上について尋ねた場合に使用します。
- ユーザーが Workbox、ウェブアプリマニフェスト、バックグラウンド同期、ウェブのプッシュ通知について言及した場合に使用します。
- ユーザーが「自分のウェブサイトをアプリのようにインストールできますか?」や「サイトをオフラインで動作させるにはどうすればよいですか?」と尋ねた場合は、PWA という言葉を使っていなくても使用します。
成果物チェックリスト
この例の構成では以下のファイルを使用します。これらのファイル名は、すべての PWA に共通する要件ではありません。
-
index.html— マニフェストをリンクし、サービスワーカーを登録します -
manifest.json— アプリのメタデータとアイコン一式を定義します -
sw.js— インストール、アクティベート、フェッチの各ハンドラーを備えたサービスワーカーです -
app.js— サービスワーカーの登録とインストールプロンプトの処理を含む、アプリのメインロジックです -
offline.html— オフライン時にナビゲーションが失敗した場合に表示されるフォールバックページです(この例のcache.addAllリストでは必要ですが、PWA 標準の要件ではありません)
ステップ 1:ウェブアプリマニフェスト(manifest.json)
インストール時のアプリの外観を定義します。<link rel="manifest"> を使って <head> からリンクする必要があります。
{
"name": "My Awesome PWA",
"short_name": "MyPWA",
"description": "A fast, offline-capable Progressive Web App.",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#ffffff",
"theme_color": "#0055ff",
"icons": [
{
"src": "/assets/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/assets/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/assets/screenshots/desktop.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
}
]
}
主なフィールド:
display:standaloneはブラウザー UI を非表示にします。minimal-uiは最小限のコントロールを表示します。browserは標準のタブ表示です。- アイコンに
purpose: "maskable"を指定すると、Android でアダプティブアイコンを使えます(セーフゾーンが重要です。コンテンツは中央の 80% に収めてください)。 screenshotsは任意ですが、デスクトップ版 Chrome の強化されたインストールダイアログには必須です。
ステップ 2:HTML シェル(index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Awesome PWA</title>
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#0055ff">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="MyPWA">
<link rel="apple-touch-icon" href="/assets/icons/icon-192x192.png">
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div id="app">
<header><h1>My PWA</h1></header>
<main id="content">Loading...</main>
<button id="install-btn" hidden>Install App</button>
</div>
<script src="/app.js"></script>
</body>
</html>
ステップ 3:サービスワーカーの登録とインストールプロンプト(app.js)
// ─── Service Worker Registration ───────────────────────────────────────────
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('[App] SW registered, scope:', registration.scope);
} catch (err) {
console.error('[App] SW registration failed:', err);
}
});
}
// ─── Install Prompt (Add to Home Screen) ───────────────────────────────────
let deferredPrompt;
const installBtn = document.getElementById('install-btn'); // may be null if omitted
// Capture the browser's install prompt — it fires before the browser's own UI
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault(); // Stop automatic mini-infobar on mobile
deferredPrompt = e;
if (installBtn) installBtn.hidden = false; // Show your custom install button
});
if (installBtn) {
installBtn.addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log('[App] Install outcome:', outcome);
deferredPrompt = null;
installBtn.hidden = true;
});
}
// Fires when the app is installed (via browser or your button)
window.addEventListener('appinstalled', () => {
console.log('[App] PWA installed successfully');
if (installBtn) installBtn.hidden = true;
});
ステップ 4:サービスワーカー(sw.js)
キャッシュのバージョン管理(重要 — デプロイ時には必ずインクリメント)
const CACHE_VERSION = 'v1';
const CACHE_PREFIX = 'my-pwa-'; // Unique to this app and deployment scope.
const STATIC_CACHE = `${CACHE_PREFIX}static-${CACHE_VERSION}`;
const DYNAMIC_CACHE = `${CACHE_PREFIX}dynamic-${CACHE_VERSION}`;
// Files to pre-cache during install (the "App Shell")
const APP_SHELL = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/assets/icons/icon-192x192.png',
'/offline.html', // Fallback page shown when network is unavailable
];
手書きの戦略は、説明のための概略です。古いページと新しいワーカーの互換性問題を避けるため、skipWaiting/clients.claim の前に更新を調整してください。キャッシュのサイズと保持期間に上限を設け、ほかのアプリのキャッシュを保持し、バックグラウンドでの再検証を拒否時の処理とともに event.waitUntil に関連付けてください。
インストール — アプリシェルを事前キャッシュする
self.addEventListener('install', (event) => {
console.log('[SW] Installing...');
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => {
console.log('[SW] Pre-caching app shell');
return cache.addAll(APP_SHELL);
})
);
// Activate immediately without waiting for old SW to die
self.skipWaiting();
});
アクティベート — 古いキャッシュをクリーンアップする
self.addEventListener('activate', (event) => {
console.log('[SW] Activating...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name.startsWith(CACHE_PREFIX) && name !== STATIC_CACHE && name !== DYNAMIC_CACHE)
.map((name) => {
console.log('[SW] Deleting old cache:', name);
return caches.delete(name);
})
);
})
);
// Take control of all pages immediately
self.clients.claim();
});
フェッチ — キャッシュ戦略
リソースの種類ごとに適切な戦略を選択してください。
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Only handle GET requests from our own origin
if (request.method !== 'GET' || url.origin !== location.origin) return;
// Explicit public resources only. Never cache authenticated/user-specific
// HTML or API responses by URL; cache contents can survive logout/account changes.
if (!APP_SHELL.includes(url.pathname)) return;
// Strategy A: Cache-First (for static assets — fast, tolerates stale)
if (url.pathname.match(/\.(css|js|png|jpg|svg|woff2)$/)) {
event.respondWith(cacheFirst(request));
return;
}
// Strategy B: Network-First (for HTML pages — fresh, falls back to cache)
if (request.headers.get('Accept')?.includes('text/html')) {
event.respondWith(networkFirst(request));
return;
}
// API caching is intentionally omitted; default to network-only for private data.
});
// ─── Strategy Implementations ──────────────────────────────────────────────
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;
try {
const response = await fetch(request);
const cache = await caches.open(STATIC_CACHE);
if (response.ok && response.type !== "opaque") await cache.put(request, response.clone());
return response;
} catch {
// Nothing useful to fall back to for assets
return new Response('Asset unavailable offline', { status: 503 });
}
}
async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open(DYNAMIC_CACHE);
if (response.ok && response.type !== "opaque") await cache.put(request, response.clone());
return response;
} catch {
const cached = await caches.match(request);
return cached || await caches.match('/offline.html') || new Response("Offline", { status: 503 });
}
}
async function staleWhileRevalidate(request) {
const cache = await caches.open(DYNAMIC_CACHE);
const cached = await cache.match(request);
const fetchPromise = fetch(request).then(async (response) => {
if (response.ok && response.type !== "opaque") await cache.put(request, response.clone());
return response;
});
return cached || fetchPromise;
}
エッジケースとプラットフォームに関する注意点
iOS / Safari の注意点
- Safari はマニフェストとサービスワーカーをサポートしていますが、
beforeinstallpromptはサポートしていません。ユーザーは共有 →「ホーム画面に追加」メニューから手動でインストールする必要があります。 - iOS と適切に連携するには、
index.html(上記参照)に示されているapple-mobile-web-app-*メタタグを使用してください。 - Safari は、非アクティブな状態が約 7 日続くと、サービスワーカーのキャッシュを消去することがあります(インテリジェント・トラッキング防止機能)。
HTTPS の要件
- サービスワーカーを登録できるのは
https://オリジンのみです。開発時はhttp://localhostだけが例外です。 - カスタムホスト名を使ってローカルで HTTPS が必要な場合は、
mkcertやngrokなどのツールを使用してください。
デプロイ時のキャッシュ無効化
- 新しいアセットをデプロイする際は、必ず
sw.jsのCACHE_VERSIONをインクリメントしてください。これにより、アクティベート時に古いキャッシュが消去され、ユーザーは新しいファイルを取得できます。 - よくある方法は、ビルドツール(例:Vite、Webpack)を使ってバージョンを自動的に挿入することです。
不透明なレスポンス(クロスオリジンリクエスト)
- クロスオリジンの no-cors レスポンスは不透明になることがあり、その場合、ステータスは 0 で、本文を読み取れません。CORS が有効なレスポンスは、不透明である必要はありません。ステータス 0 を成功と解釈しないでください。
- クロスオリジンリソースには
staleWhileRevalidateを優先するか、この処理を安全に行う Workbox のようなライブラリを使用してください。
Workbox(オプション:本番環境向けの近道)
本番環境のアプリでは、戦略を独自に実装する代わりに、Workbox(Google の PWA ライブラリ)の使用を検討してください。エッジケース、キャッシュの有効期限、バージョン管理を自動的に処理します。
// With Workbox (via CDN for simplicity — use npm + bundler in production)
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js');
const { registerRoute } = workbox.routing;
const { CacheFirst, NetworkFirst, StaleWhileRevalidate } = workbox.strategies;
const { precacheAndRoute } = workbox.precaching;
precacheAndRoute(self.__WB_MANIFEST || []); // Injected by build plugin
registerRoute(({ request }) => request.destination === 'image', new CacheFirst());
registerRoute(({ request }) => request.mode === 'navigate', new NetworkFirst());
registerRoute(({ request }) => request.destination === 'script', new StaleWhileRevalidate());
リリース前のチェックリスト
- サイトが HTTPS 経由で配信されている
-
manifest.jsonにname、short_name、start_url、display、icons(192 + 512)が設定されている - アイコンに
purpose: "any maskable"が設定されている -
sw.jsが DevTools → Application → Service Workers でエラーなく登録される - DevTools でネットワークを「Offline」に制限したときに、アプリシェルがキャッシュから読み込まれる
- オフライン時にナビゲーションが失敗すると、
offline.htmlのフォールバックがキャッシュから配信される - 現在のブラウザーにおけるインストール可能性、オフライン対応、アクセシビリティ、パフォーマンスのチェックに合格している。廃止された Lighthouse の PWA カテゴリには依存しないでください
- iOS Safari(手動インストール手順)と Android Chrome(インストールプロンプト)でテスト済みである
制限事項
- タスクが上記の説明範囲に明確に合致する場合にのみ、このスキルを使用してください。
- 出力を、環境固有の検証、テスト、または専門家によるレビューの代わりとして扱わないでください。
- 必要な入力、権限、安全上の境界、または成功基準が不足している場合は、作業を中断して確認を求めてください。