weakref 仅提供弱持有,不自动清理或通知回收,必须配合 finalizationregistry 手动清理;finalizationregistry 在对象被 gc 后回调并返回注册时的 hold value(如 key),用于从 map 中删除对应条目;但回调不及时,需结合 deref() 懒清理、定时扫描和内存压力触发的主动驱逐来保障缓存可靠性。

WeakRef 本身不提供过期能力,必须配合 FinalizationRegistry 手动触发清理
WeakRef 只是“弱持有”,它不会自动删除 Map 中的 key,也不会通知你对象何时被回收。常见错误是只用 WeakRef 包裹值、存进 Map,却忘了清理对应 key,导致 key 泄漏(key 是字符串或 symbol,永远强引用,不会被 GC)。真正能捕获回收时机的只有 FinalizationRegistry,且它只接收一个注册时传入的“hold value”(通常设为 key),在目标对象被回收时回调并把该 hold value 还给你。
实操建议:
- 每个资源项写入缓存时,同时调用
registry.register(obj, key, holdings),其中obj是你要弱持有的资源对象(如Blob、ArrayBuffer或解码后的ImageBitmap),key是字符串 ID,holdings可选(比如附加元数据) - 在 registry 回调中,从主缓存
Map里用cache.delete(key)移除条目 - 不要在回调里尝试访问
obj—— 它已经不可达;你只能依赖key做索引清理
资源池需区分“存活判定”和“过期判定”,WeakRef 只管前者
浏览器离线资源池常需双重淘汰:一是对象被 GC 回收(生命周期结束),二是逻辑过期(如缓存时间超 24 小时、使用次数超阈值、内存压力触发 LRU 驱逐)。WeakRef + FinalizationRegistry 只解决第一类,第二类必须独立实现。
推荐结构:
- 主缓存用
Map<string ref: weakref>, timestamp: number, used: number }></string>,记录创建时间与访问频次 - 定时任务(如每分钟)扫描
Map,调用ref.deref()检查是否还存活,再结合timestamp判断是否逻辑过期 - 内存紧张时(监听
navigator.storage.estimate()或performance.memory),主动调用cache.forEach(...)清理最旧/最少用项,不依赖 GC
FinalizationRegistry 回调不保证及时性,生产环境必须加 fallback 清理
FinalizationRegistry 的回调由 JS 引擎在 GC 后某个不确定时机执行,可能延迟数秒甚至更久(尤其在页面后台运行时)。这意味着 key 在 Map 中可能残留较长时间,占用内存且干扰统计。
规避方案:
- 所有
get(key)操作前,先调用ref.deref(),返回null时立即cache.delete(key)—— 这是最轻量的“懒清理” - 维护一个
Set<string></string>记录“待确认回收”的 key,在每次set()时加入,get()时移出;空闲时遍历该 Set 做批量deref()校验 - 禁用
registry.unregister():它无法可靠取消已注册项,反而增加复杂度
避免在 WeakRef 中包裹 DOM 节点或跨 iframe 对象
WeakRef 对某些宿主对象支持不稳定:HTMLElement 在部分 Chromium 版本中可被 WeakRef 持有,但 Safari 16.4+ 才开始支持;Window 或跨域 iframe.contentWindow 对象则完全不可弱引用,调用 new WeakRef(obj) 会直接抛 TypeError。
安全做法:
- 只对纯 JS 对象(
Object、ArrayBuffer、Blob、ImageBitmap)、Worker 线程传递的结构化数据使用WeakRef - 对 DOM 节点改用事件监听 +
weakrefpolyfill(如weakmap-polyfill)或显式.remove()后清空引用 - 初始化时用
typeof WeakRef !== 'undefined'和typeof FinalizationRegistry !== 'undefined'双重检测,降级为普通Map+ 手动delete
真正难的是协调 GC 时机与业务语义——WeakRef 不是银弹,它只帮你守住“不延长生命周期”这一条底线;过期策略、容量控制、错误降级,全得靠你自己在它之上搭一层确定性逻辑。











