Parallel hooks.server handle functions for performance
Waterfalls, making a series of async requests sequentially, can be a real performance killer that you should always want to avoid and SvelteKit provides features to enable that such as calling load
functions in parallel.
But one place you may accidentally introduce your own waterfalls is when using multiple Server hooks handle
functions. There are good reasons to do this, such as separating code into separate modules each dealing with different functionality, but if they are independent and async then when you use the sequence
helper function to combine them it means that they could be slowing each other down. sequence
makes them sequential (the clue’s in the name!) and sequental async = waterfalls = slower than it could be.
Examples of what some async handle functions would be might include:
Now you could combine everything into one giant handle
function with the code for different responsibilities all intertwined but there’s an easier way to keep the code modular while still allowing things to run in parallel.
The trick is to create a handle
function that will wrap them and run them in parallel. This will need to adhere to the Server hook Handle
type:
type Handle = (input: {
event: RequestEvent;
resolve(
event: RequestEvent,
opts?: ResolveOptions
): MaybePromise<Response>;
}) => MaybePromise<Response>;
But the child handle functions it calls will need to be slightly different because we don’t want all of them calling resolve(event)
as a normal handler
function would (this is used to pass control on to the next handler or to the internal SvelteKit rendering pipeline). Instead, we’ll create a simpler type for these child handlers to implement and a helper function to combine them:
import type { MaybePromise, RequestEvent } from '@sveltejs/kit'
export type ChildHandle = (input: { event: RequestEvent }) => MaybePromise<Response | void>
export function parallel(...handles: ChildHandle[]): Handle {
return async ({ event, resolve }) => {
const results = await Promise.all(handles.map(handle => handle({ event })))
for (const result of results) {
if (result) {
return result
}
}
return resolve(event)
}
}
We can now create child handlers by implementing the ChildHandle
type. In this example, we’re using the firebase-admin
SDK to verify a session cookie and set the locals.user
property for use by any server load
function. Note this function doesn’t return anything - the locals.user
property will be set if the request contained a valid session cookie, otherwise it will be null.
import type { ChildHandle } from './hooks.server'
import { auth } from './firebase.server'
export const handleAuth: ChildHandle = async ({ event }) => {
const { locals, cookies } = event
locals.user = null
const session = cookies.get('session')
if (session) {
try {
locals.user = await auth.verifySessionCookie(session)
} catch (e: any) {
console.error('error verifying session cookie', session, err)
}
}
}
Here’s another example that could set a locals.tenant
property to provide context for a SaaS site, based on the host name mapped to the service. In this example, the handler may return a 404 error response if the hostname isn’t mapped to a valid tenant.
import type { Cookies, Handle, RequestEvent } from '@sveltejs/kit'
import type { ChildHandle } from './hooks.server'
import { getSiteByHost } from './firebase.server'
export const handleTenant: ChildHandle = async ({ event }) => {
const { cookies, url } = event
locals.tenant = await getSiteByHost(url.hostname)
if (!locals.tenant) {
return new Response(undefined, { status: 404 })
}
}
Now we have these building blocks we can put them all together. The parallel
function is a valid handler and could be used directly, but there’s a good chance you may have some handlers that need to run separately or make sense to run before or after any parallel groups, so it can still be combined with the sequence
function too.
In this example, the request is first checked for being a potential bot and blocked if identified, saving the server doing any further database lookups and processing. Otherwise, the auth, tenant, and a logging handlers would all be run in parallel thereby speeding up performance.
Note that if any of these child functions returns a response, then it will be used as the response of the parallel
function and the check is in the order of the handlers listed.
import { sequence } from '@sveltejs/kit/hooks'
import { createHandler } from 'svelte-kit-bot-block'
import { handleAuth } from './handle-auth'
import { handleTenant } from './handle-tenant'
import { handleLogging } from './handle-logging'
const handleBots = createHandler()
export const handle = sequence(
handleBots,
parallel(handleAuth, handleTenant, logging),
)
Let me know what you think and if you can see any ways to improve this.