4.3 KiB
4.3 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| 将静态 I/O 提升至模块级别 | HIGH | 避免每次请求重复执行文件/网络 I/O | server, io, performance, next.js, route-handlers, og-image |
将静态 I/O 提升至模块级别
影响:高(避免每次请求重复执行文件/网络 I/O)
在路由处理函数或服务端函数中加载静态资源(字体、Logo、图片、配置文件)时,请将 I/O 操作提升至模块级别。模块级别的代码仅在模块首次导入时执行一次,而非每次请求都执行。这可以消除本应在每次调用时反复执行的文件系统读取或网络请求,从而避免冗余开销。
错误示例(每次请求都读取字体文件):
// app/api/og/route.tsx
import { ImageResponse } from 'next/og'
export async function GET(request: Request) {
// 每次请求都会执行——代价高昂!
const fontData = await fetch(
new URL('./fonts/Inter.ttf', import.meta.url)
).then(res => res.arrayBuffer())
const logoData = await fetch(
new URL('./images/logo.png', import.meta.url)
).then(res => res.arrayBuffer())
return new ImageResponse(
<div style={{ fontFamily: 'Inter' }}>
<img src={logoData} />
Hello World
</div>,
{ fonts: [{ name: 'Inter', data: fontData }] }
)
}
正确示例(在模块初始化时加载一次):
// app/api/og/route.tsx
import { ImageResponse } from 'next/og'
// 模块级别:仅在模块首次导入时执行一次
const fontData = fetch(
new URL('./fonts/Inter.ttf', import.meta.url)
).then(res => res.arrayBuffer())
const logoData = fetch(
new URL('./images/logo.png', import.meta.url)
).then(res => res.arrayBuffer())
export async function GET(request: Request) {
// 等待已开始执行的 Promise
const [font, logo] = await Promise.all([fontData, logoData])
return new ImageResponse(
<div style={{ fontFamily: 'Inter' }}>
<img src={logo} />
Hello World
</div>,
{ fonts: [{ name: 'Inter', data: font }] }
)
}
正确示例(在模块级别使用同步 fs):
// app/api/og/route.tsx
import { ImageResponse } from 'next/og'
import { readFileSync } from 'fs'
import { join } from 'path'
// 模块级别的同步读取——仅在模块初始化时阻塞
const fontData = readFileSync(
join(process.cwd(), 'public/fonts/Inter.ttf')
)
const logoData = readFileSync(
join(process.cwd(), 'public/images/logo.png')
)
export async function GET(request: Request) {
return new ImageResponse(
<div style={{ fontFamily: 'Inter' }}>
<img src={logoData} />
Hello World
</div>,
{ fonts: [{ name: 'Inter', data: fontData }] }
)
}
错误示例(每次调用都读取配置):
import fs from 'node:fs/promises'
export async function processRequest(data: Data) {
const config = JSON.parse(
await fs.readFile('./config.json', 'utf-8')
)
const template = await fs.readFile('./template.html', 'utf-8')
return render(template, data, config)
}
正确示例(将配置和模板提升至模块级别):
import fs from 'node:fs/promises'
const configPromise = fs
.readFile('./config.json', 'utf-8')
.then(JSON.parse)
const templatePromise = fs.readFile('./template.html', 'utf-8')
export async function processRequest(data: Data) {
const [config, template] = await Promise.all([
configPromise,
templatePromise,
])
return render(template, data, config)
}
何时使用此模式:
- 为 OG 图片生成加载字体
- 加载静态 Logo、图标或水印
- 读取运行时不发生变化的配置文件
- 加载邮件模板或其他静态模板
- 在所有请求中保持不变的任何静态资源
何时不使用此模式:
- 因请求或用户而变化的资源
- 运行期间可能发生更改的文件(应改用带 TTL 的缓存)
- 如果一直加载会消耗过多内存的大文件
- 不应在内存中持久保存的敏感数据
借助 Vercel 的 [Fluid Compute](https://vercel.com/docs/fluid-compute),模块级缓存在多个并发请求共享同一函数实例时尤为有效。静态资源在请求之间保持加载在内存中,不会产生冷启动开销。
在传统 Serverless 环境中,每次冷启动都会重新执行模块级代码,但随后的热调用会复用已加载的资源,直至实例被回收。