JavaScriptにおける`Service Worker`

JavaScriptにおける`Service Worker`

この記事ではJavaScriptにおけるService Workerについて説明します。

Service Workerの基本から実践的なキャッシュ制御までをステップバイステップで解説します。

YouTube Video

offline.html
 1<!DOCTYPE html>
 2<html lang="en">
 3<head>
 4  <meta charset="UTF-8">
 5  <title>Offline</title>
 6</head>
 7<body>
 8  <h1>You are offline</h1>
 9  <p>This is the offline fallback page.</p>
10</body>
11</html>
style.css
1body {
2  font-family: sans-serif;
3  background-color: #f0f0f0;
4  padding: 20px;
5}
6h1 {
7  color: #333;
8}
javascript-service-worker.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    .container-flex {
 32        display: flex;
 33        flex-wrap: wrap;
 34        gap: 2em;
 35        max-width: 1000px;
 36        margin: 0 auto;
 37        padding: 1em;
 38        background-color: #ffffff;
 39        border: 1px solid #ccc;
 40        border-radius: 10px;
 41        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 42    }
 43
 44    .left-column, .right-column {
 45        flex: 1 1 200px;
 46        min-width: 200px;
 47    }
 48
 49    h1, h2 {
 50        font-size: 1.2rem;
 51        color: #007bff;
 52        margin-top: 0.5em;
 53        margin-bottom: 0.5em;
 54        border-left: 5px solid #007bff;
 55        padding-left: 0.6em;
 56        background-color: #e9f2ff;
 57    }
 58
 59    button {
 60        display: block;
 61        margin: 1em auto;
 62        padding: 0.75em 1.5em;
 63        font-size: 1rem;
 64        background-color: #007bff;
 65        color: white;
 66        border: none;
 67        border-radius: 6px;
 68        cursor: pointer;
 69        transition: background-color 0.3s ease;
 70    }
 71
 72    button:hover {
 73        background-color: #0056b3;
 74    }
 75
 76    #output {
 77        margin-top: 1em;
 78        background-color: #1e1e1e;
 79        color: #0f0;
 80        padding: 1em;
 81        border-radius: 8px;
 82        min-height: 200px;
 83        font-family: Consolas, monospace;
 84        font-size: 0.95rem;
 85        overflow-y: auto;
 86        white-space: pre-wrap;
 87    }
 88
 89    .highlight {
 90        outline: 3px solid #ffc107; /* yellow border */
 91        background-color: #fff8e1;  /* soft yellow background */
 92        transition: background-color 0.3s ease, outline 0.3s ease;
 93    }
 94
 95    .active {
 96        background-color: #28a745; /* green background */
 97        color: #fff;
 98        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 99        transition: background-color 0.3s ease, box-shadow 0.3s ease;
100    }
101  </style>
102</head>
103<body>
104    <div class="container">
105        <h1>JavaScript Console</h1>
106        <button id="executeBtn">Execute</button>
107        <div id="output"></div>
108    </div>
109
110    <div class="container">
111        <h2>HTML Sample</h2>
112        <button id="fetchBtn">Fetch Test</button>
113    </div>
114
115    <script>
116        // Override console.log to display messages in the #output element
117        (function () {
118            // Override console.log
119            const originalLog = console.log;
120            console.log = function (...args) {
121                originalLog.apply(console, args);
122                const message = document.createElement('div');
123                message.textContent = args.map(String).join(' ');
124                output.appendChild(message);
125            };
126
127            // Override console.error
128            const originalError = console.error;
129            console.error = function (...args) {
130                originalError.apply(console, args);
131                const message = document.createElement('div');
132                message.textContent = args.map(String).join(' ');
133                message.style.color = 'red'; // Color error messages red
134                output.appendChild(message);
135            };
136        })();
137
138        document.getElementById('executeBtn').addEventListener('click', () => {
139            // Prevent multiple loads
140            if (document.getElementById('externalScript')) return;
141
142            const script = document.createElement('script');
143            script.src = 'javascript-service-worker.js';
144            script.id = 'externalScript';
145            //script.onload = () => console.log('javascript-service-worker.js loaded and executed.');
146            //script.onerror = () => console.log('Failed to load javascript-service-worker.js.');
147            document.body.appendChild(script);
148        });
149    </script>
150</body>
151</html>

JavaScriptにおけるService Worker

Service Workerは、ブラウザとネットワークの間に立って、リクエストのキャッシュやオフライン対応などを実現できるJavaScriptの仕組みです。PWA(Progressive Web App)の中心的な技術でもあり、Webアプリにネイティブアプリのような体験をもたらします。

Service Workerとは?

Service Workerは、ブラウザのバックグラウンドスレッドで実行されるJavaScriptファイルです。ページとは別スレッドで動作し、UIにはアクセスできませんが、ネットワークリクエストの傍受・キャッシュ操作・プッシュ通知などを制御できます。

Service Workerの特徴として次の点があります。

  • localhostを除きHTTPS上でのみ動作します。
  • Promiseベースの非同期APIです。
  • install, activate, fetch, pushなどによるイベント駆動です

Service Workerの登録

まずは、Service Workerをブラウザに登録するコードを書いてみましょう。

 1if ('serviceWorker' in navigator) {
 2    window.addEventListener('load', () => {
 3        navigator.serviceWorker.register('/sw.js')
 4        .then(registration => {
 5            console.log(
 6                'Service Worker registered with scope:',
 7                registration.scope
 8            );
 9        })
10        .catch(error => {
11            console.error('Service Worker registration failed:', error);
12        });
13    });
14}

解説

  • navigator.serviceWorker.register()/sw.jsService Workerファイル)を登録します。
  • then で登録成功時、catch で失敗時の処理を記述できます。
  • registration.scope は、Service Worker が影響を及ぼすパスの範囲(スコープ)を表します。
  • デフォルトでは、登録したファイル(ここでは /sw.js)のあるディレクトリとその配下がスコープになります。

Service Worker の scope

スコープを限定したい場合は、register の第2引数で scope を指定できます。

1navigator.serviceWorker.register('/sw.js', { scope: '/app/' })
2.then(registration => {
3    console.log(
4        'Service Worker registered with scope:',
5        registration.scope
6    );
7});

解説

  • このようにすると、/app/ 以下のページのみが Service Worker によって制御されます。

Service Workerファイルの作成

次に、sw.js というファイルを作成し、基本的なイベントを実装していきましょう。

1// sw.js
2const CACHE_NAME = 'my-cache-v1';
3const urlsToCache = [
4    '/',
5    '/index.html',
6    '/styles.css',
7    '/script.js',
8    '/offline.html'
9];

このコードでは、キャッシュするリソースのリストを定義しています。

各イベントの役割と仕組み

install

 1// Install event (initial caching)
 2self.addEventListener('install', event => {
 3    console.log('[ServiceWorker] Install');
 4    event.waitUntil(
 5        caches.open(CACHE_NAME).then(cache => {
 6            console.log('[ServiceWorker] Caching app shell');
 7            return cache.addAll(urlsToCache);
 8        })
 9    );
10});
  • self.addEventListener('install') は、Service Workerの初回登録時に発火します。ここで必要なファイルを事前にキャッシュに保存します。

activate

 1// Activation event (delete old caches)
 2self.addEventListener('activate', event => {
 3    console.log('[ServiceWorker] Activate');
 4    event.waitUntil(
 5        caches.keys().then(keyList => {
 6            return Promise.all(keyList.map(key => {
 7                if (key !== CACHE_NAME) {
 8                    console.log('[ServiceWorker] Removing old cache:', key);
 9                    return caches.delete(key);
10                }
11            }));
12        })
13    );
14    return self.clients.claim();
15});
  • activate イベントでは、古いキャッシュを削除することで、ストレージを最適化します。新しいバージョンのキャッシュのみを残すようにします。

fetch

1// Fetch event (cache-first strategy)
2self.addEventListener('fetch', event => {
3  console.log('[ServiceWorker] Fetch', event.request.url);
4    event.respondWith(
5        caches.match(event.request).then(response => {
6            return response || fetch(event.request).catch(() => caches.match('/offline.html'));
7        })
8    );
9});

すべてのHTTPリクエストを傍受し、キャッシュにある場合はそれを返し、なければネットワーク経由で取得します。オフライン時には代替ページ(例:offline.html)を返すようにしています。

動作の確認

実際に Service Worker の動作を確認してみます。

 1document.getElementById('fetchBtn').addEventListener('click', () => {
 2    fetch('/style.css')
 3        .then(response => response.text())
 4        .then(data => {
 5            console.log('Fetched data:', data);
 6        })
 7        .catch(error => {
 8            console.error('Fetch failed:', error);
 9        });
10});
  • ここでは、Service Worker の登録と、テストボタンをクリックしてリソースを取得する動作を確認しています。

キャッシュ戦略の例

キャッシュ戦略には次のようなものがあります。

キャッシュ優先(Cache First)

キャッシュを優先する場合の実装例は次のようになります。

1self.addEventListener('fetch', event => {
2    event.respondWith(
3        caches.match(event.request).then(response => {
4            return response || fetch(event.request);
5        })
6    );
7});
  • このコードは、リクエストされたリソースがキャッシュにあればそれを返し、なければネットワークから取得するキャッシュ優先の戦略を実装しています。

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

ネットワークを優先する場合の実装例は次のようになります。

 1self.addEventListener('fetch', event => {
 2    event.respondWith(
 3        fetch(event.request)
 4            .then(response => {
 5                return caches.open(CACHE_NAME).then(cache => {
 6                    cache.put(event.request, response.clone());
 7                    return response;
 8                });
 9            })
10            .catch(() => caches.match(event.request))
11    );
12});
  • このコードは、リクエストされたリソースをまずネットワークから取得し、失敗した場合にキャッシュから取得するネットワーク優先の戦略を実装しています。

スタイルやJavaScriptだけキャッシュ、APIはリアルタイム

スタイルやJavaScriptだけキャッシュを優先し、APIはリアルタイムでアクセスする場合の実装例は次のようになります。

 1self.addEventListener('fetch', event => {
 2    if (event.request.url.includes('/api/')) {
 3        // Fetch API responses in real-time without caching
 4        return;
 5    }
 6
 7    // Use cache-first strategy for static files
 8    event.respondWith(
 9        caches.match(event.request).then(response => {
10            return response || fetch(event.request);
11        })
12    );
13});
  • このコードは、APIリクエストには常にリアルタイムでアクセスし、スタイルやJavaScriptなどの静的ファイルにはキャッシュ優先の戦略を適用する実装です。

更新時の流れ

Service Workerの更新は以下の流れになります。

1.新しい sw.js が検出されます 2.install イベントが発火します 3.前の Service Worker が「アイドル状態」になるまで待機します 4.activate イベントが発火します 5.新しい SW に切り替わります 6.controllerchangeイベントが発火します

更新の検出

Service Workerは一度インストールされると、次回アクセスまで古いものが使われ続けるという仕様があります。更新を反映させるために、更新を検知しページをリロードさせるコードもよく使われます。

1navigator.serviceWorker.addEventListener('controllerchange', () => {
2    window.location.reload();
3});
  • controllerchange イベントは、Service Worker のコントローラー、つまり現在のページを制御している Service Worker が変わったときに発火するイベントです。
  • すでに開いているページは、現在の Service Worker を使い続け、インストールされた新しい Service Worker はすぐにはページに反映されません。そのため、controllerchange イベントで新しいコントローラーが有効になったことを検知し、ページをリロードして更新を即座に反映させる、というテクニックが使われます。

注意点とベストプラクティス

Service Workerを利用する際には、次のような注意点があります。

  • HTTPS必須 セキュリティ上の制限で、localhostを除いて、http:// では動作しません。

  • ファイル名のハッシュ管理 キャッシュの名称に、ファイルの名前やURLに加えて、バージョンも含められます。

  • クライアントとの通信 Service WorkerとページのJavaScriptとの通信には、postMessage を使います。

まとめ

Service WorkerはWebアプリのオフライン対応やパフォーマンス向上に欠かせない技術です。基本的なインストール、アクティベート、フェッチ処理の流れを理解し、目的に応じたキャッシュ戦略を実装することで、より高品質なWebアプリを構築できます。

YouTubeチャンネルでは、Visual Studio Codeを用いて上記の記事を見ながら確認できます。 ぜひYouTubeチャンネルもご覧ください。

YouTube Video