PWA実装の基礎 - WebアプリをPWA化する手順
PWA(Progressive Web Apps)は、Webアプリをネイティブアプリのように動作させる技術です。オフライン対応、ホーム画面への追加、プッシュ通知など、ユーザー体験を大幅に向上させることができます。
この記事では、実際にMechaToraのツール群をPWA化した経験をもとに、PWAの基礎から実装手順まで、初心者でも分かる形で解説します。Service Workerの基本、manifest.jsonの設定、キャッシュ戦略の選び方など、実践的な内容をお届けします。
PWAとは何か
PWAの3つの柱
PWAは以下の3つの特徴を持つWebアプリケーションです:
- Progressive(プログレッシブ):どんなブラウザでも基本機能が動作
- Responsive(レスポンシブ):あらゆる画面サイズに対応
- Connectivity independent(接続に依存しない):オフラインでも動作
ネイティブアプリとの違い
私がPWA化を決めた理由は、以下のメリットがあったためです:
- 開発コストが低い:HTML/CSS/JavaScriptで開発可能
- 配布が簡単:App Store/Google Play不要、URLだけで配布
- 更新が即時反映:アプリ審査なし、即座に全ユーザーに配信
- クロスプラットフォーム:1つのコードでiOS/Android両対応
MechaToraで運用している15個のツールは、全てPWA化することで、ユーザーがホーム画面に追加してネイティブアプリのように使えるようになりました。
PWA実装の準備
必要な要素
PWAには最低限、以下の3つが必要です:
- HTTPS対応:Service Workerはセキュア接続でのみ動作
- manifest.json:アプリの情報を定義
- Service Worker:オフライン対応やキャッシュを制御
HTTPS化
GitHub Pagesを使っている場合、自動的にHTTPS化されるため追加作業は不要です。私の場合も、mechatora.comはお名前.comで独自ドメインを取得し、GitHub PagesのカスタムドメインでHTTPS化が完了していました。
自前サーバーの場合は、Let's Encryptで無料のSSL証明書を取得できます:
# Ubuntu/Debian の場合
sudo apt-get update
sudo apt-get install certbot
sudo certbot --nginx -d yourdomain.com
manifest.jsonの作成
基本的な設定
manifest.jsonは、PWAのメタデータを定義するJSONファイルです。プロジェクトルートに作成します:
{
"name": "MechaTora - 便利なWebツール",
"short_name": "MechaTora",
"description": "日常生活を便利にする実用的なWebアプリケーション",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2563eb",
"orientation": "portrait-primary",
"icons": [
{
"src": "/images/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/images/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
各プロパティの説明
- name:アプリの正式名称(ホーム画面に表示される可能性がある)
- short_name:短縮名(ホーム画面に確実に表示される)
- start_url:アプリ起動時に開くURL
- display:表示モード(standalone, fullscreen, minimal-ui, browser)
- background_color:スプラッシュ画面の背景色
- theme_color:ステータスバーの色(Androidで反映)
- icons:アプリアイコン(192x192と512x512が推奨)
HTMLでの読み込み
index.htmlの<head>内にmanifest.jsonを読み込みます:
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#2563eb">
アイコンの準備
私が実際に使っているアイコン生成の流れ:
- 512x512pxの元画像を作成(Canvaで作成)
- Favicon Generatorで各サイズを自動生成
- 192x192、512x512を/images/に配置
Service Workerの実装
Service Workerとは
Service Workerは、ブラウザがバックグラウンドで実行するJavaScriptです。主な役割:
- ネットワークリクエストの傍受
- キャッシュの管理
- オフライン対応
- プッシュ通知(今回は扱いません)
基本的なService Worker
プロジェクトルートにsw.js(Service Worker)を作成します:
const CACHE_NAME = 'mechatora-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/images/icon-192.png',
'/images/icon-512.png'
];
// インストール時:キャッシュを作成
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('キャッシュを開きました');
return cache.addAll(urlsToCache);
})
);
});
// フェッチ時:キャッシュから返す
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// キャッシュにあればそれを返す
if (response) {
return response;
}
// なければネットワークから取得
return fetch(event.request);
})
);
});
// アクティベート時:古いキャッシュを削除
self.addEventListener('activate', (event) => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});
Service Workerの登録
メインのJavaScriptファイル(app.jsなど)でService Workerを登録します:
// Service Worker に対応しているか確認
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('Service Worker登録成功:', registration.scope);
})
.catch((error) => {
console.log('Service Worker登録失敗:', error);
});
});
}
キャッシュ戦略
3つの基本戦略
私がMechaToraのツールで使い分けている3つの戦略:
1. Cache First(キャッシュ優先)
キャッシュに存在すればそれを返し、なければネットワークから取得。静的ファイル(CSS、JS、画像)に最適。
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
return response || fetch(event.request);
})
);
});
2. Network First(ネットワーク優先)
まずネットワークから取得を試み、失敗したらキャッシュから返す。APIレスポンスなど、最新データが重要な場合に使用。
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.catch(() => {
return caches.match(event.request);
})
);
});
3. Stale While Revalidate(キャッシュを返しつつ更新)
キャッシュを即座に返しつつ、バックグラウンドで最新版を取得してキャッシュを更新。バランスが良い戦略。
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(CACHE_NAME).then((cache) => {
return cache.match(event.request).then((cachedResponse) => {
const fetchPromise = fetch(event.request).then((networkResponse) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
return cachedResponse || fetchPromise;
});
})
);
});
実践的な使い分け
MechaToraでの実際の使い分け例:
- Cache First:styles.css, app.js, 画像ファイル
- Network First:気象庁API、楽天API(地震モニター、日本酒検索)
- Stale While Revalidate:index.html(ホーム画面)
実装時のトラブルシューティング
よくあるエラー1:Service Workerが登録されない
原因:HTTPSでない、またはパスが間違っている
解決策:Chrome DevToolsのApplicationタブでエラーを確認
// Console で確認
navigator.serviceWorker.getRegistration()
.then(reg => console.log(reg));
よくあるエラー2:キャッシュが更新されない
原因:CACHE_NAMEを変更していない
解決策:バージョン番号を上げる
// 変更前
const CACHE_NAME = 'mechatora-v1';
// 変更後(ファイルを更新したら必ずバージョンアップ)
const CACHE_NAME = 'mechatora-v2';
よくあるエラー3:manifest.jsonが読み込まれない
原因:JSONの構文エラー、またはMIMEタイプが間違っている
解決策:Manifest Validatorで検証
開発時の注意点
開発中は、Chrome DevToolsの「Update on reload」をチェックすると、リロードの度にService Workerが更新されて便利です:
- Chrome DevToolsを開く(F12)
- Applicationタブ → Service Workers
- 「Update on reload」にチェック
PWAの動作確認
Lighthouseで検証
Chrome DevToolsのLighthouseタブでPWAスコアを確認できます:
- DevToolsを開く(F12)
- Lighthouseタブを選択
- 「Progressive Web App」をチェック
- 「Analyze page load」をクリック
私の場合、最初は78点でしたが、以下を改善して95点まで上がりました:
- manifest.jsonにdescriptionを追加
- アイコンを192x192と512x512の両方用意
- theme-colorをmetaタグにも追加
- offline時の404ページを用意
実機でのテスト
Android(Chrome):
- PWAサイトにアクセス
- メニュー(︙)→「ホーム画面に追加」
- 追加されたアイコンから起動
iOS(Safari):
- PWAサイトにアクセス
- 共有ボタン →「ホーム画面に追加」
- 追加されたアイコンから起動
注意:iOSはService Workerの対応が限定的で、一部機能(プッシュ通知など)が使えません。
実際の運用で学んだこと
キャッシュサイズの管理
初期は全ファイルをキャッシュしていましたが、容量が肥大化したため、必要最低限に絞りました:
// 改善前:全画像をキャッシュ(10MB超え)
const urlsToCache = [
'/',
'/images/photo1.jpg',
'/images/photo2.jpg',
// ... 100枚以上
];
// 改善後:アイコンとロゴのみ(500KB以下)
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/images/icon-192.png',
'/images/icon-512.png',
'/images/logo.png'
];
キャッシュの有効期限
7日以上経過したキャッシュは削除するようにしました:
const MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7日間
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
return caches.open(cacheName).then((cache) => {
return cache.keys().then((requests) => {
return Promise.all(
requests.map((request) => {
return cache.match(request).then((response) => {
if (response) {
const date = new Date(response.headers.get('date'));
if (Date.now() - date.getTime() > MAX_AGE) {
return cache.delete(request);
}
}
});
})
);
});
});
})
);
})
);
});
ユーザーへの通知
新しいバージョンが利用可能になったら、ユーザーに通知を出すようにしました:
// app.js
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) return;
refreshing = true;
window.location.reload();
});
navigator.serviceWorker.register('/sw.js').then((reg) => {
reg.addEventListener('updatefound', () => {
const newWorker = reg.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// 新バージョン検出
if (confirm('新しいバージョンが利用可能です。更新しますか?')) {
newWorker.postMessage({ type: 'SKIP_WAITING' });
}
}
});
});
});
// sw.js
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
まとめ
PWA化により、MechaToraのツール群は以下の改善を実現しました:
- オフライン対応:ネット環境がなくても基本機能が使える
- 高速化:キャッシュにより初回以降の読み込みが爆速
- UX向上:ホーム画面からワンタップで起動
- インストール不要:App Storeを経由せず配布可能
実装の手順をまとめると:
- HTTPS化(GitHub Pagesなら自動)
- manifest.json作成(名前、アイコン、テーマカラー)
- Service Worker実装(install, fetch, activate)
- キャッシュ戦略の選択(Cache First/Network First/Stale While Revalidate)
- Lighthouseで検証(90点以上を目指す)
最初は難しく感じるかもしれませんが、基本的なパターンは決まっているため、一度理解すれば様々なプロジェクトに応用できます。特に、個人開発のツール系Webアプリとの相性は抜群です。
GitHub Pagesなら完全無料でPWAを公開できるため、ぜひチャレンジしてみてください!