Ephem-Cache: CloudflareとQuick Tunnelsでメディア重めのローカルホストプレビューを高速化
Ephem-Cacheでlocalhostのプレビュー遅延を解消。Cloudflare Workersを使ってメディア重めの静的アセットをエッジキャッシュし、動的リクエストはローカルにトンネリングします。

Quick answer
Ephem-Cache: Cloudflareでローカルプレビューを高速化: quick comparison answer
Choose the tunnel tool based on the network model: public HTTPS URLs for webhooks and demos, private mesh access for internal apps, and managed infrastructure when policy controls matter most.
Which tunnel tool is best for public webhook testing?
Use a public HTTPS localhost tunnel with stable URLs. InstaTunnel focuses on webhook testing, demos, OAuth callbacks, and MCP endpoint workflows.
When should I choose a private network tool instead?
Choose a private mesh or Zero Trust tool when every user and service should stay inside a controlled private network.
はじめに:リモートフロントエンドのフィードバックループのボトルネック
メディア重視のランディングページを仕上げています。高解像度のWebP画像、ループする背景動画、いくつかの3Dアセットを含みます。すべてがノートパソコン上では瞬時に感じられます。開発サーバー(Vite、Next.jsなど)を起動し、リモートのデザイナーやプロダクトマネージャー、クライアントにライブレビューをしてもらいたい。
通常はトンネルを使います。ngrokやLocaltunnel、Cloudflare Tunnel(cloudflared)などのツールは、ローカルポートを公開URLに露出させます。HTMLやAPI呼び出しにはこれで十分です。ただし、重いメディアには構造的な弱点があります:すべてのアセットのバイトが、閲覧者ごとに、リロードごとに、あなたのマシンからアップロードリンクを通じて出て行きます。
- アップロードがボトルネック。 3人のレビュアーが同時にリンクを開くと、同じアップストリームパイプを競合します。45MBの動画がスムーズにストリーミングされても、アップリンクを3回越えるとカクつきます。
- プレビューが本番と異なる。 動画の停止や画像の遅さは、デプロイされたサイトよりも悪く見えるため、アップロード速度に関するフィードバックを受け取ることになります。
Ephem-Cacheは、この問題を解決する小さなパターンです。Cloudflare Workerがトンネルの前に配置され、最初のフェッチ後にCloudflareが重い静的メディアをキャッシュし、動的(HTML、API呼び出し、WebSocketによるホットリロード)リクエストはすべてあなたのマシンに直接フォワードします。
このガイドでは、アーキテクチャ、動作するWorker、セットアップを壊す開発サーバーの設定、そして知っておくべき制限とセキュリティのトレードオフについて解説します。ここでの詳細は、2026年10月14日時点のCloudflareの最新ドキュメントに基づいています。ベンチマーク数値は意図的に省いています。結果はあなたのアップリンク、ファイル、レビュアーの位置に依存します。測定方法も紹介します。
1. アーキテクチャ
[ リモートレビュアー ]
│
▼
[ Cloudflareエッジ:Workersキャッシュ有効なWorker ]
│
├── Cache HIT ──► Cloudflareのキャッシュから提供、Worker未実行
│
└── Cache MISS / 動的リクエスト
│
▼
WorkerがQuick TunnelのURL(trycloudflare.com)にプロキシ
│
▼
cloudflared ──► http://localhost:5173
ルーティングルールはシンプルです:
- キャッシュ可能なメディア(画像、動画、フォント、3Dアセット)は
Cache-Controlヘッダーを返し、Cloudflareにキャッシュさせます。最初のフェッチ後は、以降のリクエストはあなたのマシンを経由せずに応答可能です。 - その他すべて(HTML、APIルート、HMR WebSocket、サーバーサイドレンダリングページ)は
no-storeを付与し、すべてのリクエストで通過させます。
WorkersキャッシュとCache APIの違い
以前のパターンでは、Worker内でcaches.default(Cache API)を使用していました。Cloudflareのドキュメントでは、Workersキャッシュという別の機能が紹介されており、この用途にはこちらの方が適しています:
Cache API (caches.default) |
Workersキャッシュ | |
|---|---|---|
| 範囲 | ローカルデータセンター内のみ。内容は他に複製されません。 | 階層型:訪問者近くの下層キャッシュと、ネットワーク全体の集約層 |
| 動作場所 | カスタムドメイン(およびPages Functions)で動作確認済み | workers.dev、ルート、カスタムドメイン、サービスバインディングに対応 |
| 同時ミス | 連続して呼び出し可能 | 多数のリクエストを一つのキーでまとめてWorkerを一度だけ呼び出す |
| 部分コンテンツ | cache.putは206レスポンスに例外、完全なレスポンスのみキャッシュ |
CloudflareがRangeを除去し、200全体をキャッシュし、範囲指定も自動処理 |
| パージ | cache.deleteはローカルデータセンター内のみ。ゾーン全体のパージはゾーンのパージを使用 |
ctx.cache.purge()をWorker内から呼び出し、セクション5のオプションを適用 |
これらの違いは重要です。リクエストのまとめにより、最初のロード時に3人のレビュアーが45MBのアップロードを3回トリガーしないようにします。<video>のレンダリングには範囲リクエストの処理も必要です。カスタムドメイン不要で設定が簡単になります。
WorkersキャッシュにはWrangler 4.69.0以降が必要です。
2. セットアップ
ステップ1:開発サーバーとQuick Tunnelを起動
開発サーバー(Viteのデフォルトポートは5173)を起動し、別のターミナルでQuick Tunnelを開きます:
cloudflared tunnel --url http://localhost:5173
cloudflaredはランダムなhttps://<ランダムな言葉>.trycloudflare.comのURLを出力します。この部分にはCloudflareアカウントやドメインは不要です。
Quick Tunnelsの制限を理解しておきましょう。Cloudflareのドキュメントによると:
- 各トンネルは最大200のインフライトリクエストを処理。超過はHTTP
429を返します。 - Server-Sent Eventsは未サポート。 WebSocketは未リストですが、サポート対象です。
- SLAや稼働保証はなく、
cloudflared再起動ごとにURLは変わります。 config.yamlが.cloudflaredディレクトリに存在する場合はサポートされません。
200リクエストの上限はキャッシュを使うもう一つの理由です。キャッシュヒットはトンネルに到達しません。
ステップ2:開発サーバーのホストチェックを修正
このステップは見落としやすく、最も一般的な原因は空白ページの表示です。Workerがリクエストをフォワードするとき、Hostヘッダーはトンネルのホスト名であり、localhostではありません。現代の開発サーバーは未知のホストを拒否します。
Viteの場合、Blocked request. This host is not allowed.と表示されます。vite.config.tsでトンネルドメインを許可しましょう:
import { defineConfig } from "vite";
export default defineConfig({
server: {
// ドットを先頭に付けるとtrycloudflare.comのサブドメインすべてを許可
allowedHosts: [".trycloudflare.com"],
},
});
allowedHosts: trueはホストチェックを完全に無効にします。これでは開発サーバーがDNSリバインディング攻撃にさらされます。代替策として、cloudflaredは--http-host-header localhost:5173でヘッダーを書き換え可能です。
Next.jsは、/_next/webpack-hmrのHMR WebSocketを含む開発専用エンドポイントを同一オリジンリクエストに制限します。レビュアーが使用するホスト名(Workerのホスト名)をnext.config.jsのallowedDevOriginsに追加しましょう。さらにNext.js 16.1.7以降にアップグレードしてください。CVE-2026-27977は、バージョン16.0.1から16.1.6までの脆弱性で、next devがOrigin: nullのWebSocket接続を許可してしまうものでした。公開インターネットに開発サーバーを置く場合は修正済みのバージョンを使いたいです。
ステップ3:Workerプロジェクトを作成
npm create cloudflare@latest ephem-cache-worker
cd ephem-cache-worker
Cloudflareは新規プロジェクトにwrangler.jsoncを推奨しています。次の設定を使います:
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "ephem-cache-preview",
"main": "src/index.ts",
"compatibility_date": "2026-10-14",
"cache": { "enabled": true },
"vars": {
"LOCAL_ORIGIN": "https://replace-me.trycloudflare.com"
}
}
cache.enabledは、Worker実行前にCloudflareがキャッシュを確認する設定です。compatibility_dateは今日の日付に設定します。
ステップ4:Workerのコード
src/index.tsを次の内容に置き換えます。構文チェック済みですが、ご自身のプロジェクトで動作確認してください:
export interface Env {
LOCAL_ORIGIN: string; // 例:"https://random-words.trycloudflare.com"
PURGE_TOKEN?: string; // パージ用の秘密トークン(任意)
}
// レビュー中にキャッシュ価値の高い静的アセット(JSやCSSは除外)
const MEDIA =
/\.(png|jpe?g|gif|webp|avif|svg|ico|mp4|webm|mov|woff2?|ttf|otf|glb|gltf|ktx2|hdr)$/i;
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): PromiseResponse {
const url = new URL(request.url);
// オプション:このWorkerのキャッシュを手動でクリア
if (url.pathname === "/__ephem/purge" && request.method === "POST") {
if (!env.PURGE_TOKEN || request.headers.get("x-purge-token") !== env.PURGE_TOKEN) {
return new Response("Forbidden", { status: 403, headers: { "Cache-Control": "no-store" } });
}
const result = await ctx.cache.purge({ purgeEverything: true });
return new Response(result.success ? "Purged" : "Purge failed", {
status: result.success ? 200 : 500,
headers: { "Cache-Control": "no-store" },
});
}
const isUpgrade = request.headers.get("Upgrade")?.toLowerCase() === "websocket";
const isCacheableMedia =
request.method === "GET" && !isUpgrade && MEDIA.test(url.pathname);
// リクエストをトンネル先に向ける
const target = new URL(request.url);
const origin = new URL(env.LOCAL_ORIGIN);
target.protocol = origin.protocol;
target.host = origin.host;
const upstreamReq = new Request(target, request);
if (isCacheableMedia) {
// キャッシュに完全なレスポンスを保存させるために条件付きリクエストヘッダーを削除
upstreamReq.headers.delete("If-None-Match");
upstreamReq.headers.delete("If-Modified-Since");
upstreamReq.headers.delete("Range");
}
let upstream: Response;
try {
upstream = await fetch(upstreamReq);
} catch {
return new Response("Ephem-Cache: ローカルトンネルに接続できません。", {
status: 502,
headers: { "Content-Type": "text/plain", "Cache-Control": "no-store" },
});
}
// WebSocketアップグレード(HMR)はそのまま通す
if (isUpgrade || upstream.status === 101) return upstream;
const res = new Response(upstream.body, upstream);
if (isCacheableMedia && upstream.status === 200) {
res.headers.set("Cache-Control", "public, max-age=3600");
} else {
res.headers.set("Cache-Control", "no-store");
}
return res;
},
} satisfies ExportedHandlerEnv;
このコードのいくつかの設計上のポイントを解説します:
メディア以外のレスポンスは明示的にno-storeです。 Workersキャッシュが有効な場合、200レスポンスにCache-ControlヘッダーがなくてもRFC 9111のヒューリスティックに従いキャッシュされますが、no-storeを明示しないとHTMLやAPIレスポンスが古いままキャッシュされる可能性があります。
WebSocketアップグレードはそのまま返します。 CloudflareはWebSocketアップグレードリクエストをWorkersキャッシュをバイパスし、Workerを必ず呼び出します。これによりホットリロードが動作し続けます。
条件付きリクエストと範囲リクエストはメディアパスから除去します。 開発サーバーは条件付きリクエストに304を返し、304はボディを持たないためキャッシュに適しません。CloudflareはRangeも除去し、キャッシュされた完全レスポンスからリクエストされた範囲を返します。Workerが206を返すとキャッシュされません。
Workerは小さく保つ。 リクエストのフォワードとヘッダー設定だけを行います。無料プラン・有料プランともにサブリクエストとCPU時間が許容されています。
ステップ5:現在のトンネルURLでデプロイ
トンネルURLはセッションごとに変わるため、デプロイ時に渡す必要があります。ファイルを編集せずに済むように:
npx wrangler secret put PURGE_TOKEN # オプション
npx wrangler deploy --var LOCAL_ORIGIN:https://あなたの-current-tunnel.trycloudflare.com
wrangler deployは--var key:valueを設定ファイルのvarsブロックの代わりに使えます。両方設定されている場合、コマンドラインの値が優先されます。デプロイごとにキャッシュキーもリセットされるため、古い資産を引き継ぎません。これにより、短いセッションごとに新しいキャッシュが作られます。
両コマンドをパッケージスクリプトにまとめておくと、新しいセッションは一つのコマンドで完了します。
3. キャッシュの挙動
キャッシュキーとバイパスルールを理解しておくと混乱を避けられます:
- キーはエントリポイント(パス+クエリ文字列)と、デフォルトではWorkerのバージョンです。 ホスト名はキーに含まれません。
?a=1&b=2と?b=2&a=1は異なるキーです。User-AgentやCookieなどのリクエストヘッダーはVaryを設定しない限りキャッシュに影響しません。 GETとHEADのみキャッシュ対象。 他のメソッドは常にWorkerに到達します。- 自動バイパスルール。
Set-Cookieを含むレスポンスや、Authorizationヘッダーを持つリクエスト(レスポンスがpublicに明示されていない場合)、privateやno-storeとマークされたレスポンスは通常キャッシュされません。 Cf-Cache-Statusで結果を確認。MISSはWorkerが実行されてキャッシュに保存された状態、HITはCloudflareがWorkerを呼び出さずに応答した状態、BYPASSはキャッシュがスキップされた状態です。- 最初のアクセスは遅い。 大きな動画のコールドリクエストはマシンからフルパスで送信されます。Workersキャッシュはフルレスポンスを保存するため、最初の訪問者はファイル全体のコストを負担します。レビュアーの前にページを一度開いてキャッシュを温めておきましょう。
- サイズ制限あり。 CDNキャッシュの制限は、Free・Pro・Businessプランで512MB、Enterpriseで5GBです。これを超えるアセットはキャッシュされません。
4. 無効化:何をキャッシュし、どれくらいの期間キャッシュするか
ローカルファイルは頻繁に変わるため、古いアセットが問題になります。基本方針:
- メディアはキャッシュ、コードはキャッシュしない。 開発サーバーのソースファイルは安定した名前を持ち、内容が変わっても名前は変わりません。Viteはソースモジュールリクエストを条件付きにし(
304)、プリバンドルされた依存関係にはmax-age=31536000,immutableを設定します。.jsや.cssを1時間のエッジキャッシュに置くと混乱のもとなので、正規表現から除外します。 - TTLは短めに。 1時間を目安にします。レビュアーが画像を差し替える場合は
max-ageを下げてください。 - オンデマンドでパージ。 Workerのオプションエンドポイントから
ctx.cache.purge({ purgeEverything: true })を呼び出します:curl -X POST https://<あなたのWorkerホスト>/__ephem/purge -H "x-purge-token: $PURGE_TOKEN"
purgeはtagsやpathPrefixesも受け付けます。WorkersキャッシュはあなたのWorkerに限定され、ダッシュボードやAPIからのゾーンレベルのパージには影響しません。パージは無料プランのレート制限内で行われます。
4. 再デプロイでリセット。 Workerのバージョンはキーの一部なので、wrangler deployは完全な無効化になります。
5. 本番ビルドのプレビューで最終確認。 vite buildを実行し、vite previewで出力を提供すると、アセット名にコンテンツハッシュが付くため、変更されたファイルは自動的に新URLになります。この段階でjs|css|mjsを正規表現に追加できます。
5. 自分で測定する方法
特定のセットアップに依存しない数値を引用する代わりに、自分で測定しましょう:
# 1. 最初のリクエスト:Cf-Cache-Status: MISSを期待
curl -sI https://<workerホスト>/media/hero.mp4 | grep -i cf-cache-status
# 2. 2回目のリクエスト:HITを期待
curl -sI https://<workerホスト>/media/hero.mp4 | grep -i cf-cache-status
# 3. ダウンロード時間をトンネルURLと比較
curl -s -o /dev/null -w "worker: %{time_total}s %{size_download} bytes\n" https://<workerホスト>/media/hero.mp4
curl -s -o /dev/null -w "tunnel: %{time_total}s %{size_download} bytes\n" https://<your-tunnel>.trycloudflare.com/media/hero.mp4
実際の状況を把握するには、レビュアーの地域のマシンから同じ比較を行うか、レビュアーにDevToolsのNetworkタブを開いてHARファイルを共有してもらいます。最初のフェッチ後にキャッシュされたバージョンがほぼアイドル状態になるのを確認してください。
6. 制限、条件、コスト
- Workers Freeプラン: 1日10万リクエスト、1リクエストあたり10msのCPU、50サブリクエスト。ストリーミングプロキシはCPU負荷が少ないです。ただしキャッシュを有効にすると、キャッシュヒットも含めてすべてのリクエストが標準のリクエスト料金で課金されます。
- Cloudflareの大容量ファイルに関する規約。 Cloudflareの公式ブログでは、StreamやImages、R2などのCloudflareサービスでホストされた動画や大容量ファイルはCDN経由で配信可能と変更されています。外部ホスティングの動画や大容量ファイルは引き続き制限対象です。あなたのラップトップへのトンネルは外部オリジンです。大容量動画をこの設定で配信する前に、Cloudflareの最新のサービス規約を確認し、短いレビューセッションに留めてください。長期的なメディアはR2にホストしましょう。
- SLAなし。 Quick Tunnelsはテストツールです。重要な運用にはCloudflare Tunnelを使ってください。
7. セキュリティ上の注意点
このパターンは未公開のサイトを公開インターネットに置くため、いくつかのポイントに注意が必要です:
- キャッシュヒットはWorkerをスキップ。 Worker内で書いたトークンやパスワードはキャッシュミス時のみ動作します。メディアファイルがキャッシュされると、そのURLを知る誰でもアクセス可能です。秘密や embargo資産には使わないでください。
- トンネルURLも公開される。 ランダムな
trycloudflare.comアドレスを知る人は、Workerを経由せずに直接開発サーバーにアクセスできます。Cloudflareのドキュメントには--allowed-mailオプションがあり、一時PINを入力させる仕組みもありますが、このアーキテクチャと併用できません。実用的な代替案は、Workerがアップストリームリクエストに秘密のヘッダーを付与し、それを持たないリクエストを拒否する小さなViteプラグインを作ることです:// vite.config.ts(抜粋) const guard = { name: "ephem-guard", configureServer(server) { server.middlewares.use((req, res, next) => { if (req.headers["x-ephem-secret"] !== process.env.EPHEM_SECRET) { res.statusCode = 403; return res.end("Forbidden"); } next(); }); }, };
これはHTTPリクエストのみをカバーします。秘密はリポジトリに含めないでください。
- 秘密情報をバンドルに漏らさない。 クライアント側のコードや環境変数は、訪問者が読めるファイルに含まれます。データベースURLや秘密APIキーはサーバー側に置いてください。
- ホストの許可リストを絞る。
.trycloudflare.comを使い、ホストチェックを無効にしない。 - レビュー後に破棄。
cloudflaredを停止し、Workerを削除(npx wrangler delete)またはキャッシュをパージし、PURGE_TOKENをローテーションしてください。
結論
ローカルのトンネルは未完成の作業を最速で見せる方法ですが、すべての画像や動画の配信ネットワークとなってしまいます。Cloudflare WorkerとWorkersキャッシュを前に置くことで、重いメディアのリピートトラフィックをCloudflareのキャッシュに移し、あなたのマシンは最初のリクエストと動的リクエストだけを処理します。
このパターンには注意点もあります:最初の表示は遅いまま、キャッシュされたファイルはほぼ公開状態、Quick Tunnelsは稼働保証なし、Cloudflareの規約は外部ホスティングの大容量ファイルを制限しています。ただし、これらの制約内で、メディア重めのページのリモートレビューをアップロード速度にほとんど依存させなくできます。短いセッションから始め、Cf-Cache-Statusを確認し、自分のファイルで効果を測定してください。
出典
- Cloudflare Docs: Quick Tunnels
- Cloudflare Docs: Workers Cache overview
- Cloudflare Docs: Workers Cache configuration
- Cloudflare Docs: Workers Cache keys
- Cloudflare Docs: Purge and invalidate the cache
- Cloudflare Docs: Workers Cache limitations
- Cloudflare Docs: Cache API
- Cloudflare Docs: How the cache works
- Cloudflare Docs: Workers limits
- Cloudflare Docs: Wrangler commands
- Cloudflare Docs: Wrangler configuration
- Cloudflare Blog: Goodbye, section 2.8 and hello to Cloudflare’s new terms of service
- Vite: Dependency Pre-Bundling
- Vite: Why Vite
- Quasar: Opening a dev server to the public (allowedHosts guidance)
- Parallel Works: tunneling framework dev servers (Next.js allowedDevOrigins)
- CVE-2026-27977 (Next.js dev HMR WebSocket origin check)
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Related Topics
Keep building with InstaTunnel
Read the docs for implementation details or compare plans before you ship.