Service WorkerとIndexedDBで、ユーザー体験を変える!オフラインWebアプリ
インターネット接続が不安定な場所でもサクサク動くWebアプリを作りたい、オフラインでもユーザーが入力したデータを失わずに保存したい。そう考えたことはありませんか?モバイル利用が当たり前になった今、Webアプリにもネイティブアプリのような安定した使い心地が求められています。この記事では、そんな課題を解決する強力な技術、Service Worker と IndexedDB を紹介します。この2つを組み合わせることで、ネットワーク環境に左右されない、快適なオフライン対応Webアプリをどう構築すればよいのか、具体的なコード例を交えながら実践的に解説していきます。
はじめに:なぜ今、オフライン対応Webアプリが求められるのか?
スマートフォンやタブレットの普及により、私たちはいつでもどこでもWebにアクセスできるようになりました。しかし、その接続環境は常に安定しているとは限りません。電車や地下鉄での移動中、Wi-Fiが混雑しているカフェ、あるいは電波の届きにくい場所など、ネットワークが途切れたり、遅くなったりする場面は日常的に発生します。そんな時、Webページが真っ白になったり、操作不能になったりすると、ユーザーは大きなストレスを感じてしまいます。
こうした背景から、PWA (Progressive Web Apps) という考え方が重要視されています。PWAは、Webサイトをネイティブアプリのようにインストール可能にし、プッシュ通知やオフライン動作といった機能を提供する技術の総称です。その中核を担うのが、今回解説する Service Worker によるオフライン対応です。
オフライン対応は、単に「接続がない時でも動く」という守りの機能だけではありません。リソースをキャッシュから高速に読み込むことで、オンライン時の表示速度も劇的に向上させます。つまり、オフライン対応はあらゆるネットワーク環境において、ユーザー体験 (UX) を向上させるための攻めの技術でもあるのです。
Service Workerの基礎知識:Webの縁の下の力持ちを理解する
Service Worker とは、ブラウザがWebページとは別にバックグラウンドで実行するスクリプトのことです。Webページ本体のJavaScriptとは独立して動作するため、ページを閉じていてもプッシュ通知を受け取るなどの処理ができます。オフライン対応において、Service Worker はネットワークリクエストをプロキシのように仲介する役割を果たします。
Service Worker の最大の特徴は、Webページからのネットワークリクエストを監視し、そのリクエストを横取り (intercept) できる点です。具体的には fetch というイベントを購読することで、ページが画像やAPIデータをリクエストした際に介入し、「ネットワークに問い合わせる代わりに、キャッシュに保存したデータを返す」といった制御が可能になります。
この Service Worker を利用するには、まずWebページ側でスクリプトファイルを登録する必要があります。以下がその基本的なコードです。
// main.js (Webページのスクリプト)
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker 登録成功:', registration);
})
.catch(error => {
console.log('Service Worker 登録失敗:', error);
});
});
}
注意点として、Service Worker はセキュリティ上の理由から HTTPS (または localhost) 環境でしか動作しません。これは、悪意のある中間者攻撃によって Service Worker が改ざんされるのを防ぐためです。開発中は localhost を使えば問題なく試せます。
実践!Cache APIでリソースを賢くキャッシュする戦略
Service Worker の力を最大限に引き出すのが Cache API です。これは、HTTPレスポンスをキーと値のペアで保存するためのブラウザAPIで、Service Worker から利用することで、HTML, CSS, JavaScript, 画像といった静的なリソースを意図的にキャッシュできます。
一般的な戦略として、「App Shell (アプリケーションの骨格となるUI部分)」を構成するリソースを、Service Worker のインストール時にまとめてキャッシュする方法があります。Service Worker にはライフサイクルがあり、install イベントはその Service Worker が初めて登録された時に一度だけ発火します。
// sw.js (Service Workerのスクリプト)
const CACHE_NAME = 'nozomono-app-cache-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/script/main.js',
'/images/logo.png'
];
// Service Worker のインストール時に実行される
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('キャッシュストレージを開きました');
return cache.addAll(urlsToCache);
})
);
});
リソースをキャッシュしたら、次はそのキャッシュを利用するように fetch イベントを書き換えます。以下の例は「キャッシュファースト」と呼ばれる戦略で、まずキャッシュにリソースがないか探し、あればそれを返し、なければネットワークにリクエストを取りにいきます。
// sw.js
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// キャッシュに一致するリクエストがあれば、それを返す
if (response) {
return response;
}
// キャッシュになければ、ネットワークにリクエストする
return fetch(event.request);
})
);
});
この仕組みにより、一度訪れたユーザーは、オフライン環境でもApp Shellが表示され、オンラインに戻れば最新のコンテンツを取得するという、快適な体験を提供できます。キャッシュ戦略には他にも「ネットワークファースト」や「Stale-While-Revalidate」などがあり、コンテンツの特性に応じて使い分けることが重要です。
IndexedDBでデータを永続化:オフラインでも情報が消えない仕組み
Cache API が静的リソースの保存に長けているのに対し、ユーザーが入力したデータや動的に取得したAPIレスポンスのような構造化データを保存するのに適しているのが IndexedDB です。IndexedDB は、ブラウザ内で利用できるキー・バリュー型のNoSQLデータベースです。
localStorage と比較されることもありますが、IndexedDB には以下のような優れた点があります。
- 大容量:
localStorageが数MB程度なのに対し、IndexedDBは数十MBから数GB (ブラウザや空き容量に依存) のデータを保存できます。 - 非同期処理:
IndexedDBの操作は非同期で行われるため、メインスレッドをブロックせず、UIの応答性を損ないません。 - 構造化データ: JSONオブジェクトをそのまま保存でき、特定のプロパティにインデックスを付けて高速な検索が可能です。
- トランザクション: 複数の操作をひとまとめにして、すべて成功するかすべて失敗するかのどちらかという、データの整合性を保つ仕組みがあります。
例えば、オフライン環境でユーザーがToDoリストに新しいタスクを追加したり、ブログ記事の下書きを保存したりするアプリケーションを考えてみましょう。こうしたデータを IndexedDB に保存しておけば、ユーザーがブラウザを閉じてもデータは消えません。そして、次にオンラインになったタイミングでサーバーに同期させることができます。IndexedDB のAPIは少し複雑なため、Dexie.js のようなラッパーライブラリを利用すると、より直感的にコードを書けるようになります。
Service WorkerとIndexedDB連携:ミニマルなオフラインWebアプリを構築しよう
静的リソースをキャッシュする Service Worker と、動的データを保存する IndexedDB。この2つを連携させることで、本格的な オフライン対応Webアプリ が完成します。
典型的な連携パターンは、「バックグラウンド同期」です。ユーザーがオフライン時に何かデータをサーバーに送信しようとした場合、その操作が失敗しても、ユーザー体験を損なわないように振る舞います。
- UIからのデータ送信: ユーザーがフォームを送信すると、Webページは通常通り
fetch()でサーバーにPOSTリクエストを送ります。 - Service Workerによる介入:
Service Workerのfetchイベントがこのリクエストを捕捉します。 - ネットワーク状態の判定:
fetch()を試み、ネットワークエラー (catch節) になった場合、オフラインであると判断します。 - IndexedDBへのデータ待避: 送信に失敗したリクエストの情報 (URL, メソッド, ボディなど) を
IndexedDBに「未送信データ」として保存します。 - UIへの応答: Webページ側には、オフラインのため後で同期する旨を伝える仮のレスポンスを返します。これにより、ユーザーにはあたかも操作が受け付けられたかのように見せることができます (Optimistic UI)。
- オンライン復帰時の同期: ユーザーがオンラインに復帰したタイミングで、
Service WorkerはIndexedDBに保存されている未送信データを読み出し、サーバーに再送信します。この処理はBackground Sync APIを使うと、より信頼性の高い実装が可能です。
この一連の流れにより、ユーザーはネットワーク状況を気にすることなく、いつでもアプリケーションを操作できます。入力したデータが失われる心配がなく、シームレスな体験を提供できるのです。
開発・デバッグ時の注意点と運用ベストプラクティス
Service Worker の開発は、その独特なライフサイクルやキャッシュの挙動から、いくつか注意すべき点があります。開発をスムーズに進めるために、ブラウザの開発者ツールを積極的に活用しましょう。
Google ChromeのDevToolsでは、「Application」パネルから Service Worker の状態を確認できます。ここでは、現在アクティブな Service Worker の確認、手動での停止や更新、キャッシュストレージ (Cache Storage) や IndexedDB の中身を直接覗いたり削除したりすることが可能です。特に「Offline」チェックボックスをオンにすれば、ネットワークが切断された状態を簡単にシミュレートでき、デバッグに非常に役立ちます。
運用時のベストプラクティスとして、キャッシュのバージョン管理 が挙げられます。sw.js ファイルを更新しても、古い Service Worker が制御しているページを開いている限り、新しいバージョンはすぐには有効化 (activate) されません。この問題を避けるため、Cache API で使うキャッシュ名に v1, v2 のようなバージョン番号を含めるのが一般的です。そして、新しい Service Worker が activate されるタイミングで、古いバージョンのキャッシュを削除する処理を実装します。これにより、ユーザーは常に最新のリソースを利用できるようになります。
オフライン対応は一度にすべてを実装しようとせず、まずはApp Shellのキャッシュから始め、次に読み取り専用のデータ、最後にデータの書き込みと同期、というように段階的に機能を拡張していくことをお勧めします。小さく始めて、ユーザーの反応を見ながら改善していくことが、成功への近道です。


