【翻译】React编译器及其原理:为何类对象可能阻碍备忘录法生效

原文链接:anita-app.com/blog/articl...

作者:ilDon

本文反映了作者的个人观点与思考。由于作者并非英语母语者,最终表述经人工智能编辑以确保清晰度与准确性。

React编译器现已稳定并可投入生产环境(React博客,2025年10月7日),它显著减少了手动使用useMemouseCallbackReact.memo的需求。

这对大多数 React 代码库而言是重大利好,尤其适用于采用纯净函数组件和不可变数据的架构。但存在一种模式正变得日益棘手:依赖类实例计算衍生值的类密集型对象模型。

若渲染时逻辑依赖类实例,编译器备忘录机制的精确度可能无法满足需求,开发者往往不得不重新引入手动备忘录机制以恢复控制权。

React编译器通过可观察依赖关系进行优化

官方文档说明React编译器会基于静态分析和启发式算法自动对组件和值进行备忘存储:

关键细节在于:备忘存储仍取决于React能观察到的输入内容。

在 React 中,对象的备忘比较基于引用(采用 Object.is 的语义)。memouseMemo 的文档都明确说明了这一点:

因此,如果有效值隐藏在对象实例内部,而该实例引用发生变化,React 就会认为值也发生了变化。

ElementClass 示例

假设你将元素建模如下:

JavaScript 复制代码
class ElementClass {
  constructor(private readonly isoDate: string) {}

  public getFormattedDate(): string {
    const date = new Date(this.isoDate);

    if (Number.isNaN(date.getTime())) {
      return 'Invalid date';
    }

    return date.toLocaleString('en-US', {
      year: 'numeric',
      month: 'short',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit',
      timeZoneName: 'short',
    });
  }
}

而在一个组件中:

JavaScript 复制代码
export function Row({ elementInstance }: { elementInstance: ElementClass }) {
  const formattedDate = elementInstance.getFormattedDate();
  return <span>{formattedDate}</span>;
}

这段代码是可读的。但从外部来看,相关的响应式输入实际上是 elementInstance(对象引用)。

如果状态管理层返回了一个新的 ElementClass 实例,React/编译器会检测到新的依赖关系,并重新计算格式化后的值------即使底层的 isoDate 字符串并未改变。

手动逃生舱门功能正常,但噪音较大

你可以强制使用更窄的依赖项:

JavaScript 复制代码
class ElementClass {
  constructor(public readonly isoDate: string) {} // <-- expose isoDate as a public property
  // unchanged
}

export function Row({ elementInstance }: { elementInstance: ElementClass }) {
  const formattedDate = useMemo(
    () => elementInstance.getFormattedDate(),
    [elementInstance.isoDate],
  );

  return <span>{formattedDate}</span>;
}

这确实可行,React 明确将 useMemo/useCallback 作为编译器环境下的逃生通道:

但此时我们又陷入了手动处理依赖关系的困境,还不得不将内部逻辑暴露给 UI。

编译器友好的替代方案:纯数据 + 纯辅助函数

若 UI 接收纯粹的不可变数据,依赖关系将变得显式且低成本:

JavaScript 复制代码
type Element = {
  isoDate: string;
};

export function Row({ element }: { element: Element }) {
  const formattedDate = DateHelpers.formatDate(element.isoDate);
  return <span>{formattedDate}</span>;
}

现在,DateHelpers.formatDate 的相关输入是一个基本类型(isoDate),而非隐藏在类实例方法调用背后的状态。这样,编译器就能将formatDate的输出进行备忘存储,仅将 isoDate 作为唯一依赖项------这个基本值在发生变化时会正确触发备忘存储机制。

有人可能会提出异议:即便在这个简单的对象示例中,整个element仍会被传递给组件。因此Row组件终究会重新渲染,唯一实质区别在于formattedDate不再被重新计算。

这种说法没错:若传递整个对象且其引用发生变化,该组件就会重新渲染。我们稍后将详细探讨这个问题。

在探讨该问题的解决方案之前,我想强调:对于大型应用而言,即使仅考虑派生值的备忘录化,类实例与普通数据之间的差异依然显著。React编译器会注入备忘录单元和依赖项检查。若依赖项是不稳定的对象引用,缓存命中率将很低:

  • 你仍需为备忘录槽位支付额外内存成本,
  • 仍需执行依赖项检查,
  • 仍需因引用变更而重新计算。

换言之,当渲染路径中充斥着类实例且未进行手动备忘时,编译器的优化往往会变成额外开销而非性能提升

现在,让我们回到传递整个对象的问题。若传递对象后其引用发生变化,组件将重新渲染。无论对象是类实例还是普通对象,此特性均成立。若需避免因对象引用变更导致的冗余渲染,可仅传递子组件实际需要的原始值,而非完整对象。如此,组件仅在相关原始值变更时重新渲染,而非对象引用变更时:

JavaScript 复制代码
export function Row({ isoDate }: { isoDate: string }) {
  const formattedDate = DateHelpers.formatIsoDate(isoDate);
  return <span>{formattedDate}</span>;
}

现在依赖关系已显式化且采用原始类型(isoDate),而非隐藏在实例方法背后。

可能的反对意见是:即使采用面向对象的方法,仍可将element.getFormattedDate()的结果传递给子组件,而该结果本质上仍是字符串:

JavaScript 复制代码
function Parent({ element }: { element: ElementClass }) {
  return <Row formattedDate={element.getFormattedDate()} />;
}

function Row({ formattedDate }: { formattedDate: string }) {
  return <span>{formattedDate}</span>;
}

Row 组件现在接收原始属性,但耗时或重复的计算只是向上移了一层,转移到了 Parent 组件中。

如果 element 组件频繁通过引用发生变化,element.getFormattedDate() 方法仍会频繁重新执行。因此瓶颈并未消除,只是转移了位置。

采用数据优先的架构后,你可以直接跨边界传递 isoDate 数据,并将衍生计算作为纯函数保留在需求附近。

这更契合 React 的纯粹性与不可变性模型:

实用经验法则

在 React 渲染路径中,优先采用数据优先模型而非行为丰富的类实例。

仅在边界处使用类(如领域模型、解析器、适配器),但向组件传递可序列化的纯数据,并将渲染时推导保持为纯函数。

借助 React Compiler,这通常能带来:

  1. 更高的自动备忘录命中率
  2. 更少的手动 useMemo 逃逸机制
  3. 更清晰的依赖推理
  4. 更少因对象身份变化导致的意外重计算

React Compiler 消除了大量优化工作,但仍会奖励依赖关系明确的代码。在现代 React 的 UI 渲染中,普通对象加纯辅助函数往往是更具可扩展性的选择。

相关推荐
mCell7 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell8 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
恋猫de小郭8 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
少云清8 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
银烛木8 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_607076608 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
听海边涛声8 小时前
CSS3 图片模糊处理
前端·css·css3
IT、木易8 小时前
css3 backdrop-filter 在移动端 Safari 上导致渲染性能急剧下降的优化方案有哪些?
前端·css3·safari
0思必得09 小时前
[Web自动化] Selenium无头模式
前端·爬虫·selenium·自动化·web自动化
anOnion9 小时前
构建无障碍组件之Dialog Pattern
前端·html·交互设计