MechaToraのブログ

記事のアイキャッチ画像
技術・開発

PWA実装の基礎 - WebアプリをPWA化する手順

PWA(Progressive Web Apps)は、Webアプリをネイティブアプリのように動作させる技術です。オフライン対応、ホーム画面への追加、プッシュ通知など、ユーザー体験を大幅に向上させることができます。

この記事では、実際にMechaToraのツール群をPWA化した経験をもとに、PWAの基礎から実装手順まで、初心者でも分かる形で解説します。Service Workerの基本、manifest.jsonの設定、キャッシュ戦略の選び方など、実践的な内容をお届けします。

PWAとは何か

PWAの3つの柱

PWAは以下の3つの特徴を持つWebアプリケーションです:

ネイティブアプリとの違い

私がPWA化を決めた理由は、以下のメリットがあったためです:

MechaToraで運用している15個のツールは、全てPWA化することで、ユーザーがホーム画面に追加してネイティブアプリのように使えるようになりました。

PWA実装の準備

セクション画像

必要な要素

PWAには最低限、以下の3つが必要です:

  1. HTTPS対応:Service Workerはセキュア接続でのみ動作
  2. manifest.json:アプリの情報を定義
  3. 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"
    }
  ]
}

各プロパティの説明

HTMLでの読み込み

index.htmlの<head>内にmanifest.jsonを読み込みます:

<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#2563eb">

アイコンの準備

私が実際に使っているアイコン生成の流れ:

  1. 512x512pxの元画像を作成(Canvaで作成)
  2. Favicon Generatorで各サイズを自動生成
  3. 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での実際の使い分け例:

実装時のトラブルシューティング

よくあるエラー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が更新されて便利です:

  1. Chrome DevToolsを開く(F12)
  2. Applicationタブ → Service Workers
  3. 「Update on reload」にチェック

PWAの動作確認

Lighthouseで検証

Chrome DevToolsのLighthouseタブでPWAスコアを確認できます:

  1. DevToolsを開く(F12)
  2. Lighthouseタブを選択
  3. 「Progressive Web App」をチェック
  4. 「Analyze page load」をクリック

私の場合、最初は78点でしたが、以下を改善して95点まで上がりました:

実機でのテスト

Android(Chrome):

  1. PWAサイトにアクセス
  2. メニュー(︙)→「ホーム画面に追加」
  3. 追加されたアイコンから起動

iOS(Safari):

  1. PWAサイトにアクセス
  2. 共有ボタン →「ホーム画面に追加」
  3. 追加されたアイコンから起動

注意: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のツール群は以下の改善を実現しました:

実装の手順をまとめると:

  1. HTTPS化(GitHub Pagesなら自動)
  2. manifest.json作成(名前、アイコン、テーマカラー)
  3. Service Worker実装(install, fetch, activate)
  4. キャッシュ戦略の選択(Cache First/Network First/Stale While Revalidate)
  5. Lighthouseで検証(90点以上を目指す)

最初は難しく感じるかもしれませんが、基本的なパターンは決まっているため、一度理解すれば様々なプロジェクトに応用できます。特に、個人開発のツール系Webアプリとの相性は抜群です。

GitHub Pagesなら完全無料でPWAを公開できるため、ぜひチャレンジしてみてください!