PWA開発ガイド2026年版 — Service WorkerからWeb Push通知まで


Progressive Web App(PWA)は、ネイティブアプリのような体験をWebで実現する技術です。この記事では、2026年時点の最新PWA開発手法を実践的に解説します。

PWAとは

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

  • インストール可能: ホーム画面に追加できる
  • オフライン対応: ネットワークなしでも動作
  • プッシュ通知: ユーザーエンゲージメント向上
  • 高速: キャッシュによる即座のロード
  • セキュア: HTTPS必須

マニフェストファイル

PWAの基本設定を記述します。

{
  "name": "My Awesome App",
  "short_name": "MyApp",
  "description": "An awesome Progressive Web App",
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#4F46E5",
  "background_color": "#FFFFFF",
  "orientation": "portrait-primary",
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "screenshots": [
    {
      "src": "/screenshots/home.png",
      "sizes": "540x720",
      "type": "image/png",
      "form_factor": "narrow"
    }
  ],
  "shortcuts": [
    {
      "name": "New Post",
      "url": "/new",
      "icons": [{ "src": "/icons/new.png", "sizes": "96x96" }]
    }
  ]
}

HTMLでの読み込み:

<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#4F46E5">
<link rel="apple-touch-icon" href="/icons/icon-192.png">

Service Workerの基本

Service Workerは、ブラウザとネットワークの間に位置するプロキシです。

登録

// app.ts
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js');
      console.log('Service Worker registered:', registration.scope);
    } catch (error) {
      console.error('Service Worker registration failed:', error);
    }
  });
}

基本的なService Worker

// sw.js
const CACHE_NAME = 'my-app-v1';
const URLS_TO_CACHE = [
  '/',
  '/styles.css',
  '/script.js',
  '/offline.html',
];

// インストール
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(URLS_TO_CACHE);
    })
  );
  self.skipWaiting(); // 即座に有効化
});

// アクティベート
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames
          .filter((name) => name !== CACHE_NAME)
          .map((name) => caches.delete(name))
      );
    })
  );
  self.clients.claim(); // 即座に制御
});

// フェッチ
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});

Workboxによる高度なキャッシング

Workboxは、Service Workerの開発を簡単にするライブラリです。

インストール

npm install workbox-webpack-plugin

Webpack設定

// webpack.config.js
const WorkboxPlugin = require('workbox-webpack-plugin');

module.exports = {
  // ...
  plugins: [
    new WorkboxPlugin.GenerateSW({
      clientsClaim: true,
      skipWaiting: true,
      runtimeCaching: [
        {
          urlPattern: /^https:\/\/api\.example\.com/,
          handler: 'NetworkFirst',
          options: {
            cacheName: 'api-cache',
            expiration: {
              maxEntries: 50,
              maxAgeSeconds: 5 * 60, // 5分
            },
          },
        },
        {
          urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
          handler: 'CacheFirst',
          options: {
            cacheName: 'image-cache',
            expiration: {
              maxEntries: 100,
              maxAgeSeconds: 30 * 24 * 60 * 60, // 30日
            },
          },
        },
      ],
    }),
  ],
};

キャッシング戦略

Cache First(キャッシュ優先)

画像や静的アセットに適しています。

import { CacheFirst } from 'workbox-strategies';
import { registerRoute } from 'workbox-routing';

registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [
      {
        cacheWillUpdate: async ({ response }) => {
          return response.status === 200 ? response : null;
        },
      },
    ],
  })
);

Network First(ネットワーク優先)

APIリクエストに適しています。

import { NetworkFirst } from 'workbox-strategies';

registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({
    cacheName: 'api',
    networkTimeoutSeconds: 5,
  })
);

Stale While Revalidate

頻繁に更新されるコンテンツに適しています。

import { StaleWhileRevalidate } from 'workbox-strategies';

registerRoute(
  ({ request }) => request.destination === 'script',
  new StaleWhileRevalidate({
    cacheName: 'scripts',
  })
);

オフライン対応

ネットワークエラー時にオフラインページを表示します。

import { setCatchHandler } from 'workbox-routing';

setCatchHandler(async ({ event }) => {
  if (event.request.destination === 'document') {
    return caches.match('/offline.html');
  }
  return Response.error();
});

Web Push通知

プッシュ通知でユーザーエンゲージメントを向上させます。

購読

async function subscribeToPush() {
  const registration = await navigator.serviceWorker.ready;

  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY),
  });

  // サーバーに送信
  await fetch('/api/subscribe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(subscription),
  });
}

function urlBase64ToUint8Array(base64String: string) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/-/g, '+')
    .replace(/_/g, '/');

  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);

  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

通知受信

// sw.js
self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? {};
  const options = {
    body: data.body,
    icon: '/icons/icon-192.png',
    badge: '/icons/badge-72.png',
    data: { url: data.url },
  };

  event.waitUntil(
    self.registration.showNotification(data.title, options)
  );
});

self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  event.waitUntil(
    clients.openWindow(event.notification.data.url)
  );
});

サーバー側(Node.js)

import webpush from 'web-push';

webpush.setVapidDetails(
  'mailto:your-email@example.com',
  PUBLIC_VAPID_KEY,
  PRIVATE_VAPID_KEY
);

async function sendNotification(subscription: PushSubscription, payload: any) {
  try {
    await webpush.sendNotification(subscription, JSON.stringify(payload));
  } catch (error) {
    console.error('Push notification failed:', error);
  }
}

Background Sync

オフライン時のリクエストを自動再送信します。

// Service Worker
import { BackgroundSyncPlugin } from 'workbox-background-sync';
import { registerRoute } from 'workbox-routing';
import { NetworkOnly } from 'workbox-strategies';

const bgSyncPlugin = new BackgroundSyncPlugin('postQueue', {
  maxRetentionTime: 24 * 60, // 24時間
});

registerRoute(
  ({ url }) => url.pathname.startsWith('/api/posts'),
  new NetworkOnly({
    plugins: [bgSyncPlugin],
  }),
  'POST'
);

クライアント側:

async function createPost(data: PostData) {
  try {
    await fetch('/api/posts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });
  } catch (error) {
    // Service Workerがバックグラウンドで再送信
    console.log('Request queued for background sync');
  }
}

File System Access API

ローカルファイルへのアクセスを可能にします。

async function saveFile(content: string) {
  try {
    const handle = await window.showSaveFilePicker({
      suggestedName: 'document.txt',
      types: [{
        description: 'Text Files',
        accept: { 'text/plain': ['.txt'] },
      }],
    });

    const writable = await handle.createWritable();
    await writable.write(content);
    await writable.close();
  } catch (error) {
    console.error('Save failed:', error);
  }
}

async function openFile() {
  const [handle] = await window.showOpenFilePicker();
  const file = await handle.getFile();
  const content = await file.text();
  return content;
}

インストールプロンプト

カスタムインストールUIを作成します。

let deferredPrompt: any;

window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault();
  deferredPrompt = e;
  // カスタムボタンを表示
  document.getElementById('install-button')?.classList.remove('hidden');
});

document.getElementById('install-button')?.addEventListener('click', async () => {
  if (deferredPrompt) {
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    console.log(`User ${outcome} the install prompt`);
    deferredPrompt = null;
  }
});

パフォーマンス最適化

プリキャッシュ

重要なアセットを事前にキャッシュします。

import { precacheAndRoute } from 'workbox-precaching';

precacheAndRoute(self.__WB_MANIFEST);

ナビゲーションプリロード

Service Worker起動中のネットワークリクエストを並列化します。

addEventListener('activate', (event) => {
  event.waitUntil(self.registration.navigationPreload.enable());
});

addEventListener('fetch', (event) => {
  event.respondWith(async function() {
    const response = await event.preloadResponse;
    if (response) return response;
    return fetch(event.request);
  }());
});

まとめ

PWAは、ネイティブアプリに近い体験をWebで実現できる強力な技術です。

主な機能:

  • Service Workerによるオフライン対応
  • Workboxによる高度なキャッシング
  • Web Push通知
  • Background Sync
  • File System Access

適切に実装すれば、ユーザーエクスペリエンスを大幅に向上させることができます。