我为什么把公司项目从 React 改成原生 JavaScript + Web Components,以及踩过的坑

🔑 关键词:React替代方案,Web Components,原生JavaScript性能,前端框架对比,构建工具简化

📖 摘要:一个从 React 迁移到原生 Web Components 的开发者复盘:包体积减少 62%,首屏渲染快 340ms,但阴影 DOM 和表单提交的坑让我差点放弃。

去年年中我接手了一个内部后台管理系统,React + Redux + TypeScript + Webpack,光依赖就有 2200 个。每次启动要等 15 秒,热更新经常跑到 3 秒以上。我查了下构建后的主包是 1.2MB(gzip 后 380KB),但一个只负责展示表格和表单的项目,真的需要这些吗?后来我花了两周时间把它重构成了原生 JavaScript 模块 + Web Components,没有用任何框架。包体积直接掉到 460KB,gzip 后 148KB。首屏加载时间从原来的 2.1 秒降到了 1.76 秒,在最低配的 i3-4005U 测试机上更明显——从 4.2 秒变 3.4 秒。这 340ms 的差距在第二次访问时更直观,因为 Service Worker 缓存了全部资源,后续刷新基本 0.3 秒内出内容,而以前后端还要等 Redux 把状态 hydrate 一遍。

图片

但这不是一篇吹原生打框架的文章。真正让我想放弃 React 的原因不是性能,而是维护成本。我们的业务逻辑经常改,一个页面要响应不同权限加不同表单字段。在 React 里,每次加字段都要改 store、action、reducer、selector,还要保证 ts 类型同步。后来我发现用原生自定义元素时,直接把数据绑到 element 的 property 上,再监听 setter 去更新 DOM,逻辑全部封装在元素内部。改业务时只需要改组件的属性和内部模板,不再拖网线一样扯到全局状态。不过新问题来了:表单控件的 shadow DOM 隔离导致 select 元素的下拉框没法穿透,还得手动设置 picker 的 z-index;还有 label 的 for 属性也找不到 shadow 内部的 input,最后不得不改成 aria-label 和 :focus-within 配合。这些坑很具体,比如 internal form validity 的校验,原生 input 自带 required 验证在 shadow DOM 里不会自动阻止表单提交,我花了一个下午给每个自定义表单元素写 validity 检查逻辑。

图片

再说说构建工具。以前 Webpack 配置有 300 多行,splitChunks、thread-loader、mini-css-extract-plugin、babel-loader 都要调。现在我把项目改成了零构建方案,直接用 ES Modules 跑在浏览器里,开发服务器就是一个 Python 的 http.server 加上一个 20 行的小脚本做路径重写。因为支持的浏览器版本比较新(Chrome 90+,由于是内部系统),我连 transpile 都不需要,原生 class、optional chaining、private field 全都可以跑。生产环境也是直接把静态文件丢到 CDN,没有 tree-shaking 但按需 import 也够用。但是项目里有一个老旧的 Excel 导出工具依赖 CommonJS,怎么都没法在 ESM 里直接 import,最后我做了个变通:把这个库放在 Web Worker 里,用 compatibility script 把 worker 暴露成全局,再在原生模块里通过 self.createObjectURL 加载。这个方法很 hack,但能跑。

图片

如果你问我会不会建议别人也全部抛弃框架,我的答案是不一定。我们项目是内部管理工具,用户量最大也就 500 人,没有 SEO 需求,也不依赖 React 的生态库。如果你的项目是面向 C 端的落地页,需要复杂的动效、状态管理、服务端渲染,那 React 或 Vue 的生态和社区成熟度确实能帮你省下很多自己造轮子的时间。我自己写 Web Components 也重造了一个类似 useEffect 的东西——自定义元素 attributeChangedCallback 只能监听 attribute 变化,对对象类型的数据不敏感,要手动处理。后来我写了一个简易的 state 管理器,大概 150 行,每次 set 就触发渲染,倒也挺像那么回事。但说实话,如果当初我知道 Lit 这个库(它不是框架,是库),我应该会直接用 Lit 的 reactive property 和 directive,不会傻到全部手写。这算是我这次重构最大的遗憾,也是一次比较真实的教训。

图片

现在这个项目已经稳定运行四个月了,每周的改动时间比原来少了大概 30%,因为我不用再关心 Redux 里那些模板代码。但是我也不敢说这是正确决策——因为团队里新来的两个应届生对 React 很熟,但对原生 DOM api 和自定义 element 的 lifecycle 并不熟,他们上手反而更慢。前期我花了两周给他们做培训,还要把很多细节写成文档。所以如果你面临同样的选择,我建议你先画一张矩阵:项目生命周期超过三年吗?团队里有没有两个以上精通原生 JS 的人?业务组件是否需要跨项目复用?如果答案都是否,那可能留在框架里更稳妥。对我来说,这次重构更像是一种对技术依赖的反思,而不是对框架的宣战。

图片