摘要:Web 组件曾被视为 Web 开发的未来,但是否真的如此?作者凭借 7 年生产实践,深入剖析 Web 组件的利弊。从标准制定的限制到性能开销,从开发复杂性到未来适应性,Web 组件是否值得我们付出代价?这篇文章将为你揭开真相,带你重新审视这一技术在 Web 开
作者 | Ryan Carniato
译者 | Sambodhi
策划 | Tina
导读:Web 组件曾被视为 Web 开发的未来,但是否真的如此?作者凭借 7 年生产实践,深入剖析 Web 组件的利弊。从标准制定的限制到性能开销,从开发复杂性到未来适应性,Web 组件是否值得我们付出代价?这篇文章将为你揭开真相,带你重新审视这一技术在 Web 开发中的价值。几年前,我曾撰文提出“Web 组件可能并不是 Web 开发最有利的发展方向”。
那篇文章以温和的视角剖析了 Web 组件的适用场景与局限所在,并非要制造 "非此即彼" 的对立叙事,而是希望读者能自行得出理性的结论。
但在过去几年里情况却愈发恶化。或许有些开发者从未认真对待 Web 组件,但我始终保持关注 —— 不仅在生产环境中使用了长达七年,还在早期为 Shadow DOM 等特性编写过多个 polyfill,使其在原生支持尚未成熟前便能投入实际应用。
正是这些深入的实践经验,让我得出了一个明确的结论:Web 组件可能是 Web 未来面临的最大潜在风险。
乌托邦愿景
我承认,这个论断或许略显激进。但我之所以这么认为,背后有着许多原因,而这一切都源于对 Web 组件所声称优势的理解。
Web 组件的愿景是:终有一天,开发者无论使用何种工具编写组件,都能像使用原生 DOM 元素一样,将其无缝集成到任何网站,而无需考虑网站的构建方式。不必担忧特定的构建工具、运行时机制,也无需担心与这些组件的交互方式会发生变化。
这描绘了一个可移植且互操作的 Web。它能够大幅降低未来迁移成本,它为任何可能的未来做好准备。它被视为让网站和应用程序免受时代淘汰的理想方案。
如此诱人的前景,难道不令人向往吗?这正是 Web 组件的承诺既充满吸引力,又潜藏风险的根本原因。
标准之争
我不必特意翻出那幅 xkcd 经典漫画,你也能体会到标准所面临的挑战。越是雄心勃勃的标准,越容易引发反对或催生替代方案。这种分歧不会因某项技术被标准化而消失;它仅仅意味着存在一种被奉为 "官方推荐" 的方式 —— 你可以选择采纳,也可以选择摒弃。
若以 JavaScript 框架的爆炸式增长为参照,我们显然还远未就 Web 组件的开发方式达成共识。即便今天看似有所进展,但十年前差距甚至更远。
引入更高级的基础原语可能产生积极影响:原本困难的任务突然变得简单,从而激发更多的探索。2010 年代中期 Web 组件的兴起直接导致了 JavaScript 框架数量的激增,这也正是我创建 SolidJS 的重要灵感来源。类似的案例还有 Vite 推动元框架(Metaframeworks)生态蓬勃发展。
但此类标准化也可能带来负面效应:当假设过多时,探索替代方案的难度就会剧增,因为整个生态都围绕既定标准运转。还有什么比永远不会改变的 Web 标准更权威呢?
根据规范,JSX 并没有明确的语义,但如果你试图让那些广泛使用的工具意识到它们做了过多的假设,实际上这会带来问题。如果 JSX 被浏览器标准化,这些假设就会被统一,这可能会限制开发者的灵活性,甚至引发兼容性问题,简直让人难以想象—— 且不论 Inferno、Solid、Million 等框架如何通过 JSX 转换实现深度优化,即便是 React 自身也在持续改进其转换机制。
这只是众多例子中的一个。那些本应助力开发的技术,实则可能成为创新的枷锁。我们在标准化任何高层级机制时都必须慎之又慎,因为每一个预设都是对可能性的限制。当标准的存在已然影响整个技术生态的演进方向时,“非强制采用”的辩解将显得苍白无力。
机会成本的真实存在
作为框架作者,我对此感同身受。我常说,在这个领域中,技术发现与技术发明同样重要。我的意思是,设计决策中存在着某种客观规律 —— 遵循这些规律,不同的工具最终会走向相似的实现路径。这并非工具间的拙劣模仿,而是它们都遵循着相同的技术引力轨迹。
出于同样的原因,一旦某个发现改变了我们的视角,损害在我们写下第一行代码之前就已经发生。如果你的工作是设计一个系统,你不希望有冗余的部分,你需要的是明确且有目的的边界。与其在同一个问题上做无数变种,不如尝试复用一个解决方案。尤其是当你意识到为了完成常见任务,你需要多个组件,这些组件往往是相互交织的。
例如,React 从 2017 年宣布 Suspense,到 2022 年终于有了用于 RSC 的数据抓取方案,这段漫长的等待让开发者们映像深刻。为什么需要五年?理解各个部分如何契合需要时间,这本身是合理的。但更深层的原因是,React 不想在他们没有完整答案之前分阶段发布。随着深入探索,他们发现这些部分是相互关联的,虽然可以拆分,但它们需要彼此配合,才能呈现完整的解决方案。
对于如何在 React 中使用 Suspense 进行数据获取,RSC 并不符合每个人的期望,人们本可以从客户端数据获取原型中受益。React 在这里选择了雄心勃勃的路径,这作为独立工具本无可厚非,决定了什么是最好的,但这一过程可以有很多种不同结果。
作为开发者,我可以选择不使用 React。虽然我可以选择不使用 React 的某些功能,但很明显,React 中的一切已经围绕自己的思维模式进行调整。我甚至希望能轻松地从 React 迁移出去。
但这里有一个很大的区别。React 是一个库,而不是标准。当涉及到标准时,这些选项就不一样了。如果我只想要作用域样式,而现在却必须使用 Shadow DOM,因为它是与 Web 组件的最契合的抽象,那我就只能接受这种限制。
当原语超出了预期的使用范围,或者说过度抽象时,你就无法挽回了。任何经历过大型项目架构重构的人都知道,最难的部分就是调整边界。如果事物属于同一逻辑分组,它们的更新会更容易。但当你需要拆解某些部分时,才真正面临挑战。你可以添加一层抽象来解决问题,但去除一层就难了。
抽象的代价
Web 组件的根本问题在于它们是建立在自定义元素之上的。元素 ≠ 组件。更确切地说,元素是组件的一个子集。可以说,每个元素都可以是组件,但并非所有组件都是元素。
这意味着每个接口都需要通过 DOM。有些是已经定义好的方式,但并不完全适配;而有些是新定义的方式,改变了处理元素的方式,以适应扩展功能。
DOM 元素有 attributes(属性) 和 properties(属性)之分。attributes 用于 HTML 定义的属性,因此它们只能接受字符串值;而 properties 作为 JavaScript 接口,可以处理任何类型的值。原生 DOM 元素有很多关于特定 attributes 和 properties 的规则,比如一些属性是布尔型的(其存在即表示适启用),而另一些则是伪布尔型的(需要显式声明 "true" 或 "false")。有些 properties 会反映到 attributes 中,而有些则不会。
模板语言的目标是提供统一的解决方案。我们可以围绕已知元素和属性制定特定的规则,但对于自定义元素来说,我们并不清楚其行为。因此,一些模板库采用了特殊前缀来指示如何设置这些属性。例如,在 Solid 的 JSX 中,使用attr:prop:bool:前缀来区分不同的赋值方式,因此每个运行时环境和编译器钩子都需要专门处理这些情况。你可能会认为我们需要更好的模板标准。但就像上面提到的 JSX 一样,我们必须仔细考虑标准化带来的影响。几年前,大多数人可能会认为像 LitHTML 这样的模板渲染方式是一个不错的选择,其他解决方案也可以输出类似的结构。然而,随着时间的推移,我们意识到,像 Solid 这样的响应式渲染(reactive rendering)方式其实更为高效,而这恰恰是因为它改变了模板的语义。如果我们当时贸然推进标准化,结果可能会是一个并非最优的解决方案。
这还没完呢!DOM 元素可以克隆,但自定义元素的行为却不同,通常需要手动导入。它们具有基于 DOM 的生命周期,具体触发时机是同步还是异步,取决于何时进行元素升级。这会对响应式跟踪和上下文 API 等造成了极大的破坏。然而,这些细节对于与原生 DOM 行为和事件的兼容性至关重要,而 JavaScript 组件则不必担心这些问题。
在 Shadow DOM 中,确实存在一些独特的行为。例如,事件目标的定位方式。有些事件不会“组合”,也就是不会在 Shadow DOM 边界之外冒泡;有些事件则因为无法识别不同的目标而使冒泡不一致,比如在“focusin”事件中,无论哪个子元素获得焦点,Shadow Host 始终会成为目标。虽然这些细节我们可以聊上好几天,但不想因此分散太多注意力。有些是当前的短板,有些是设计使然,但它们有一个共同点:都需要额外的考虑,而这本来是不必要的。
当然,这也会带来性能方面的开销,请参阅我以前写的《再探用户界面组件的真正成本》(The Real Cost of UI Components Revisited)。
即便你认为这种性能开销可以忽略不计,但当进行 SSR (服务器端渲染)会怎样呢?是,你可以完全通过 Web 组件实现 SSR,水合(Hydration)过程也可以顺利完成。但问题是,它们是 DOM 接口,而服务器端是没有 DOM 的。你可以制作一个封装器来处理大部分问题,但这都增加了额外的开销。这一切的根源在于我们试图让组件具备它本不应具备的功能。
Web 组件在服务端没有统一标准,需要特定解决方案。选择 Web 组件并不会比选择 Vue 或 React 带来更多的好处,使用时需要考虑到这一点。
Web 组件的真正成本
从整体来看,Web 组件的“灵活性”带来的是整个生态复杂度的增加。即使工具对 Web 组件的支持逐渐加强,但随之而来的是所有使用该工具的用户都在承担成本,需要编写更多代码并执行更多操作,以应对各种边缘情况,这是一种影响所有人的隐藏税负。
我曾谈到过,标准化过早会带来灾难性的后果。可能会扼杀未来的创新,因为它假设太多。比如水合技术的改进,像是可恢复性(Resumability)、部分水合(Partial or Selective Hydration)等都依赖于事件委托,但 Shadow DOM 干扰了这一点,Web 组件根本无法适配这些优化模型。有些人可能会说 SSR 是个疏忽,因为我们在 2013 年时并未过多考虑,但随着时间的推移,这一问题只会愈发严重。
随着编译器和构建工具的不断进步,开发者不再需要关注组件的存在,而是更多地专注于提升用户体验。在应用中,组件只是开发过程中的一个工具,最终会被“隐藏”或“优化掉”。
我并不是说没有人能够找到一些有趣的解决办法,但这些解决办法都意味着需要承担由于基础抽象不当而带来的隐性成本。这就是为什么这个话题如此难以讨论,它不是一个可以改进的问题,而是一个根本性的错误,涉及到多个方面。
现在,我们可以说不同的解决方案有不同的权衡。而像 Web 组件这样的东西,可能只有在标准组织的支持下才能成功,因为它必须具备足够的普及性才能发挥作用。这是我们正在朝着实现的理想,只是我们还没有完全达到这个目标。
但这真的是理想吗?
重寻乌托邦
Web 组件确实能提供更好的隔离性(Isolation) 和可移植性(Portability),但前端领域的限制要大得多,每千字节的 JavaScript 代码都很重要。你不想随意组合代码,原因不只是为了便于维护,更是为了减少传输负载,而这也正是问题开始浮现的地方。
如果你的目标是让项目能够适应未来发展,那么你需要做好准备,在同一页面上保留同一库的不同版本。这对于 Lit 和 React 来说并无二致。你可以选择从不更新任何内容,但没有 Web 组件你也可以选择这样做,在这方面并没有额外的保障。若要让项目适应未来发展,唯一的办法就是锁定现有内容、维护多个版本,并在页面上加载更多的 JavaScript 代码。实际上,你最终会选择同步更新所有的库,而 Web 组件除了增加更多开销外,并不会为你提供任何帮助。与其使用 Web 组件,不如直接使用库。
第二个考虑因素是粒度。如果你有多个微前端,那么每个微前端都是一个可替换的独立单元。如果将来你觉得它不是处理事务的最佳方式,就可以把它替换掉。但一旦你在各处都采用了 Web 组件,你就需要处理每个接触点。Web 组件与微前端和微服务不同,因为它们可以横向使用。这有利于标准化,不过我从未见过哪家使用 jQuery 的公司能够彻底弃用它。
Web 组件最具吸引力的用途,是充当某种微前端容器。在此情形下,不存在扩展成本,外部通信极少,而且组件易于替换,属于一次性使用场景。然而,在这类场景中,即便不使用 Web 组件,实际操作中的阻碍也足够小,所以它们并非不可或缺。比如,为了能在页面上方便地放置 Zendesk 小部件,我可能会选用 Web 组件,但这种抽象化带来的成本投入,真的划算吗?
结 论
归根结底就是这样。我不能否认在某些场景下使用 Web 组件确实在易用性方面有优势。但它所带来的代价是巨大的。虽说我不应只因一种技术可能导致一些糟糕的模式就急于否定它,然而当它始终无法契合理想场景时,就很难对其表示支持了。往好里说,它只是一个摆设。
Web 组件是一种彻头彻尾的妥协。有时我们确实需要做出妥协。但这没什么值得兴奋的。而且,有些妥协方案确实比其他的要好。
有人对我说,十年后可能没有人会使用 Solid,但 Web 组件仍会存在,并且拥有和今天一样的接口。但我想了一下,回答说,我仍然会选择用 Solid 构建它,因为这是当前最好的选择。即使十年后我不得不使用十年前的 Solid 版本,它也会比 Web 组件版本更好。十年并不能抹去这一点,希望到时我能使用更好的东西。
从某种意义上讲,Web 组件本身并无过错,因为它们只是它们自己。真正危险的是,它们被赋予了自身并不具备的特性,做出了无法兑现的承诺。正是它们的存在扭曲了周围的一切,使整个 Web 处于危险之中。这是每个人都必须付出的代价。
声明:本文为 InfoQ 翻译,未经许可禁止转载。
今日好文推荐
“李飞飞团队50 美元炼出 DeepSeek R1”被质疑,上海交大本科生新“低成本推理”或成新宠!
国产 DeepSeek V3 被秒成"前浪"?谷歌开放最强 Gemini 2.0 全家桶:速度快60倍,上下文还长16倍!
苹果开始“拯救”Swift ?突然开源百万 App 在用的 Swift Build,迈出推动跨平台一致性的关键一步
10年了,开发人员仍然不明白 Electron 的意义
来源:InfoQ