为什么不该在每个请求里 provide Layer?
我让同一个 handler 跑三次:每次都 Effect.provide(Db.layer),Layer 构建了 3 次;换成一个 ManagedRuntime,只构建 1 次。
答案是:Effect.provide(layer) 每次运行都会用一个新的 memo map 把 Layer 构建一遍,放进 fetch handler 就是每个请求重建一次;ManagedRuntime 把构建结果放在模块级,跨请求复用。
在 Workers 上这件事更要紧,因为一个 isolate 会被很多请求复用,数据库客户端、配置、HTTP 客户端本该只构建一次。所谓 Layer,是对「怎么构建一个依赖」的描述,可以先看 Effect 服务与 Layer:把依赖写进类型里;ManagedRuntime 是持有构建结果的运行时,见 Effect 的 ManagedRuntime 是什么?。下面用一段代码把次数量出来。
三种写法各构建几次?
import { Context, Effect, Layer, ManagedRuntime } from "effect"
let builds = 0
class Db extends Context.Service<Db, { readonly id: number }>()("app/Db") {
static readonly layer = Layer.effect(Db, Effect.sync(() => Db.of({ id: ++builds })))
}
const handler = Db.use((db) => Effect.succeed(db.id))
async function main() {
// 反模式:每个请求 provide 一次,每次都是一个全新的 memo map
for (let i = 0; i < 3; i++) {
await Effect.runPromise(handler.pipe(Effect.provide(Db.layer)))
}
console.log("per-request provide:", builds)
// 正确:模块级 ManagedRuntime
builds = 0
const runtime = ManagedRuntime.make(Db.layer)
for (let i = 0; i < 3; i++) await runtime.runPromise(handler)
console.log("shared runtime:", builds)
// 两个 runtime 默认各有自己的 memo map;共享 memo map 才能共享构建结果
builds = 0
const memoMap = Layer.makeMemoMapUnsafe()
const r1 = ManagedRuntime.make(Db.layer, { memoMap })
const r2 = ManagedRuntime.make(Db.layer, { memoMap })
await r1.runPromise(handler)
await r2.runPromise(handler)
console.log("two runtimes, shared memoMap:", builds)
}
void main()输出是 per-request provide: 3、shared runtime: 1、two runtimes, shared memoMap: 1。
为什么每次 provide 都会重建?
因为每次运行 Effect.provide(layer) 用的都是一个新的 memo map,构建结果记在 memo map 里,新的 memo map 里什么都没有。放在 fetch handler 里,意味着每个请求都重新构建数据库客户端、配置、HTTP 客户端。
有多个 runtime 时,怎么共享构建结果?
第三段输出说明:如果你因为某种原因有多个 runtime,各自默认有独立的 memo map,需要显式传同一个 memoMap 才会共享构建结果。上面的写法是 Layer.makeMemoMapUnsafe() 创建一个,再用 ManagedRuntime.make(layer, { memoMap }) 传给每个 runtime。
Layer 记忆化在嵌套 provide 下的细节,见 Effect Layer 记忆化:v4 里嵌套 provide、fresh 与 local 到底各构建几次。
那请求相关的数据怎么办?
别放进 Layer。请求级数据(env、请求 id、租户)做成 Service,在 fetch 里用 provideService 注入,静态 Layer 留给模块级 runtime。完整写法见 Effect 在 Workers 上怎么组织 Runtime?。
版本是 effect 4.0.5,上面的代码我在 Node 下用 tsx 跑过,三个输出就是跑出来的结果。没有在 workerd 或 wrangler 里跑过,这里量化的是 Effect 自身的 Layer 构建次数,与具体平台无关。