Server-Timing ヘッダの情報をフロントエンド解析に使う
Server-Timing というヘッダがあります。サーバーからブラウザへのレスポンスヘッダとして、リクエスト内で発生した指標を送ることができます。
Server-Timing: miss;desc="Cache Miss", hit;desc="Cache Hit", db;dur=53.2, app;dur=47.2
何に使うかというと、ブラウザの DevTools や JS API からこの値にアクセスすることができます。
(現代のブラウザサンドボックスやJSではサーバーから送られたすべてのヘッダを覗き見れるわけではありませんが、これは見える値の一つです)
サーバーから Server-Timing を返す方法
実験用の hono + deno で雑なサーバーをでっち上げます。
import { Hono } from "npm:hono@4.6.9";
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
type Post = {
title: string;
content: string;
};
type User = {
id: number;
name: string;
};
const renderHtml = (user: User, post: Post) => `
<html>
<head>
<title>Hello</title>
<meta charset="utf-8">
</head>
<body>
<h1>Hello</h1>
<div>
<h2>${post.title} by ${user.name}</h2>
<p>${post.content}</p>
</body>
</html>
`;
async function getPost(_userId: number): Promise<Post> {
const rand = Math.floor(Math.random() * 300);
await wait(rand);
return {
title: "Main Content",
content: `This is the main content. It took ${rand}ms to load.`,
};
}
async function getUser(): Promise<User> {
const rand = Math.floor(Math.random() * 200);
await wait(rand);
return {
id: 1,
name: "John Doe",
};
}
// server app
const app = new Hono();
app.get("/", async (c) => {
const userStarted = performance.now();
const user = await getUser();
const userDuration = performance.now() - userStarted;
const started = performance.now();
const post = await getPost(user.id);
const duration = performance.now() - started;
const html = renderHtml(user, post);
return c.html(html, 200, {
"Server-Timing": `post;dur=${duration},user;dur=${userDuration}`,
});
});
Deno.serve(app.fetch);
deno run -A server.ts
で起動して、 http://localhost:8000
にアクセスします。
やってること
- getUser で適当に遅延を入れて解決
- gutPost は getUser に依存して、適当に遅延を入れて解決
- この2つに遅延を
name;dur=time
の形式でServer-Timing
ヘッダに入れて返す
Chrome DevTools でこの値を確認する
これは GET http://localhost:8000
の Content-Type: text/html
のリクエストヘッダとして返却されています。
Chrome 131だと、 Chrome DevTools > Network > 対象リクエスト選択 > Timing で一番下までスクロールするとそのタイミングの内訳が見れます。
JS の PerformanceObserver から Server-Timing にアクセスする
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
entry.serverTiming.forEach((serverEntry) => {
console.log(
JSON.stringify(serverEntry, null, 2)
);
});
});
});
["navigation", "resource"].forEach((type) =>
observer.observe({ type, buffered: true }),
);
/*
{
"name": "post",
"duration": 217.5807999999961,
"description": ""
}
VM387:4 {
"name": "user",
"duration": 98.9096000000136,
"description": ""
}
*/
このAPIは基本的に Same-Origin のみ実行可能ですが、 Timing-Allow-Origin
これを実行できるオリジンを追加することもできます
運用方法
各種RUMやAPMではサーバー側では、分散トレーシング等で同等のデータを計測しているはずです。
これをクライアントのリクエストヘッダとして露出していいか?が争点になります。
フロントエンドのチューニング目線視点だと、ブラウザからはブラックボックス化されてる初期リクエストのTTFBやAPIレスポンスを解析する手がかりになります。
攻撃者視点だと、遅いメトリクスを知ることで効率よくDDOSを作ることが可能になるかもしれません。また、仮にシステムコールのようなプリミティブなデータを露出してしまうと、システムコールする経路がある事自体が知られてしまいます。
個人的な意見としては、計測作業が負荷にならない範囲でアプリケーションの開発者の視点で追加してしまっていい気がします。似たようなものとしてE2Eテスト用の data-testid
をHTMLでそのまま露出しているサイトも多いです(例: facebook)。
data-testid の場合は外部スクレイピングが捗るより、本番環境で E2E を実行出来るというアドの方が大きく、Server-Timing も同種のデータだと考えます。
これでもまだセキュリティ懸念がある場合、staging 環境のみ露出、特定のIDのユーザー(開発者)のみ露出、特定のトークンを持つ人だけ露出、といった段階的な制御ができれば十分な気がします。開発者用データですからね。
というわけで、外形監視のみでもこれがあればフロントエンドのパフォーマンス計測ができるよ、という話でした。
Discussion