| 1 | --- |
| 2 | title: Hoist Static I/O to Module Level |
| 3 | impact: HIGH |
| 4 | impactDescription: avoids repeated file/network I/O per request |
| 5 | tags: server, io, performance, next.js, route-handlers, og-image |
| 6 | --- |
| 7 | |
| 8 | ## Hoist Static I/O to Module Level |
| 9 | |
| 10 | **Impact: HIGH (avoids repeated file/network I/O per request)** |
| 11 | |
| 12 | When loading static assets (fonts, logos, images, config files) in route handlers or server functions, hoist the I/O operation to module level. Module-level code runs once when the module is first imported, not on every request. This eliminates redundant file system reads or network fetches that would otherwise run on every invocation. |
| 13 | |
| 14 | **Incorrect (reads font file on every request):** |
| 15 | |
| 16 | ```typescript |
| 17 | // app/api/og/route.tsx |
| 18 | import { ImageResponse } from 'next/og' |
| 19 | |
| 20 | export async function GET(request: Request) { |
| 21 | // Runs on EVERY request - expensive! |
| 22 | const fontData = await fetch( |
| 23 | new URL('./fonts/Inter.ttf', import.meta.url) |
| 24 | ).then(res => res.arrayBuffer()) |
| 25 | |
| 26 | const logoData = await fetch( |
| 27 | new URL('./images/logo.png', import.meta.url) |
| 28 | ).then(res => res.arrayBuffer()) |
| 29 | |
| 30 | return new ImageResponse( |
| 31 | <div style={{ fontFamily: 'Inter' }}> |
| 32 | <img src={logoData} /> |
| 33 | Hello World |
| 34 | </div>, |
| 35 | { fonts: [{ name: 'Inter', data: fontData }] } |
| 36 | ) |
| 37 | } |
| 38 | ``` |
| 39 | |
| 40 | **Correct (loads once at module initialization):** |
| 41 | |
| 42 | ```typescript |
| 43 | // app/api/og/route.tsx |
| 44 | import { ImageResponse } from 'next/og' |
| 45 | |
| 46 | // Module-level: runs ONCE when module is first imported |
| 47 | const fontData = fetch( |
| 48 | new URL('./fonts/Inter.ttf', import.meta.url) |
| 49 | ).then(res => res.arrayBuffer()) |
| 50 | |
| 51 | const logoData = fetch( |
| 52 | new URL('./images/logo.png', import.meta.url) |
| 53 | ).then(res => res.arrayBuffer()) |
| 54 | |
| 55 | export async function GET(request: Request) { |
| 56 | // Await the already-started promises |
| 57 | const [font, logo] = await Promise.all([fontData, logoData]) |
| 58 | |
| 59 | return new ImageResponse( |
| 60 | <div style={{ fontFamily: 'Inter' }}> |
| 61 | <img src={logo} /> |
| 62 | Hello World |
| 63 | </div>, |
| 64 | { fonts: [{ name: 'Inter', data: font }] } |
| 65 | ) |
| 66 | } |
| 67 | ``` |
| 68 | |
| 69 | **Correct (synchronous fs at module level):** |
| 70 | |
| 71 | ```typescript |
| 72 | // app/api/og/route.tsx |
| 73 | import { ImageResponse } from 'next/og' |
| 74 | import { readFileSync } from 'fs' |
| 75 | import { join } from 'path' |
| 76 | |
| 77 | // Synchronous read at module level - blocks only during module init |
| 78 | const fontData = readFileSync( |
| 79 | join(process.cwd(), 'public/fonts/Inter.ttf') |
| 80 | ) |
| 81 | |
| 82 | const logoData = readFileSync( |
| 83 | join(process.cwd(), 'public/images/logo.png') |
| 84 | ) |
| 85 | |
| 86 | export async function GET(request: Request) { |
| 87 | return new ImageResponse( |
| 88 | <div style={{ fontFamily: 'Inter' }}> |
| 89 | <img src={logoData} /> |
| 90 | Hello World |
| 91 | </div>, |
| 92 | { fonts: [{ name: 'Inter', data: fontData }] } |
| 93 | ) |
| 94 | } |
| 95 | ``` |
| 96 | |
| 97 | **Incorrect (reads config on every call):** |
| 98 | |
| 99 | ```typescript |
| 100 | import fs from 'node:fs/promises' |
| 101 | |
| 102 | export async function processRequest(data: Data) { |
| 103 | const config = JSON.parse( |
| 104 | await fs.readFile('./config.json', 'utf-8') |
| 105 | ) |
| 106 | const template = await fs.readFile('./template.html', 'utf-8') |
| 107 | |
| 108 | return render(template, data, config) |
| 109 | } |
| 110 | ``` |
| 111 | |
| 112 | **Correct (hoists config and template to module level):** |
| 113 | |
| 114 | ```typescript |
| 115 | import fs from 'node:fs/promises' |
| 116 | |
| 117 | const configPromise = fs |
| 118 | .readFile('./config.json', 'utf-8') |
| 119 | .then(JSON.parse) |
| 120 | const templatePromise = fs.readFile('./template.html', 'utf-8') |
| 121 | |
| 122 | export async function processRequest(data: Data) { |
| 123 | const [config, template] = await Promise.all([ |
| 124 | configPromise, |
| 125 | templatePromise, |
| 126 | ]) |
| 127 | |
| 128 | return render(template, data, config) |
| 129 | } |
| 130 | ``` |
| 131 | |
| 132 | When to use this pattern: |
| 133 | |
| 134 | - Loading fonts for OG image generation |
| 135 | - Loading static logos, icons, or watermarks |
| 136 | - Reading configuration files that don't change at runtime |
| 137 | - Loading email templates or other static templates |
| 138 | - Any static asset that's the same across all requests |
| 139 | |
| 140 | When not to use this pattern: |
| 141 | |
| 142 | - Assets that vary per request or user |
| 143 | - Files that may change during runtime (use caching with TTL instead) |
| 144 | - Large files that would consume too much memory if kept loaded |
| 145 | - Sensitive data that shouldn't persist in memory |
| 146 | |
| 147 | With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute), module-level caching is especially effective because multiple concurrent requests share the same function instance. The static assets stay loaded in memory across requests without cold start penalties. |
| 148 | |
| 149 | In traditional serverless, each cold start re-executes module-level code, but subsequent warm invocations reuse the loaded assets until the instance is recycled. |
| 150 |