2026年10月8日にGoogle web.devのLearn PWA本文を再確認した。PWAの導入はmanifestを置くだけではなく、Webの基礎品質、service workerのライフサイクル、Cache Storage、ネットワーク失敗時の画面、更新とロールバック、インストール後の通知・権限を一つの運用として設計する。キャッシュしたHTMLやデータが古くなる条件を定義し、オンライン時・オフライン時・更新途中・ストレージ不足を実機で検証する。
PWAは、Webの配信方法を使いながら、インストールや通信断への対応などを組み合わせる設計です。manifestを置くだけで、すべての端末で通知やオフライン注文が動くわけではありません。まず「通信が切れたとき何を表示し、何を未完了のまま残すか」を決めましょう。
30秒で要点:PWAの導入判断
- manifestは名前や起動先などの情報、Service Workerは取得やキャッシュを扱う仕組み。
- 公開情報の閲覧と、注文・個人情報の送信を分ける。未接続を完了表示にしない。
- 更新待機、対象ブラウザー、許可の有無を確認してから導入範囲を広げる。
Webアプリに何を足すか
表が収まらない場合は、横にスクロールできます
| 要素 | 担当すること | それだけでは保証しないこと |
|---|---|---|
| Web App Manifest | 名前・アイコン・起動先・表示方法の希望 | 全環境でのインストール |
| Service Worker | 対象リクエストの処理、キャッシュ方針 | あらゆるページのオフライン動作 |
| Pushと通知 | 許可・購読・配信・表示の連携 | 全利用者への即時配信 |
| 業務サーバー | 注文などの受入・認可・処理結果 | 通信断の画面だけでの完了判定 |
ネイティブアプリとの比較も、配布方法だけで優劣を決めません。必要な端末機能、更新手順、保存容量、支援技術、運用体制を対象環境で確認します。「低コスト」「必ず高速」は見積りや計測の結果として扱います。manifestの仕様
最小教材:公開案内と通信断ページだけを保存する
以下は同一オリジンの小さい静的サイト用です。publicから配信する/offline.html、/help.html、/styles/main.css、192/512pxのPNGを先に用意してください。架空店舗の公開案内に限定し、会員情報・注文APIは含めません。
1. sw.jsのキャッシュ対象を絞る
最初のブロックと後述のnetworkPageを、同じsw.jsへ一度ずつ置きます。fetchハンドラーを重ねて登録しないでください。
const CACHE = 'pwa-lesson-v1';
const PUBLIC = ['/offline.html', '/help.html', '/styles/main.css',
'/icons/icon-192.png', '/icons/icon-512.png'];
self.addEventListener('install', event => {
event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(PUBLIC)));
});
self.addEventListener('activate', event => {
event.waitUntil(caches.keys().then(keys => Promise.all(
keys.filter(k => k.startsWith('pwa-lesson-') && k !== CACHE)
.map(k => caches.delete(k))
)));
});
self.addEventListener('fetch', event => {
const r = event.request;
const u = new URL(r.url);
if (r.method !== 'GET' || u.origin !== self.location.origin) return;
if (r.mode === 'navigate') {
event.respondWith(networkPage(r));
} else if (!u.search && PUBLIC.includes(u.pathname)) {
event.respondWith(caches.open(CACHE).then(async cache =>
(await cache.match(r)) || fetch(r)));
}
});
addAll対象の一つでも取得できなければインストールを成功扱いしません。削除も自分の教材用prefixに限定します。更新されたworkerが待機し、以前のページを閉じるまで切り替わらない場合があります。ここでは強制的なskipWaiting()を入れていません。更新通知や未保存入力の扱いを決めてから追加します。Service Workerのライフサイクル
2. manifestとHTMLをつなぐ
ファイル名はmanifest.webmanifestです。アイコンは宣言だけでなく、実ファイルの寸法と応答を確認します。
{
"id": "/",
"name": "架空店舗の案内",
"short_name": "店舗案内",
"start_url": "/help.html",
"scope": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#17475e",
"icons": [
{"src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png"},
{"src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png"}
]
}
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#17475e">
3. 登録とオフライン時の表示を分ける
次は通常のページ側のスクリプトです。HTTPSまたは開発用localhostなど、Service Workerが利用できる環境で試します。登録成功は、そのページが既にworkerに制御されているという意味ではありません。
if ('serviceWorker' in navigator && window.isSecureContext) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(() => console.info('登録済み。制御状態と更新待機は別に確認'))
.catch(() => console.error('登録できませんでした'));
});
}
これはsw.jsに加える関数です。サーバーのHTTPエラーはそのまま返し、通信自体が失敗した場合にだけ公開の通信断ページを返します。
async function networkPage(request) {
try { return await fetch(request); }
catch {
const cache = await caches.open(CACHE);
const offline = await cache.match('/offline.html');
return offline || new Response('接続できません。送信は完了していません。',
{ status: 503, headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
}
}
offline.htmlには「接続を確認してください。注文は完了していません」と表示し、ログイン済み情報を埋め込みません。この教材は注文POSTを保留・再送するキューを実装していません。後から追加する場合は、重複送信防止、取消、期限、サーバー側の処理状況照会も必要です。
4. Pushの表示部分だけを切り出す
同じworkerへ追加できる表示例です。未知の通知データを既定文に置換し、クリック先は同一オリジンの公開案内へ限定します。許可を求める画面、購読情報の保存、送信サーバーは別工程です。
self.addEventListener('push', event => {
let data = {};
try { data = event.data ? event.data.json() : {}; } catch {}
const title = typeof data?.title === 'string' ? data.title.slice(0, 80) : '店舗からのお知らせ';
const body = typeof data?.body === 'string' ? data.body.slice(0, 160) : '接続して案内を確認してください';
event.waitUntil(self.registration.showNotification(title,
{ body, icon: '/icons/icon-192.png', data: { url: '/help.html' } }));
});
self.addEventListener('notificationclick', event => {
event.notification.close();
event.waitUntil(clients.openWindow(new URL('/help.html', self.location.origin).href));
});
通知を拒否した人も通常の案内を読めるようにします。Push対応やインストール条件はブラウザーで違うため、Next.jsのPWAガイドでも許可・購読・配信を分けて確認してください。本稿で外部Pushを送った実績はありません。
React・Vue・Next.jsへの組込み
以下の三つの足場作成コマンドは、別々の新しい練習用フォルダーで使う選択肢です。通信・パッケージ追加を伴うため、本稿の隔離教材検証では実行していません。作成結果のファイル配置を確認し、前節のworkerとmanifestを自分で接続します。
Reactの足場と登録関数
Create React Appは2025年2月14日に新規アプリ向けの非推奨が告知されました。旧CRAのPWAテンプレートを現在の標準として案内しません。Reactの告知
npm create vite@latest pwa-react-lesson -- --template react-ts
Reactの学習用ビルドツール案内を基にした足場です。次はJSモジュールとしてページ側から一度呼ぶ登録関数で、CRA専用のserviceWorkerRegistrationを前提にしません。
export async function registerPwa() {
if (!('serviceWorker' in navigator) || !window.isSecureContext) return null;
return navigator.serviceWorker.register('/sw.js');
}
登録エラーは呼出し元で扱います。SSR中には呼ばず、ブラウザー側の初期化で使ってください。
Vueの足場
VueのQuick Startで案内されるcreate-vueを使う例です。旧vue add pwaの成功を前提にせず、生成した公開フォルダーへ教材を配置します。
npm create vue@latest
Next.jsの足場とworker応答設定
公式create-next-appによる作成例です。以前のnext-pwa追加と設定の役割は、公式の作成手順とworker配信設定へ分けました。第三者プラグインの現行互換性は保証しません。
npx create-next-app@latest pwa-next-lesson
次はnext.config.mjsの一例です。既存設定がある場合は統合し、上書きで失わないでください。これだけではworkerの登録もキャッシュも作られません。
const nextConfig = {
async headers() {
return [{ source: '/sw.js', headers: [
{ key: 'Content-Type', value: 'application/javascript; charset=utf-8' },
{ key: 'Cache-Control', value: 'no-cache, no-store, must-revalidate' }
] }];
}
};
export default nextConfig;
架空店舗で採用範囲を決める
例えば案内ページが200KB、通信断ページが5KBなら、その二つの本文は合計205KBです。CSSやアイコン、HTTPヘッダーなどを含まない仮想計算で、実キャッシュ容量の測定値ではありません。
表が収まらない場合は、横にスクロールできます
| 試す場面 | 期待すること | 採用を止める条件 |
|---|---|---|
| 初回オンライン | 必須5資産を取得できる | 一つでも404などでinstall失敗 |
| 次回の通信断 | 公開案内または通信断案内を読める | 個人情報や完了していない注文が表示される |
| worker更新 | 待機と切替の方針を説明できる | 入力中の内容を黙って失う |
| 通知拒否 | 案内自体は使える | 通知許可が閲覧の強制条件になる |
隔離検証で確かめるのはキャッシュ分岐や異常時の処理です。実ブラウザーのインストール、保存量、通信断、更新、通知許可は対象端末で別に試します。企業事例の改善率を自社の予測値へ流用せず、閲覧成功率や二重注文の有無など、目的に合う観測項目を先に決めてください。