Files
2026-07-13 21:36:17 +08:00

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 环境中,每次冷启动都会重新执行模块级代码,但随后的热调用会复用已加载的资源,直至实例被回收。