【暴力】Server-Timing HeadersをNext.jsで無理やり使う
前回に引き続きネク虐をしていきます。
この記事は大体 @saitoeku3 さん, yue4u さんの受け売りであることを予めお断りしておきます! ありがとうございます!!! 自分が見つけた部分はrequestオブジェクトが共有されてるやんけ!だけです。
Server Timingってなに?
Server-Timing - HTTP | MDN を読むかChatGPTに聞いてください
Next.js公式の動きは?
Feature Requestが送られてはいるけど実装はされていません
Add support for server-timing header · Issue #12382 · vercel/next.js
1リクエストに対する1グローバル状態を持つ方法
Next.jsでは、_appと_documentとpageの間ではContextを通じてオブジェクトをやり取りすることができます(非公式な方法です)
// pages/_app.tsx
YourApp.getInitialProps = async (ctx) => {
const documentCtx = ctx.ctx
documentCtx.CHIAN_BREAKER_PROPERTY = '治安悪'
}
// pages/_document.tsx
YourDocument.getInitialProps = async (ctx) => {
console.log(ctx.CHIAN_BREAKER_PROPERTY) // => '治安悪'
}
Contextを通してごにょごにょやることで1ページ1グローバル状態みたいなことができます。
ただしこの方法は、getInitialPropsでのみ利用可能な方法で、getServerSidePropsではContextオブジェクトが共有されないため、利用できません。
しかし調べてみたところrequestオブジェクトは全てのContextで共通のオブジェクトが利用されているため、このオブジェクトをキーとして、1リクエストに対する1つのグローバル状態を共有することができそうです。
今回はrequestをキーにしてグローバル状態を保持する方針で実装してみます。
ざっと書き下すと以下のような雰囲気です。
// なんか適当なところ.tsx
export const perRequestStore = new WeakMap<{}, string>();
// pages/テキトーなページ.tsx
export async function getServerSideProps(ctx) {
perRequestStore.set(ctx.req, 'こんにちは from gSSP')
}
// pages/gIPなテキトーなページ.tsx
Page.getInitialProps = async (ctx) => {
perRequestStore.set(ctx.req, 'こんにちは from gIP')
}
// pages/_document.tsx
YourDocument.getInitialProps = async (ctx) => {
// ...
if (ctx.res) {
console.log(perRequestStore.get(ctx.req))
// => 'こんにちは from gSSP' とか 'こんにちは from gIP' が出る
}
// ...
}
ServerTimingを記録してシリアライズする処理を書く
ここまで書けば聡明なみなさまなら「みなまで言うな」と声を揃えていただけると思いますが、無粋に書きます
type ServerTimingEntry = {
name: string
start?: number
dur?: number
desc?: string
}
const serverTimingsStore = new WeakMap<{}, Map<string, ServerTimingEntry>>();
export const ServerTiming = {
time(ref: WeakRef<{}>, name: string, desc?: string) {
const ident = ref.deref()
if (!ident) return
const timings = serverTimingsStore.get(ident) ?? new Map()
timings.set(name, { name, start: performance.now(), desc })
},
timeEnd(ref: WeakRef<{}>, name: string) {
const ident = ref.deref()
if (!ident) return
const entry = serverTimingsStore.get(ident)?.get(name)
if (entry?.start == null) throw new Error(`ServerTiming: Timer ${name} does not exist`)
entry.dur = performance.now() - entry.start;
},
toHeaders(ref: WeakRef<{}>) {
const ident = ref.deref()
if (!ident) return
const entries = serverTimingsStore.get(ident)?.get(name)
if (!entries) return
return [
'Server-Timing',
entries.map(entry => {
if (entry.dur == null) return null
const desc = entry.desc ? `; desc="${entry.desc.replaceAll('"', '')}"` : '';
return `${entry.name.replaceAll('/', '-')};dur=${entry.dur}${desc}`
})
.filter(s => s != null)
.join(',')
] as const
}
}
YourDocument.getInitialProps = async (ctx) => {
// ...
if (ctx.res) {
const serverTimingHeader = ServerTiming.toHeaders(new WeakRef(ctx.req))
ctx.res.setHeader(...serverTimingHeader)
}
// ...
}
あとはアプリの中のrequestが取れる場所で適当に呼ぶだけです。
Requestオブジェクトを変に保持しているとメモリリークの原因になります。例えばAPIリクエストの前後にフックを仕掛けて時間を計測したいときなど、遠いところからrequestを参照するときはWeakRefなどを使っておくと安心です。
以下の例のWeakRefは完全に蛇足です。
export async function getServerSideProps(ctx) {
ServerTiming.time(new WeakRef(ctx.req), 'API Request')
const data = await getSomeData()
ServerTiming.timeEnd(new WeakRef(ctx.req), 'API Request')
return { props: { data } }
}
VRoid HubではReduxにWeakRefを突っ込むことでActionからrequestを参照できるようにして、各APIの通信にかかった時間を計測するようにしています。
おわりです、よかったね。
大雑把に作ったから、細かい実装はあとからよしなに読み替えてね。<敬具 />
はなくら組代表 はなくら より
Discussion