news 2026/9/23 21:49:38

Relay 数据驻留与垃圾回收指南:理解 presence-of-data、Query Retention 与 GC 策略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Relay 数据驻留与垃圾回收指南:理解 presence-of-data、Query Retention 与 GC 策略

Relay 在客户端内存 Store 中缓存查询结果,以便在无需网络请求的情况下复用数据、快速渲染页面。但数据不会永远驻留:Relay 会通过垃圾回收(Garbage Collection)删除不再被任何组件引用的数据。本文基于 Relay 官方指南(presence-of-data.md)展开,系统讲解数据何时存在于 Store、何时被回收,以及如何通过environment.retaingcSchedulergcReleaseBufferSize精确控制数据的保留时长,帮助你在“复用缓存”与“控制内存”之间找到平衡。

数据何时存在:缓存的生命周期

理解 Relay 缓存复用的第一步,是弄清楚数据在 Store 中的生命周期(lifetime)——即数据是否存在于 Store 中,以及它会存在多久。

基本规律如下:

  • 查询首次被获取后,该查询及其变量对应的数据会写入 Relay Store;
  • 只要该查询正在屏幕上被渲染,其数据一般会持续存在;
  • 从未被获取过的查询,其数据自然缺失于 Store 中。

也就是说,数据的存在与否,首先取决于“是否被获取过”,其次取决于“是否仍在被使用”。但这里有一个不可避免的矛盾:随着应用不断获取不同的查询,Store 中的数据会无限累积,变得过大、过陈旧(stale)。为了控制内存占用,Relay 运行一个名为**垃圾回收(Garbage Collection)**的进程,删除不再使用的数据。

设计要点:垃圾回收的存在,与“复用缓存”的目标天然存在张力。如果数据被过早删除,后续再次复用时就会落空,导致必须重新发起网络请求才能渲染界面。因此,如何让想要复用的数据在需要的时间内保持缓存,是本节(乃至整条 reusing-cached-data 指南线)的核心命题。

Relay 的垃圾回收机制

Relay 对本地内存 Store 执行垃圾回收的方式是:删除任何不再被应用中任何组件引用的数据

从实现层面看,这一机制由 RelayModernStore 承担,关键逻辑如下:

  • 每个查询(operation)在 Store 内部对应一个root entry,记录在_roots(一个Map)中,并维护一个引用计数refCount(见 RelayModernStore.js#L115-L123);
  • 调用environment.retain()会使对应 root entry 的refCount加一;释放(dispose)时减一(见 RelayModernStore.js#L383-L440);
  • refCount降到 0 时,该查询的数据进入“可被回收”状态——但如果配置了 release buffer,会先进入缓冲期;
  • GC 的实际执行由_collect()生成器与_gcStep()分步驱动,通过gcScheduler调度(见 RelayModernStore.js#L854-L886)。

值得注意的细节:即使某个查询从未被显式 retain,只要gcReleaseBufferSize > 0且 release buffer 未满,查询数据在写入时也会被临时记入_roots并放入 release buffer,以便近期内复用(见_recordSourceOperation,RelayModernStore.js#L606-L634)。

:::note 通常你不需要操心垃圾回收与数据保留的配置——这些应由应用基础设施在RelayEnvironment层面统一配置。本文内容供理解与参考,属于“知道底层如何运转”的知识储备。 :::

Query Retention:手动保留查询数据

保留(retain)一个查询,是向 Relay 表明:该查询及其变量对应的数据不应被垃圾回收删除。多个调用方可以同时保留同一个查询;只要至少有一个调用方还在保留,该查询的数据就不会从 Store 中被删除。

默认行为:组件挂载期间的自动保留

默认情况下,任何使用useQueryLoader/usePreloadedQuery及其他相关 API 的查询组件,会在自身挂载期间自动保留所渲染的查询。组件卸载后,它们会**释放(release)**该查询——这意味着此后任意时刻该查询的数据都可能被删除。

这套自动保留逻辑对应 Store 中 root entry 引用计数的增减:组件挂载时计数加一,卸载时计数减一;归零即进入可回收状态。

在组件生命周期之外保留:environment.retain

如果需要在组件的生命周期之外继续保留某个查询,可以使用environment.retain(operationDescriptor)操作:

// 保留查询;这将阻止 Relay 对 // 该查询及其变量的数据执行垃圾回收 const disposable = environment.retain(queryDescriptor); // 释放 disposable 将放开对该查询及其变量数据的保留, // 意味着如果它没有被其他地方保留, // 就可能被 Relay 的垃圾回收随时删除 disposable.dispose();

如上所述,这样可以做到即使查询组件已卸载,仍继续保留查询数据,让其他组件或未来再次挂载的同一组件能够复用这些保留的数据。

完整示例:从 graphql 标签到 OperationDescriptor

environment.retain接收的是 Relay 内部的操作描述符(OperationDescriptor),而不是裸的graphql模板字符串。需要先用getRequest获取请求描述符,再通过createOperationDescriptor绑定具体变量:

const { createOperationDescriptor, getRequest, graphql, } = require('relay-runtime'); // graphql 查询对象 const query = graphql`...`; // 构造 Relay 内部的查询表示 const queryRequest = getRequest(query); const queryDescriptor = createOperationDescriptor( queryRequest, variables, ); // 保留查询;这将阻止 Relay 对 // 该查询及其变量的数据执行垃圾回收 const disposable = environment.retain(queryDescriptor); // 释放 disposable 将放开对该查询及其变量数据的保留, // 意味着如果它没有被其他地方保留, // 就可能被 Relay 的垃圾回收随时删除 disposable.dispose();

更完整的用法可参考 retaining-queries.md,其中同样强调:

Relay 会根据挂载的查询组件自动管理查询数据的保留,因此产品代码中通常不需要直接调用retain。对于高级或特殊场景,查询数据的保留应由基础设施层代码(如 Router)统一处理。

适用场景:典型的例子是路由级代码——当用户离开某个页面、组件卸载后,Router 可以在导航层面保留该页面对应的查询数据,使得用户返回时能瞬间渲染,而无须等待网络请求。

控制 Relay 的垃圾回收策略

目前可以在创建 Relay Store 时提供2 个选项来控制垃圾回收行为。它们的真实签名可在 RelayModernStore 构造函数 中确认。

GC Scheduler:何时执行 GC

gcScheduler是一个可传给 Relay Store 的函数,用于决定一次 GC 执行应该被调度到何时运行

// 示例调度函数 // 接收一个回调,并将其调度到未来的某个时间执行 function gcScheduler(run: () => void) { resolveImmediate(run); } const store = new Store(source, {gcScheduler});

要点:

  • 默认行为:如果不提供gcScheduler,Relay 会使用resolveImmediate来调度 GC。resolveImmediate是基于 Promise 的setImmediate替代方案,将回调放到微任务队列中尽快执行(见 resolveImmediate.js);
  • 自定义策略:可以提供自己的调度函数,让 GC 比默认行为更不激进,例如基于时间、基于 scheduler 优先级或其他启发式规则;
  • 约定:按惯例,实现不应立即执行回调(即不应同步调用run),而应将其延后到合适的时机。

源码中的调度链路为:scheduleGC()创建_gcRun生成器后调用gcScheduler(_gcStep)_gcStep每执行完一步若未完成,会再次调用gcScheduler调度下一步,从而把一次完整 GC 分摊到多次调度中(见 RelayModernStore.js#L854-L886)。此外,在乐观更新(optimistic update)期间可通过holdGC()挂起 GC,待恢复后再执行。

GC Release Buffer Size:释放缓冲大小

Relay Store 内部维护一个释放缓冲区(release buffer),用于在查询被其原始持有者释放后(默认发生在渲染该查询的组件卸载时),仍然临时保留指定数量的查询。这使得在用户返回之前访问过的页面、标签页或内容时,更有可能(且更容易)复用缓存数据

配置方式是在创建 Store 时指定gcReleaseBufferSize

const store = new Store(source, {gcReleaseBufferSize: 10});

要点:

  • 缓冲区大小为 0等价于没有释放缓冲区,查询一旦被释放就会被立即回收;
  • 默认大小:环境的 release buffer 大小为10

源码层面的对应关系(RelayModernStore.js#L169-L170):

this._gcReleaseBufferSize = options?.gcReleaseBufferSize ?? DEFAULT_RELEASE_BUFFER_SIZE; // 10

缓冲区的运作机制(RelayModernStore.js#L642-L651):

  • 被释放(refCount为 0)的 root entry 会被推入_releaseBuffer
  • 当缓冲区长度超过gcReleaseBufferSize时,最旧(最早加入)的条目被逐出(evict),并从_roots中删除,随后调度 GC;
  • 因此,缓冲区越大,越多的最近访问过的查询得以保留,复用命中率越高,但同时内存占用也越大——这是需要在内存与复用率之间权衡的配置。

测试用例describe('GC with a release buffer', ...)明确验证了该行为:例如“在 caller 释放后仍将数据保留在 release buffer 中”“如果数据不过期,即使只发布数据也会被保留在 release buffer 中”(见 RelayModernStore-test.js 附近的测试组)。这从侧面印证:release buffer 正是“页面/标签往返复用”的核心机制。

实践建议

综合官方指南与源码实现,可总结出如下实践建议:

  1. 默认不动 GC 配置:默认的gcSchedulerresolveImmediate)与gcReleaseBufferSize = 10已覆盖大多数场景。GC 与保留策略应由应用基础设施(Environment 层、Router 层)统一配置,产品代码一般无需干预;
  2. 需要跨页面复用数据时,用 retain:若某个查询的数据在组件卸载后仍需被复用(如返回上一页、切换标签页),在 Router 等基础设施层调用environment.retain()并在适当时机dispose(),是最直接、最可控的方式;
  3. 权衡 release buffer 大小:增大gcReleaseBufferSize可提升近期访问数据的复用命中率,但会增加内存占用;设为 0 则关闭缓冲、立即回收,适合对内存敏感的场景;
  4. 自定义 gcScheduler 控制回收时机:若 GC 过于频繁导致卡顿,可按时间或优先级延后调度;注意约定上不要同步执行回调。

延伸阅读

  • 缓存复用总览:reusing-cached-data/introduction.md
  • 数据是否过期:staleness-of-data.md
  • 获取策略(store-or-network等):fetch-policies.md
  • 手动保留查询的完整 API:retaining-queries.md
  • 核心实现:RelayModernStore.js、resolveImmediate.js
  • 行为验证测试:RelayModernStore-test.js、RelayModernEnvironment-Retain-test.js
  • 前端
  • 开发工具

【免费下载链接】relay

Relay is a JavaScript framework for building>项目地址:https://gitcode.com/gh_mirrors/relay29/relay

点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 21:49:02

Winform Ribbon控件源码实战:从编译集成到二次开发与避坑指南

简介:面向C# WinForm开发者的Ribbon控件源码包,聚焦Office风格后台界面的快速落地,适用于需要在桌面应用中重构工具栏、选项卡与命令面板的实战场景。资源共212个文件,其中126个cs文件构成核心实现,包括界面渲染、工具…

作者头像 李华
网站建设 2026/9/23 21:48:02

SaaS多租户架构设计:数据隔离、上下文透传与配额计费实战

简介:这份《SaaS架构设计》PDF文档面向希望系统掌握SaaS架构原理与实践的开发者、架构师及技术学习者,围绕多租户系统从需求分析到性能优化的完整设计链路展开。内容涵盖SaaS成熟度模型四级分级、RUP“41”视图模式(场景、逻辑、开发、过程、…

作者头像 李华
网站建设 2026/9/23 21:44:11

基于Python机器学习的网络入侵检测系统实战:KDD99预处理与模型对比

简介:基于Python机器学习的网络入侵检测系统源码与全部数据以压缩包形式提供,主要面向计算机相关专业正在完成课程设计、期末大作业的学生,以及需要项目实战练习的入门学习者。资源共16个文件,包含4个Python程序文件、2个KDD CUP数…

作者头像 李华
网站建设 2026/9/23 21:42:15

遥感舰船检测数据集实战:从VOC/YOLO格式到YOLO模型部署

简介:这份数据集面向遥感影像目标检测与舰船识别任务,提供Pascal VOC与YOLO两种通用标注格式,可直接用于训练和验证YOLO系列、SSD、Faster R-CNN等模型,适合高校实验室、算法工程师及竞赛选手。压缩包共2000个文件,含1…

作者头像 李华
网站建设 2026/9/23 21:40:33

PSO三参数联合优化RBF神经网络实战指南

简介:本资源是一个基于Python实现的PSO优化RBF神经网络的轻量级项目,面向机器学习初学者与算法实践者,聚焦于用粒子群优化算法自动调优径向基函数神经网络的关键参数(如中心、宽度、权值),提升非线性拟合与…

作者头像 李华