https://d226lax1qjow5r.cloudfront.net/blog/blogposts/how-javascript-service-workers-increased-our-site-speed-by-97-5/js_service-workers.png

Javascriptサービス・ワーカーによるサイト・スピードの97.5%向上

最終更新日 August 10, 2023

所要時間:1 分

ここでは、サービスワーカーを使用することでウェブサイトの読み込みを97.5%高速化した方法、ユーザーに毎回最新バージョンを確実に提供する方法、そしてあなたにもできる方法をご紹介します。

ビビッド・デザイン・システムのウェブサイトは、わずか1年で大きく成長しました。いくつかの基本的なページから始まり、今では本格的なドキュメントサイトに成長し、私たちの製品の使い方の実例を紹介しています。

私たちのコードではコード分割を多用しており、製品にはアイコンやCSSファイルのような様々な静的リソースが含まれています。さらに、私たちのドキュメントはiFrameを使ってコンポーネントを表示しており、それぞれが依存関係を個別に呼び出しています(そう、Microfrontendアーキテクチャのようなものです)。

しかし、時が経つにつれ、物事が壊れ始めた。

私たちの開発環境から始まった。何度かリロードすると、動かなくなるんだ。

Vivid Docs Loading Errorvivid-docs-loading-error.gif

それは我慢できた。私たちが我慢できなかったのは、ユーザーから送られてきたチケットで、その状況がどれほどひどいものだったかを私たちに示すことだった。

Chrome Performance User Ticketvivid-user-ticket.png

私たちはそれを何とかしなければならないと思った。ウェブサイトをスピードアップする必要があったのです。ここでは、私たちがどのようにウェブサイトのパフォーマンスをデバッグし、修正したかを紹介します。

パフォーマンスの向上

では、最後から始めよう。私たちの主な目標は、サーバーへの呼び出し回数を減らすことだった。副次的な目標(あるいはボーナス)は、アプリ内のページのロード時間を短縮することでした。結果を見て、自分で判断してください:

Vivid Performance Before Service Workersvivid-performance-before.png

上の画像では、変更前のネットワークリクエストのプロファイルを見ることができる。ページのロードに860msという膨大な時間を要したところもあった!ページの平均ロード時間は約400msでした。一番右のセクションに、ファイルをロードしようとしているサイトのグレーのバーが見えます。 all.cssファイルを読み込もうとしているグレーのバーが見えます。このため、サイトの読み込みが止まり、ユーザーが更新しようとすると、実際にはさらに悪化する!

Vivid Performance Before Service Workersvivid-performance-after.png

では、どうやったのか?

問題:クロームの接続制限

Chromeでは、サーバーへの同時接続は6回までとなっている。私たちを驚かせたのは、タイムアウトの長さと、リフレッシュしたり別のページを閲覧したりしてもリクエストが「ライブ」のままだったことだ。

すべてのページで100近いリクエストがあったため(極端なコード分割をしていた)、Chromeは数分閲覧しただけですべてのユーザーのウェブサイトへの接続をシャットダウンしてしまったのです。そこで、Chromeが読み込む必要のあるファイル数を減らすというミッションが残されました。

問題:ファイルが多すぎる

試みその1:プリフェッチを削除する

ファイル数が多すぎることをどうやって確認したのか?クロームのインスペクターを開くと、コードはこのようになっていた:

Chrome Inspector Loading Too Many Fileschrome-inspector-before-service-workers.png

これらのファイルは、私たちのファイル・マニフェストのほんの一部です。次のページのロード時間を短縮するために、プリフェッチも行っています。

最初のステップは、プリフェッチを止めることだった。これはリクエストの数を減らしたが、プリフェッチはネットワークがアイドルの時にしか起こらないので、あまり効果がなかった。要求されるスクリプトの数を減らす必要がありました。

試みその2:すべてのファイルを1つの大きなバンドルにまとめる

我々のプロジェクトでは rollupを使ってファイルをバンドルしている。私たちの構成は、すべてをコード分割し、コンシューマが独自のバンドルを使ってコード分割、バンドル、ツリーシェイクを行うようになっている。

このステップでは、すべてのコンポーネントを確認し、バレル・ファイルを作成し、他のすべてのスクリプト・タグの代わりに使用する1つの大きな`vivid-components.js`ファイルにまとめました。この コミットをご覧ください。

ほとんどのページで役に立ったが、iFrameを覚えているだろうか?トップページがすでに持ってきていたファイルの複製が、まだたくさん持ってきていたのだ。

こうして、私たちは最終的かつ最も効果的な解決策にたどり着いた: サービス・ワーカー.

ソリューションサービス・ワーカー

サービスワーカーは、アプリとネットワークの間のレイヤーだ。アプリに出入りするすべてのリクエストをリスニングし、それらを処理します。

私たちの場合、リクエストを処理し、レスポンスをキャッシュし、次のリクエストでレスポンスを返したい。

サービスワーカーの登録方法

最初のステップは、サービスワーカーをクライアントに登録することです:

(async function() {
	const registration = await navigator.serviceWorker.register(
		'/sw.js',
		{
			scope: '/',
		}
	);
})();

サービスワーカーは、含まれるフォルダに従ってリクエストをフェッチすることができます。これが、プロジェクトのルートに置いた理由です。

我々のプロジェクトでは、実際のファイルはルートにない。そのため、ルートからリクエストをフェッチすることを可能にしながらも、すばらしい開発体験を与えてくれます。ルートからリクエストを取得できるようにしながらも を使うこともできます。を使うこともできますが、"build to root "のトリックを使えばその必要はありません。

サービス・ワーカーの機能

サービス・ワーカーはこんな感じだ:

const addResourcesToCache = async (resources) => {
	const cache = await caches.open('vivid-cache');
	await cache.addAll(resources);
};

const putInCache = async (request, response) => {
	const cache = await caches.open('vivid-cache');
	await cache.put(request, response);
};

const cacheFirst = async ({ request, preloadResponsePromise, fallbackUrl }) => {
	const responseFromCache = await caches.match(request);
	if (responseFromCache) {
		return responseFromCache;
	}

	const preloadResponse = await preloadResponsePromise;
	if (preloadResponse) {
		console.info('using preload response', preloadResponse);
		await putInCache(request, preloadResponse.clone());
		return preloadResponse;
	}

	try {
		const responseFromNetwork = await fetch(request);
		await putInCache(request, responseFromNetwork.clone());
		return responseFromNetwork;
	} catch (error) {
		const fallbackResponse = await caches.match(fallbackUrl);
		if (fallbackResponse) {
			return fallbackResponse;
		}
		return new Response('Network error happened', {
			status: 408,
			headers: { 'Content-Type': 'text/plain' },
		});
	}
};

const enableNavigationPreload = async () => {
	if (self.registration.navigationPreload) {
		await self.registration.navigationPreload.enable();
	}
};

self.addEventListener('activate', (event) => {
	event.waitUntil(enableNavigationPreload());
});

self.addEventListener('install', (event) => {
	event.waitUntil(
		addResourcesToCache([
			'./',
			'./index.html',
			'/assets/styles/core/all.css',
			'/assets/scripts/vivid-components.js',
			'/assets/scripts/live-sample.js',
		])
	);
});

self.addEventListener('fetch', (event) => {
	event.respondWith(
		cacheFirst({
			request: event.request,
			preloadResponsePromise: event.preloadResponse,
			fallbackUrl: './assets/images/vivid-logo.jpeg',
		})
	);
});

2つのユーティリティ関数がある: addResourcesToCacheリソースをキャッシュに追加する関数と putInCacheリクエストとそのレスポンスをキャッシュに入れることです。

どちらも キャッシュグローバルオブジェクトを使用します。 CacheStorageオブジェクトにアクセスできます。

cacheFirstここでマジックが起こる。キャッシュからレスポンスを取得しようとする。もし見つかったら、キャッシュされたレスポンスを返す。(12-15行目)

もしうまくいかなければ、プリロードからレスポンスを得ようとする。うまくいけば問題ない。キャッシュして、プリロードされたレスポンスを返す。(17-22行目)

これに失敗した場合は、ネットワーク(サーバーなど)からのリクエストに移り、レスポンスを取得し、キャッシュする。(24-27行目)

すべて失敗した場合は、画像とともにエラーを返すだけだ。(29-35行目)

サービスワーカーのライフサイクル

サービスワーカーのライフサイクル:

  1. 登録(もう経験済み)

  2. インストール

  3. アクティベーション

サービスワーカーはインストールフェーズを聞き、主要なリソースをキャッシュに追加します。

self.addEventListener('install', (event) => {
	event.waitUntil(
		addResourcesToCache([
			'./',
			'./index.html',
			'/assets/styles/core/all.css',
			'/assets/scripts/vivid-components.js',
			'/assets/scripts/live-sample.js',
		])
	);
});

ユーティリティ waitUntilユーティリティに注目してください。このユーティリティは、非同期処理が終了するのを待つので、競合状態を回避するのに役立ちます。

次に activateフェーズでは、コンテンツのプリロードを有効にします(このとき waitUntil).

const enableNavigationPreload = async () => {
	if (self.registration.navigationPreload) {
		await self.registration.navigationPreload.enable();
	}
};

self.addEventListener('activate', (event) => {
	event.waitUntil(enableNavigationPreload());
});

最後のステップは fetch.このリスナーはリクエストをインターセプトし、私たちの cacheFirst関数を使って処理できるようになります:

self.addEventListener('fetch', (event) => {
	event.respondWith(
		cacheFirst({
			request: event.request,
			preloadResponsePromise: event.preloadResponse,
			fallbackUrl: './assets/images/vivid-logo.jpeg',
		})
	);
});

注目すべきは respondWithユーティリティに注目してほしい。文字どおり、その言葉どおりのことをする。リクエストがあれば、どんなレスポンスでも返すことができる。この場合 cacheFirst.

サービスワーカーでバージョンを扱う方法

サービスワーカーのバージョンを更新したい時があるかもしれません。私たちの場合は、ライブラリのアップデートのために必要です。そのためには、サービスワーカーのファイルにバージョンを記述し、バージョン管理されたキャッシュを作成し、古いキャッシュを削除しなければなりません。

サービスワーカーでバージョン管理されたキャッシュを作成する方法

バージョンを追加するのは簡単だ: const VERSION = ‘3.17.0’;

これはリリースのたびに手動で変更できる。

例えば、我々のプロジェクトではバンドルにロールアップを使っているので、次のような "トリック "を施した:

  1. バージョンをこのように設定した: const VERSION = ‘SW_VERSION’;

  2. ビルド中に、バージョンを package.json

  3. ロールアップのreplaceプラグインを使って、ビルドごとにバージョンを設定しています。

私たちのセットアップをご覧ください こちら.

サービスワーカーでバージョン管理されたキャッシュを作成する方法

お気づきのように caches.openメソッドは文字列を受け付ける:

const cache = await caches.open(VERSION);

これは、後で参照できるキャッシュ名またはキャッシュIDを期待する。

サービスワーカーやウェブサイトのキャッシュを更新する必要があることはよくあります。例えば、ライブラリのバージョンを上げたり、ページに新しいセクションを追加したり、レスポンスに変更を加えるたびに、キャッシュの有効期限を待たずに表示させたい場合などです。

idとしてバージョンを使うことで、各バージョンのキャッシュに対応し、現在のバージョンを表示し、古いものを削除することができる。

サービスワーカーで古いキャッシュを削除する方法

これでバージョン管理されたキャッシュを削除できる。

関数を作ってみよう。 removeOldCache:

async function removeOldCache(event) {
	await caches.keys().then(function (keys) {
		return Promise.all(keys.filter(function (key) {
			return key !== VERSION;
		}).map(function (key) {
			return caches.delete(key);
		}));
	}).then(function () {
		return self.clients.claim();
	});
}

この関数はすべてのキャッシュ・キーに目を通し(2行目)、新しく有効化されたバージョンでないすべてのキーについて、それを削除する(6行目)。これが終わったら self.clients.claimメソッドを使用して、新しいサービスワーカーがすべてのタブをコントロールするようにブラウザに伝えます(9行目)。

この関数は活性化中に呼び出される:

self.addEventListener('activate', (event) => {
	event.waitUntil(removeOldCache(event));
	event.waitUntil(enableNavigationPreload());
});

キャッシュはすべてのサービスワーカーのグローバルキャッシュであることに注意してください。私たちの場合、安全にキャッシュを削除できますが、あなたの場合、複数のキャッシュがあるかもしれないので、必要なキャッシュだけを削除するためにバージョンに接尾辞をつけることを考えるかもしれません。

例えば、HTMLキャッシュとサーバー・リクエスト・キャッシュを分けたい場合だ。HTMLキャッシュはクライアントサイドに関連する部分を変更したときにクリアされ、サーバーリクエストキャッシュは他のタームで変化します。

サービスワーカーの更新

新しいサービス・ワーカーには待機期間がある。交代には数時間かかるかもしれません。インストールフェーズで self.skipWaiting()を追加することで、この待ち時間をスキップすることができます。

これで、Service Workerは、私たちが変更(例えば、ライブラリの新しいバージョンをアップロードする)すると、即座に更新されます。

概要

サービスワーカーはウェブ開発者にとって非常に強力なツールです。サーバーとクライアント間の通信を制御することができます。ここではコンテンツをキャッシュする典型的な例を紹介しましたが、他にも多くの使用例があります。

例えば、サーバーのレスポンスをキャッシュして返せば、ユーザーはオフラインの状態でもアプリケーションを使い続けることができる。

このキャッシュの例は、私たちの問題に対する解決策を示しています。しかし、Service Workersは開発における他の多くのニーズに応えてくれます。あなたの意見を聞かせてください。 ツイッターまたは VonageコミュニティSlack!

ありがとう オリア・ビトンそして ミキ・エズラ・スタンガーこの記事の丁寧なレビューをありがとう

シェア:

https://a.storyblok.com/f/270183/400x400/7bf76cb05c/yonatankra.png
Yonatan Kraボネージ・ソフトウェア・アーキテクト

ヨナタンは、C/C++からMatlab、PHP、javascriptまで、アカデミーや業界で素晴らしいプロジェクトに携わってきた。元Webiks社CTO、WalkMe社ソフトウェアアーキテクト。現在はVonageのソフトウェア・アーキテクトであり、eggheadのインストラクターでもある。