Skip to content

Instantly share code, notes, and snippets.

@jacobparis
Created April 29, 2023 23:24
Show Gist options
  • Save jacobparis/1e428524be3a31096ba3ecb35a7a15bb to your computer and use it in GitHub Desktop.
Save jacobparis/1e428524be3a31096ba3ecb35a7a15bb to your computer and use it in GitHub Desktop.
Server Timing Utilities for Remix
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(",")
}
@pbteja1998
Copy link

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.

@jacobparis
Copy link
Author

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!

@shmuli9
Copy link

shmuli9 commented Aug 14, 2024

How can this be used with a HeadersFunction?

@andrelandgraf
Copy link

andrelandgraf commented Oct 12, 2024

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