WeakRef 不能自动清理缓存,必须配合 FinalizationRegistry 才能实现“对象回收、键即删除”;WeakRef 仅避免阻止 GC,其自身作为强引用滞留 Map 中会形成无效占位符,导致查找变慢、内存反升。

WeakRef 本身不能自动清理缓存池,必须搭配 FinalizationRegistry 才能实现“对象一回收、键就删除”的自动清理;单独用 WeakRef 存进 Map,只会让缓存表越积越多、查得越来越慢。
WeakRef 单独塞进 Map 是个陷阱
很多人把 new WeakRef(value) 包一层后直接塞进 Map 或 Object,以为这样缓存就“轻量”了。其实不是:WeakRef 实例本身是强引用对象,只要它还在 Map 里,这个 WeakRef 就不会被 GC;而一旦 value 被回收,weakRef.deref() 就返回 undefined,但你的 Map 里还留着这个“空壳”。长期运行后,缓存结构里全是无效占位符,查找变慢、内存不降反升。
关键点在于:WeakRef 只解决“能不能收走 value”,不解决“要不要删掉 key → weakRef 这条映射”。后者必须你主动做。
FinalizationRegistry 是唯一能感知真正回收的机制
FinalizationRegistry 的回调不是“对象快死了来提醒你”,而是“对象已被 GC,此刻已不可访问”。它只允许你在回调里做副作用操作,比如 cache.delete(key),绝不能访问原对象(它已经不存在了)。
- 注册时传的
heldValue必须是string或symbol—— 普通对象作 key 会导致后续无法匹配、清理失败 - 必须在创建
WeakRef后立刻调用registry.register(value, key, weakRef),且第三个参数unregisterToken必须是该WeakRef实例本身,否则将来无法精准注销(避免重复触发或漏触发) - 回调执行时机不确定,可能延迟几秒甚至更久,不能用于实时性要求高的逻辑(比如资源配额同步)
每次 set 都要同步完成三件事
构建大容量业务字典缓存池时,“存引用”和“清键值”必须拆开两步,且每条写入都得闭环。典型错误是把 registry.register() 放在 set() 外面、或复用同一个 registry 却没传对 unregisterToken,结果一个对象回收触发多次清理,或根本没触发。
正确做法是每次 set(key, value) 时,同步完成:
- 用
new WeakRef(value)包裹值 - 把
key → weakRef存入缓存Map - 立刻调用
registry.register(value, key, weakRef)—— 这里weakRef是注销凭证,确保将来能取消该注册(防止 key 冲突或重复注册)
容易被忽略的边界点
大容量场景下,FinalizationRegistry 回调的延迟和批量合并行为会放大影响:多个对象同时回收,可能被合并进一次回调,传入的 key 是数组而非单值;如果缓存池底层用了分片 Map 或带前缀的 key 命名,回调里 cache.delete(key) 就可能删错位置。更麻烦的是,Node.js 与 Chrome 对 registry 的触发策略不完全一致,本地测试通过不代表线上稳定 —— 最好在回调里加 typeof key === 'string' 校验,再做 delete。










