组件是纯开销 你的框架是纯开销 组件 DX > 性能消失的组件 未来是无组件的

2025-05-28

组件是纯开销

你的框架纯粹是开销

组件 DX > 性能

消失的组件

未来是无组件的

几年前,我在《UI 组件的真实成本》一文中探讨了 JavaScript 框架中组件的成本。我问过一个问题:组件是否只是开销?

答案是:视情况而定。我测试的虚拟 DOM 库ivi在处理更多组件时没有问题。但LitSvelte 的表现则差得多。当我将它们分解成更多组件后,它们的性能几乎降到了React的水平。它们所有非 VDOM 的性能优势基本上都消失了。

替代文本

版本从具有最少组件数的“0”到每行有一个组件的“1”,再到每个组件都<td>包裹在一个组件中的“2”。

幸运的是,对于这两个框架来说,几乎所有基准测试都可以写成单个组件。

但是您上次用单个组件编写应用程序是什么时候?

他们辩解说,一页 5 万个组件确实有点多。但这仍然揭示了我们需要克服的一个不可避免的缺陷。两年过去了,我仍然坚持这个结论。

所以我要在这里向非虚拟 DOM 的支持者们大胆声明。我认为组件应该像框架一样消失。如果新世界是编译器,我们可以做得更好。我们可以针对打包代码块进行优化,而不是 ES 模块。如果组件被丢弃,想想通过内联它们可以减少多少开销。

但我逐渐意识到这其中还有比性能更重要的事情。


你的框架纯粹是开销

这篇文章并非针对潜伏在各个网站评论区的 Vanilla JavaScript 纯粹主义者,而是从 JavaScript 框架构建者的角度,对 JavaScript 框架进行诚实的审视。

当人们说虚拟 DOM 纯粹是开销时,他们通常指的是不必要的对象创建和差异处理。Svelte 的创建者 Rich Harris 很好地阐述了这一点。

当然,如上所示,有比 Svelte 更快的虚拟 DOM 库,那么这是怎么回事呢?

请考虑文章中的这个例子:



function MoreRealisticComponent(props) {
  const [selected, setSelected] = useState(null);

  return (
    <div>
      <p>Selected {selected ? selected.name : 'nothing'}</p>

      <ul>
        {props.items.map(item =>
          <li>
            <button onClick={() => setSelected(item)}>
              {item.name}
            </button>
          </li>
        )}
      </ul>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

批评之处在于,任何状态更新都会强制 VDOM 重新渲染所有内容。即使更改了选择,也需要重新创建整个列表。然而,大多数高性能 VDOM 库能够识别出这些 VDOM 节点中的大多数永远不会更改,并会缓存它们,而不是每次渲染都重新创建。

但更重要的是,有一个每个 React 开发者都知道的隔离更新的解决方案。不,不是useMemo。创建一个子组件。

几乎无需任何额外成本,VDOM 库就可以通过将这种逻辑包装到其他组件中来阻止更新传播。只需简单检查一下属性的引用即可知道何时重新渲染。VDOM 的性能非常出色,这一点毋庸置疑。

说到useMemo最近有人关注的事实,它可能不应该是你首先想到的。然而,反应式库往往默认使用记忆功能。

在 React 或任何其他 VDOM 库中,当你想从结构上打破更新周期时,你会拆分组件并提升状态。而为了使用像 Svelte 这样的库来提升初始渲染性能,你需要反其道而行之,尽可能多地移除中间组件。

为什么?因为每个组件都是一个单独的反应式作用域。这通常意味着不仅仅是创建反应式作用域。在它们之间同步更新会产生开销。文章开头的基准测试证实了这一点。

当我们忙于关注 VDOM 库如何完成所有这些可能不必要的工作时,我们并没有注意到我们的反应库正在做所有这些不必要的记忆。

所以是的,你的 Reactive 库也是纯粹的开销。


组件 DX > 性能

当我审视这两种方法时,我发现了同样的问题。我们构建组件的方式对应用程序的性能影响太大了。这是一个问题。

组件的用途不仅仅是性能。组件的结构直接影响代码的可维护性。

当组件太少时,最终会导致逻辑重复。典型的组件包含状态和视图。控制流越复杂,嵌套状态越多,就越需要同时在两个组件中重复相同的逻辑。当出现新的需求时,比如切换可见性这种简单操作,你就会发现自己需要在多个地方创建相同的条件语句。



export function Chart({ data, enabled, headerText }) {
  const el = useRef();
  useEffect(() => {
    let chart;
    if (enabled) chart = new Chart(el.current, data);
    return () => chart?.release();
  }, [enabled]);

  return (
    <>
      <h1>{headerText}</h1>
      {enabled && <div ref={el} />}
    </>
  );
}


Enter fullscreen mode Exit fullscreen mode

由于……,我们需要在多少个不同的地方进行额外检查props.enabled?你能找到全部 4 个吗?这不是 React 独有的。Svelte(以及大多数框架)中的等效代码涉及 3 个位置。

相反,将组件分解成过多会导致耦合过高。需要传递的 props 太多。这通常被称为prop 钻探。这种间接传递方式会使状态的改变变得异常复杂。可能会继续传递不再使用的 props,传递过少的 props 而被默认 props 吞没,并且重命名会使追踪更加模糊。



function Toggle() {
  const [on, setOn] = useState(false)
  const toggle = () => setOn(o => !o)
  return <Switch on={on} onToggle={toggle} />
}
function Switch({on, onToggle}) {
  return (
    <div>
      <SwitchMessage on={on} />
      <SwitchButton onToggle={onToggle} />
    </div>
  )
}
function SwitchMessage({on}) {
  return <div>The button is {on ? 'on' : 'off'}</div>
}
function SwitchButton({onToggle}) {
  return <button onClick={onToggle}>Toggle</button>
}


Enter fullscreen mode Exit fullscreen mode

消失的组件

替代文本

未来属于原语。比组件更小的原语。就像你今天在响应式系统中看到的那样。它们可能看起来像你在 React Hooks 和 Svelte 中看到的那样。但有一个例外:它们不与创建它们的组件绑定。

细粒度响应式的强大之处以及Solid无与伦比的性能并非源于细粒度的更新。创建时成本过高。真正的潜力在于,我们的更新不依赖于组件。而这远不止于此。

在反应模型和这些钩子之间,我们融合了一种变化的语言:

State-> Memo->Effect

或者,如果你愿意,也可以写成Signal-> Derivation-> Reaction。我们不再需要组件来描述更新了。这正是 React 开发者直观感受到的 Hooks 带来的不匹配之处。为什么我们需要同时跟踪组件的重新渲染和 Hooks 上的闭包呢?

而典型的单文件组件 (SFC) 则恰恰相反,我们仍然在用技术强加(不必要的)界限。有没有想过为什么 JavaScript 框架和 Web 组件之间会有摩擦?因为太多东西被混为一谈,混淆在一个概念上。

每次编写组件时,我们都会思考如何构建代码。我们感觉自己并非自主选择。但其实不必如此。


未来是无组件的

这并不意味着我们不会编写可复用的组件或模板。只是组件会消失,消除它们对输出的影响。这不需要编译器来启动。我们可以让组件的重量不超过一个简单的函数调用。这本质上是Solid 的,但这只是解决这个问题的一种方法。

我们也不需要分离来实现这一点。没有必要将所有状态都提升到一个状态管理工具中,让它充当渲染器的傀儡。我建议积极地进行共置。现代框架都有这个权利。无论是 JSX 还是 SFC,我们都一直在整合它们,并且应该继续下去。

最终,如果编译器能够超越当前正在处理的文件,运用语言来理解整个应用,想象一下将会打开怎样的大门。我们的逻辑和控制流可以独自定义边界。这不仅能提升性能,还能让我们摆脱再次担心这个问题的精神负担。

在编写我们的网站和应用程序时,如果能重新获得像 HTML 那样纯粹的声明性,那岂不是很棒?那种纯粹的剪切粘贴的幸福感?我不确定这会走向何方,但它就从这里开始。

文章来源:https://dev.to/this-is-learning/components-are-pure-overhead-hpm
PREV
JavaScript 框架 TodoMVC 大小比较
NEXT
我最初需要的 NgRx 技巧