はじめに
この記事は、クソアプリ - Qiita Advent Calendar 2024 - Qiitaの7日目の記事です このアドベントカレンダーも今年で10周年で、おめでたい。オレ以外の毎年書いているいかれたメンバーは何人いるのだろうか?
今までの記事のリンク これを数えたらこの記事を入れてクソアプリが13個ある。アドベントカレンダーが10年皆勤賞で参加しているのに、3個多い。どうやら日常でもいらん物を作ってはブログを書いているらしい。1円にもならないというのもかかわらずだ。
さて、遡ること10年以上前、2000年代後、当時SEだった僕は美容室に努めていた地元の友人にこんな約束をした。 「いつか自分の美容室をオープンしたらHPをつくってやるよ」
経て2020年頃、ついに彼は独立し美容室がオープンした。約束を守る時が来たのだ。
制作費や運用費をもらわない代わりに好き勝手やらせてもらうことした。 飲食店や美容室のHPは必要な情報にアクセスしやすくするべきだと思っていた。
わかりやすいシンプルなサイトにしてやろうじゃないか。GoogleのSEO対策もしてやろう。
Googleに好かれつつ、シンプルなHPで可能な遊べる余白はなにか?
!?
"スピードの向こう側"だ。
オレが手に入れてやる・・・! "その領域"・・ "スピードの向こう側"を・・!!
今年のクソアプリは、お前らを連れて行ってやるぜ "スピードの向こう側" によォ!!
※注意 この記事は、2000年代のインターネットのノリと週刊マガジン連載されていた"疾風伝説 特攻の拓"(かぜでんせつ ぶっこみのたく) の薄い知識の影響を受けています
作ったもの
道楽で運用している地元の友だちの美容室のHP(隠しページあり)はこちら。
今回は、このHPを"スピードの向こう側へ" 走り抜けた軌跡を紹介していくぜ・・・!
HPのベース
Astroを使って静的サイトをSSGしている。 Astroはコンパイルすることでいらない情報を諸々削除たり圧縮したり表示を早くすることに特化したFW。 またページ内の一部のJavaScriptを遅延ロードして動かせ、Reactに対応している。
UIコンポーネントにTailwindベースのdaisyUIを使用
Prefetchを使うことで、ページ内のリンクをキャッシュして画面遷移時に表示早くするようにしている。
第一世代のデプロイ先Netlify
HPのホスティング先は慣れている Netlify いや
"電脳璃覇威" にデプロイした
パフォーマンスの計測したLighthouseの結果を見てくれ
こいつを見てどう思う?
!?
すごく大きいです100点満点です
Netlifyにホスティングしたキャッシュを使わない場合の表示速度は、トップページで500ms程度
ここまで十分速度も早い、必要な情報なコンテンツは全部実装しているし、画像もSVGにして容量を下げている。おまけにSEO対策もした。地方都市の美容室のHPにしては十分だ。
だが待ってほしい、今回のテーマは"スピードの向こう側"だ。
スピードの向こう側への旅はまだ始まったばかりだ・・・!!
第二世代のデプロイ先 Cloudflare
"電脳璃覇威"にも弱点がある。日本にCDNサーバのリージョンがないのだ
一番近くてシンガポール
そして通信プロトコルがHTTP/2に対応しているが "HTTP/3" には対応していない
CDNサーバが日本にあって、HTTP/3に対応している、
それでいてNetlify並に開発体験がいい、そして無料枠が大きいところ
そんな "都合" の良いサービスが有るか?あるわけがねェ・・・!
!?
!?
"雲羅宇怒陽炎"
"こいつ"だ・・・!!! ドエレーー ”COOOL”じゃん・・・?
CloudflareのWorkers & PagesにデプロイしてCDNをキャッシュヒットさせた状態で、トップページにアクセスすると。 30ms !!
もちろんレスポンスヘッダにCache-Controlも追加している、サーバにアクセスせずにディスクキャッシュ利用した場合は 3msを叩き出す。
だが、まだまだ足りない・・!もっとだ・・!もっとスピードの向こう側へ・・・!!
計測!計測!計測
トップページには、お知らせ情報にアクセスしてJsonを取得してカルーセルを表示するReactのパーツがある。
このAPIを高速化していくぜ!!!
お知らせ情報を取得するWebAPIの機能
美容室のブログであるアメブロにあるお知らせ情報だけをスクレイピングしてJSONに変換して返す。
CloudflareのWorkersにTypeScriptでコードを作成してデプロイしている。 このTypeScriptのコードで利用しているFWはHonoを使っている。Cloudflare上で動きルーティングその他速いらしい。 今回の機能のサイズでは速さを実感するほどものではないがFW自体のサイズが小さいので起動時に時間がかかることがないだろう。
WorkersのTypeScriptのコードでデータをキャッシュする方法は下記の3つのサービスでできそうだ。
Cacheは地域のデータセンターで保存されるらしいが、このデータセンターとCDNのエッジサーバで保存されるのだろうか?
KVの方が早いかもしれない。
D1はSQLiteがCDNエッジサーバで動くらしい、とても早そうだ。
一体どれが一番早いのだろうか?わからない。
それでは計測だ!!!
ローカル環境でベンチマークテスト
Workersはローカルで開発できるので、ベンチマーク用のコードをそれぞれ用意した
Cache版の計測用コード(クリックすると展開)
import { Context } from "hono"; export const benchmarkCache = async (c: Context) => { const cache = caches.default; const writeStart = performance.now(); for (let i = 0; i < 100; i++) { const key = `https://example.com/benchmark_key_${i}`; const value = new Response('benchmark_value'); await cache.put(new Request(key), value); } const writeEnd = performance.now(); const readStart = performance.now(); for (let i = 0; i < 100; i++) { const key = `https://example.com/benchmark_key_${i}`; const response = await cache.match(new Request(key)); const value = response ? await response.text() : null; } const readEnd = performance.now(); const totalWriteDuration = writeEnd - writeStart; const totalReadDuration = readEnd - readStart; const avgWriteDuration = totalWriteDuration / 100; const avgReadDuration = totalReadDuration / 100; return c.json({ totalWriteDuration, totalReadDuration, avgWriteDuration, avgReadDuration }); };
KV版の計測用コード(クリックすると展開)
import { Context } from "hono";
export const benchmarkKV = async (c: Context) => {
const writeStart = performance.now();
for (let i = 0; i < 100; i++) {
await c.env.kv.put(benchmark_key_${i}
, 'benchmark_value');
}
const writeEnd = performance.now();
const readStart = performance.now();
for (let i = 0; i < 100; i++) {
const value = await c.env.kv.get(`benchmark_key_${i}`);
}
const readEnd = performance.now();
const totalWriteDuration = writeEnd - writeStart;
const totalReadDuration = readEnd - readStart;
const avgWriteDuration = totalWriteDuration / 100;
const avgReadDuration = totalReadDuration / 100;
return c.json({ totalWriteDuration, totalReadDuration, avgWriteDuration, avgReadDuration });
};
D1版の計測用コード(クリックすると展開)
import { Context } from "hono";
export const benchmarkD1 = async (c: Context) => {
// テーブルが存在しない場合は作成
await c.env.d1.prepare(
CREATE TABLE IF NOT EXISTS benchmark_table (
key TEXT PRIMARY KEY,
value TEXT
)
).run();
// テーブルをクリア
await c.env.d1.prepare(`DELETE FROM benchmark_table`).run();
const writeStart = performance.now();
for (let i = 0; i < 100; i++) {
await c.env.d1.prepare(`INSERT INTO benchmark_table (key, value) VALUES (?, ?)`)
.bind(`benchmark_key_${i}`, 'benchmark_value')
.run();
}
const writeEnd = performance.now();
const readStart = performance.now();
for (let i = 0; i < 100; i++) {
const result = await c.env.d1.prepare(`SELECT value FROM benchmark_table WHERE key = ?`)
.bind(`benchmark_key_${i}`)
.first();
}
const readEnd = performance.now();
const totalWriteDuration = writeEnd - writeStart;
const totalReadDuration = readEnd - readStart;
const avgWriteDuration = totalWriteDuration / 100;
const avgReadDuration = totalReadDuration / 100;
return c.json({ totalWriteDuration, totalReadDuration, avgWriteDuration, avgReadDuration });
};
上記のベンチマーク用のWorkersのコードをローカルで動かすと書き込みはD1が一番早く、読み込みはKVが一番早い。
キャッシュ方式 | 合計書き込み時間 (ms) | 合計読み込み時間 (ms) | 平均書き込み時間 (ms) | 平均読み込み時間 (ms) |
---|---|---|---|---|
Cache | 95 | 35 | 0.95 | 0.35 |
KV | 102 | 17 | 1.02 | 0.17 |
D1 | 71 | 43 | 0.71 | 0.43 |
今回の使い方だとキャッシュいかに早く取り出して返すかが重要なので、読み込み速度を重視してKVを利用するのが良さそうだ。
Cloudflare Workers上で速度を計測
今度は、実際にCloudflareの環境でそれぞれの方式を使ってAPIを実装した。
そのアメブロをスクレイピングしたデータをキャッシュしたものを取り出して返すAPIの速度を比較すると下記のような結果となり、ベンチマークで計測しただけではわからなかった事が判明した。
キャッシュ方式 | 平均速度 | 備考 |
---|---|---|
キャッシュなし | 200ms | |
Cache | 25ms~50ms | 80-100msと上振れすることも多い |
KV | 30ms程度 | 少し間をおいて動かすときにアクセスする際にLambda的なのを起動するのか最初だけ500msぐらいかかる |
D1 | 480ms程度 | キャッシュなしより遅い、KVと同様に起動時は1秒以上かかる |
この結果から、API上のキャッシュは最初の起動が遅いが安定的なKVを利用することとした。 D1が早いことを期待したが少なくとも2024年現在では、今回の使い方ではKVに軍配が上がった。
どこにでもキャッシュを仕掛けろ!
キャッシュはできるだけ近いところに仕掛けるのが効果が高い
でキャッシュするほうが効果てきめんだ。
さぁここまでにCloudflareの利用によって、HTMLや画像など静的なファイルはCDNでキャッシュし、Cache-Controlをレスポンスヘッダーにつけることによってブラウザにキャッシュを利用している。
WebAPIではWorkersにおいてアメブロをスクレイピングしたデータをKVに保存することでキャッシュするようにした。
まだだ、まだまだ行ける。
!?
WebAPIのアクセスをクライアントでキャッシュしたらサーバにアクセスせずに済むんじゃぁないか?
こんな感じでuseEffectに仕込めばWebAPIアクセスもキャッシュ利用してキャンセルできる
const cachedArticles = localStorage.getItem('latestArticles'); const cachedTime = localStorage.getItem('cacheTime'); const now = new Date().getTime(); if (cachedArticles && cachedTime && now - cachedTime < 600000) { setLatestArticles(JSON.parse(cachedArticles)); setLoading(false); return; }
最終的結果
最終的なパフォーマンスを見て見よう
HA☆YA☆SU☆GI☆RU
さぁ各過程で工夫した箇所をもう一度まとめてみよう
項目 | 内容 |
---|---|
SSG FW | Astro |
他ページの先読み | Prefetch |
プロトコル | HTTP/3 |
CDNサーバ | 東京リージョン |
WebAPIの実行 | Cloudflareのエッジサーバ |
WebAPIのFW | Hono |
WebAPIのキャッシュ | KVの利用 |
ブラウザ静的ファイルのキャッシュ | Cache-Controlの利用 |
ブラウザWebAPIアクセスのキャッシュ | LocalStorageの利用 |
まとめ
いかがだっただろうか?見た目的には取り立てて派手さのないHPでも視線を変えると遊べる余白があることを証明できたのではないだろうか?
高速化の過程を見せることをお前らを"スピードの向こう側"に導くことができたのではないだろうか。