Created
April 29, 2023 23:24
-
-
Save jacobparis/1e428524be3a31096ba3ecb35a7a15bb to your computer and use it in GitHub Desktop.
Server Timing Utilities for Remix
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type PerformanceServerTimings = Record< | |
string, | |
Array<PerformanceServerTiming> | |
> | |
/** | |
* Run this on the server to get a `time` function that can be used to time | |
* server-side operations and add them to the `Server-Timing` header. | |
*/ | |
export function getServerTiming() { | |
const serverTimings: PerformanceServerTimings = {} | |
return { | |
time<T>( | |
serverTiming: | |
| string | |
| { | |
name: string | |
description: string | |
}, | |
fn: Promise<T> | (() => T | Promise<T>), | |
) { | |
return time(serverTimings, serverTiming, fn) | |
}, | |
getHeaderField() { | |
return getServerTimeHeaderField(serverTimings) | |
}, | |
getServerTimingHeader() { | |
return { | |
"Server-Timing": getServerTimeHeaderField(serverTimings), | |
} | |
}, | |
} | |
} | |
export async function time<T>( | |
serverTimings: PerformanceServerTimings, | |
serverTimingParam: | |
| string | |
| { | |
name: string | |
description: string | |
}, | |
fn: Promise<T> | (() => T | Promise<T>), | |
) { | |
const start = performance.now() | |
const name = | |
typeof serverTimingParam === "string" | |
? serverTimingParam | |
: serverTimingParam.name | |
const description = | |
typeof serverTimingParam === "string" ? "" : serverTimingParam.description | |
if (!serverTimings[name]) { | |
serverTimings[name] = [] | |
} | |
let result: T | |
try { | |
result = typeof fn === "function" ? await fn() : await fn | |
} catch (error) { | |
void recordServerTiming(serverTimings, { | |
name, | |
description: "Error", | |
}) | |
// Re-throw the error so that the caller can handle it | |
throw error | |
} | |
void recordServerTiming(serverTimings, { | |
name, | |
description, | |
duration: performance.now() - start, | |
}) | |
return result | |
} | |
function recordServerTiming( | |
serverTimings: PerformanceServerTimings, | |
timing: { | |
name: string | |
description?: string | |
duration?: number | |
}, | |
) { | |
const serverTiming: PerformanceServerTiming = { | |
name: timing.name, | |
description: timing.description ?? "", | |
duration: timing.duration ?? 0, | |
toJSON() { | |
// https://w3c.github.io/server-timing/#dom-performanceservertiming-tojson | |
// we don't need this, but the PerformanceServerTiming spec asks for it | |
return JSON.parse(JSON.stringify(serverTiming)) | |
}, | |
} | |
serverTimings[timing.name].push(serverTiming) | |
} | |
export function getServerTimeHeaderField( | |
serverTimings: PerformanceServerTimings, | |
) { | |
// https://w3c.github.io/server-timing/#the-server-timing-header-field | |
return Object.entries(serverTimings) | |
.map(([name, timingInfos]) => { | |
// duration is in milliseconds with microseconds precision | |
const dur = timingInfos | |
.reduce((totalDuration, { duration }) => totalDuration + duration, 0) | |
.toFixed(3) | |
const desc = timingInfos | |
.map(({ description }) => description) | |
.filter(Boolean) | |
.join(" & ") | |
return [ | |
name.replaceAll(/(:| |@|=|;|,)/g, "_"), | |
// desc and dur are both optional | |
desc ? `desc=${JSON.stringify(desc)}` : null, | |
dur ? `dur=${dur}` : null, | |
] | |
.filter(Boolean) | |
.join(";") | |
}) | |
.join(",") | |
} |
https://www.jacobparis.com/guides/remix-server-timing
Adding this comment just for reference in case anyone (like me) wants to refer to the actual blog post that shows how to use this.
Thanks, great idea!
How can this be used with a HeadersFunction?
Thanks for the gist! I added a timeSync
to avoid the extra tick of await
when the timed function is not async. Probably an unnecessary optimization, but I use it to measure satori
calls.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
https://www.jacobparis.com/guides/remix-server-timing
Adding this comment just for reference in case anyone (like me) wants to refer to the actual blog post that shows how to use this.