Skip to content

从升级到 Rsbuild 说起,前端项目为什么需要工程化 ​

这段时间,我陆陆续续完成了两个旧项目的构建工具的升级(从老版本的使用 Webpack 的 Create React App 升级到基于 Rspack 的 Rsbuild)。

关于具体如何升级,后续会再单独出文进行讲解。而本文,我则希望聚焦于解答 “为什么前端项目要工程化,优秀的工程化对前端项目到底意味着什么” 这一问题。

在我看来,前端项目实现并做好工程化,能带来以下几个方面的收益:

  • 开发效率的直接提升;
  • 适应各类需求的开发;
  • 更好地适应未来的前端技术发展;

在下文中,我将对这几个方面进行详细的说明。

升级前后对比 ​

首先对这次升级的一个后台项目的成果进行一个总览:

image-20251001204856915

指标升级前升级后提升幅度
模块热替换(HMR)无,修改代码后需要手动刷新页面和恢复页面状态,单次操作约15秒有,修改代码后直接自动更新,并恢复页面状态,0.66s22.72倍
开发服务器启动132s25.7s5.14倍
生产打包623.4s25.7s24.30倍
生产打包内存占用13.7GB,需要关闭其他软件,腾出内存,否则可能内存不足导致打包失败。2034MB,影响极小(后续升级为流水线打包后则完全无影响)6.76倍
依赖数量98个69个缩减29.59%
配置文件和其他无用文件31个5个缩减83.87%
生产打包和发布需要本地打包和手动提交,产生大量打包产物的 git 历史,严重影响代码合并流水线打包,仓库中不存储打包产物,只有源代码减少1253个打包产物文件

从以上指标可以非常直观地看到,从原有的 CRA 升级到 Rsbuild 后,无论是开发效率,还是项目的可维护性,都有了大幅度的提升。

这就是优秀的前端工程化所带来的直接效果。

开发效率的直接提升 ​

从上面的成果总览中就可以直观地看到,升级后,各个环节的耗时都得到了大幅度的降低。这也是前端项目工程化最直接的目的——提升开发效率。

前端经过这几十年的飞速发展,诞生了各种层出不穷的框架、工具库、设计模式,其目的都是为了提升开发效率。而工程化则是支撑这些技术发挥作用的基础设施,没有工程化,这些技术也难以整合到项目中,更无法发挥作用。

更别提还有 HMR 这种颠覆前端开发效率的技术,也是工程化带来的。

接下来,我就以这次升级为例,详细说一下工程化方面的优化对开发效率到底具体提升在哪。

各开发环节加速 ​

首先是各开发环节的加速

先看一下旧的开发流程:

old-development-process

一个需求的前端部分的开发和上线一般分为以下几个阶段:

  1. 开发;
  2. 联调;
  3. 测试;
  4. 上线;

而每个阶段都通常需要大量重复以下这两个子流程:

  • 修改代码;
  • 部署代码到对应环境。

而旧的开发流程中,这两个子流程中就存在以下几个非常低效的步骤,根据是否可优化去掉和是否可提升速度,可以分为以下两类:

  • 可优化去掉的步骤:

    • 手动刷新页面;
    • 恢复页面状态;
    • 关闭其他软件,腾出内存;
    • 提交打包产物。
  • 可提升速度的步骤:

    • 修改代码后等待模块重新打包完成;
    • 生产打包;

通过将构建工具从 CRA 升级到 Rsbuild,几乎不进行额外配置的情况下,就可以将整个开发流程优化为以下这样子:

new-development-process

如果熟悉各类脚手架,其实会发现这里的 HMR 其实 Webpack 就支持,各种基于 Webpack 的脚手架也都默认开启,并不一定需要通过升级打包工具来实现。

而流水线打包就更是完全和打包工具无关的,只是把本地打包流程迁移到了线上。

那这两个优化是否可以做到在升级打包工具之前就实现呢?

我的回答是,可以。但是原先项目的各种打包配置过于复杂,导致难以梳理出 HMR 被关闭的原因,以及开启的方法。同理,原先的打包时间过长、内存占用过高,也需要深度优化后才能将打包过程迁移到流水线上。

综合下来,基于原有技术栈进行优化的开发成本过高,不如直接推倒重来、升级打包工具来得简单。

降低工程化自身的开发和维护成本 ​

从前一节末尾的分析中就可以知道,如果一个项目的工程化一开始设计得不好,那么后续的开发成本和维护成本都是非常高的。

原因无外乎以下两点:

  • 原打包工具、脚手架设计不够现代化,在长期开发中非常容易就导致项目工程化的配置变得过于复杂,无法维护;
  • 原打包工具本身就存在性能瓶颈,为了提升热更新、打包速度,需要付出大量的开发成本;

而这次升级的 Rsbuild 正是为了解决这两个问题而设计的。

首先,它可以开箱即用,内置并默认开启了语法降级、分包优化等各种功能,像是对 React 、Less 等框架的支持也只需要简单引入对应的插件即可,几乎不用进行额外配置。

同时,它有原生中文文档,可以非常方便地进行查阅和学习,各种配置介绍也非常详细,学习成本极低。

当然,更重要的是,因为底层基于 Rspack 开发(Rspack 是基于 Rust 开发的,底层语言层面上就远超 JS),它在性能上相比各种基于 Webpack 的脚手架有着巨大的优势。

对各类需求的适应性 ​

正如上文所说,这次升级的 Rsbuild 的设计更为现代化,同时其几乎兼容原 Webpack 生态,所以不管是重新开发一个 Rsbuild 插件,还是找到一个已有的插件来实现相关功能,都要更简单。

而且,由于使用 Rust 开发,未来接口稳定后,甚至可以直接使用 Rust 语言来开发插件,进一步提升插件性能。

也正是依托于 Rsbuild 这样的打包工具,很多现代前端技术理念才得以整合应用到项目中。比如现在习以为常的 语法降级、分包优化、各种框架的支持、CSS 预处理器 都是前端工程化的产物。

通过这次升级为 Rsbuild,未来还可以尝试和扩展以下功能:

  • unplugin-auto-import:实现各种框架和库的 API 的自动导入,无须手动导入;
  • 模块联邦:现代前端模块共享方案,降低项目复杂度,提升应用性能;
  • Rstest:Rspack 生态下的的测试框架,更优秀的性能和更丰富的功能; ……

示例:项目埋点插件 ​

如果只是从社区中找插件,那显然不可能覆盖所有的项目需求,我这里也提供了一个示例,说明如何通过前端工程化解决一些个性化的需求。

项目地址:https://github.com/RJiazhen/rsbuild-tracking-plugin-demo

CodeSandbox地址:https://codesandbox.io/p/github/RJiazhen/rsbuild-tracking-plugin-demo/main

这是一个简单的 React 项目,只有一个示例页面,其滚动到底部有一个广告区域。

tsx
const OriginalPage = () => {
  const [showBanner, setShowBanner] = useState(true);

  /** 点击广告回调 */
  const handleBannerClick = () => {
    alert('点击了广告');
  };

  /** 点击关闭按钮回调 */
  const handleCloseBanner = (e: React.MouseEvent) => {
    e.stopPropagation();
    setShowBanner(false);
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>原始页面</h1>
      <p>这是一个很长的页面,滚动到底部可以看到广告。</p>
      {Array.from({ length: 50 }).map((_, i) => (
        <p key={i}>这是页面内容的一部分... {i + 1}</p>
      ))}
      {showBanner && (
        <div
          onClick={handleBannerClick}
          style={{
            // ...
          }}
        >
          <h2>这是一个横幅广告</h2>
          <button
            onClick={handleCloseBanner}
            style={{
              // ...
            }}
          >
            关闭
          </button>
        </div>
      )}
    </div>
  );
};

现在,计划接入一个埋点服务,服务供应商提供了一个 js 文件,其作用是挂载两个埋点方法到全局,调用这两个方法即可发送埋点:

javascript
window.tracking = {
  show: (name) => {
    console.log(`[Tracking] Show: ${name}`);
  },
  click: (name) => {
    console.log(`[Tracking] Click: ${name}`);
  },
};

如果按照常规的开发方式,就是直接在源代码中编写相关的调用方法逻辑:

tsx
import 'http://localhost:3000/tracking.js'; // 引入埋点脚本

const TrackedPage = () => {
  const [showBanner, setShowBanner] = useState(true);
  const bannerRef = useRef<HTMLDivElement>(null);

  // 使用 IntersectionObserver 监听广告元素的可见性
  useEffect(() => {
    if (!showBanner || !bannerRef.current) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          window?.tracking?.show('ad'); // 发送显示埋点
          observer.disconnect();
        }
      },
      { threshold: 0.1 }, // 当广告元素可见度达到 10% 时触发
    );

    observer.observe(bannerRef.current);

    return () => {
      observer.disconnect();
    };
  }, [showBanner]);

  /** 点击广告回调 */
  const handleBannerClick = () => {
    window?.tracking?.click('ad'); // 发送点击埋点
    alert('点击了广告');
  };

  /** 点击关闭按钮回调 */
  const handleCloseBanner = (e: React.MouseEvent) => {
    e.stopPropagation();
    // 发送点击埋点
    if (window.tracking) {
      window.tracking.click('close-ad');
    }
    setShowBanner(false);
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>埋点页面</h1>
      <p>这是一个很长的页面,滚动到底部可以看到广告。</p>
      {Array.from({ length: 50 }).map((_, i) => (
        <p key={i}>这是页面内容的一部分... {i + 1}</p>
      ))}
      {showBanner && (
        <div
          ref={bannerRef}
          onClick={handleBannerClick}
          style={{
            // ...
          }}
        >
          <h2>这是一个横幅广告</h2>
          <button
            onClick={handleCloseBanner}
            style={{
              // ...
            }}
          >
            关闭
          </button>
        </div>
      )}
    </div>
  );
};

可以看出来,这样加入埋点逻辑的维护成本非常大,埋点逻辑本身是和已有的业务逻辑无关的,但是相关的代码却和业务逻辑耦合在一起。

即使进行二次封装,或者编写一个专门的高阶组件,其埋点代码都显然是对已有的业务逻辑代码的侵入。

那有没有办法像 Vue 的自定义指令一样,只简单地添加一个属性,就实现埋点逻辑呢?

答案是可以的,只需要编写一个解析 JSX 节点的插件,然后在解析过程中,如果节点上存在相关的属性,就自动添加相关的埋点逻辑。

实现了该功能后,只需要在组件中添加一个属性,就可以实现埋点逻辑:

tsx
const AutoTrackedPage = () => {
  const [showBanner, setShowBanner] = useState(true);

  /** 点击关闭按钮回调 */
  const handleBannerClick = () => {
    alert('点击了广告');
  };

  /** 点击广告回调 */
  const handleCloseBanner = (e: React.MouseEvent) => {
    e.stopPropagation();
    setShowBanner(false);
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>自动埋点页面</h1>
      <p>这是一个很长的页面,滚动到底部可以看到广告。</p>
      {Array.from({ length: 50 }).map((_, i) => (
        <p key={i}>这是页面内容的一部分... {i + 1}</p>
      ))}
      {showBanner && (
        <div
          onClick={handleBannerClick}
          data-track-show="ad" // 添加显示埋点属性
          data-track-click="ad" // 添加点击埋点属性
          style={{
            // ...
          }}
        >
          <h2>这是一个横幅广告</h2>
          <button
            onClick={handleCloseBanner}
            data-track-click="close-ad" // 添加点击埋点属性
            style={{
              // ...
            }}
          >
            关闭
          </button>
        </div>
      )}
    </div>
  );
};

具体的插件实现代码,请查看 Github 仓库文件,这里也说明一下,示例代码非常简陋,仅作为示例,请勿直接用在生产环境。

这个插件在实现自动添加埋点逻辑的同时,还支持了以下的特性:

  • 动态引入埋点文件,从而减少无埋点页面的加载资源;
  • 生成整个应用的埋点信息;
  • 检查埋点是否重复,自动给出警告;

同时,还可以继续扩展以下功能:

  • 支持模拟挂载埋点文件,减少开发时对外部环境的依赖;
  • 在打包时,使用无头浏览器进行埋点测试,自动生成埋点标记示意图;

而这些都是基于打包工具提供的 API 实现的,也由于遵循了打包工具的插件规范,所以是可插拔的,可以非常方便地移除、替换和独立使用。

typescript
export default defineConfig({
  plugins: [
    pluginReact(),
    // 不再需要自动添加埋点逻辑时,只需要移除这个插件就可以了
    pluginAutoTracking({
      outputTransformedFiles: true,
    }),
  ],
});

同时,也是基于 Rsbuild 自身的特性,甚至可以考虑使用 Rust 来重新编写成 SWC 插件(示例中的是使用 TS 编写的 Babel 插件),从而进一步提升插件的性能。

从这里也可以看到,该插件的实现是高度依赖于打包工具的,所以打包工具本身的上下限同时也决定了插件的上下限。


总结地来说,当前前端工程化使用的各种打包工具,由于其确实给前端项目开发带来了巨大的效率提升,得到整个前端社区的广泛认可,所以发展到今日,依托于其的整个插件生态也非常繁荣,通过这些社区提供的插件,可以非常方便地实现各种需求。

而就算不使用已有的插件,因为现代打包工具提供了非常丰富的 API,自己开发插件也非常方便。

基于这一点,现代前端项目的工程化开发,基于这些打包工具可以说是必选项。

但同时,各类打包工具也决定了工程化的上限,轻则性能不好,影响开发效率,重则 API 不完善,连功能都无法实现。

比如最直接的性能问题,其直接影响到开发效率。过去的 Webpack 、Rollup 等打包工具都是基于 JS 开发的,受限于 JS 本身的特性和 JS 运行时的性能,在面对大型项目时,其性能已经捉襟见肘了。自身的性能不行,基于其开发的插件的性能自然也很难好到哪去。

这也是为什么现代的前端打包工具(Rspack,Rolldown等)都转向使用 Rust 来开发,也是为了给性能兜底。

所以,一个前端项目的迭代和维护不能只是业务功能方面的,也需要考虑到工程化方面的改进。对打包工具等工程化方面的优化,也是项目维护的重要一环。

前端技术的未来发展 ​

基于构建工具的生态建设 ​

当前前端发展的大趋势是以打包工具为核心,构建一整套完整的工具链。过去那种 webpack 官方基本只维护自己这一个工具,其他功能基本全靠社区各自维护的时代一去不复返了。

因为很多效率的提升需要深度介入底层的语言解析过程,如果打包工具不整合好解析工具,那很多想法也都只是空中楼阁。

void-zero

Void Zero 的生态布局

而之前 Vite 团队专门成立的 VoidZero 公司,也是把统一整个前端工具链作为目标,从底层的语法解析器到打包工具、构建工具,甚至测试框架都由其亲自己进行开发和管理。

rspack

以 Rspack 为核心的官方生态系统

而这次升级的 Rsbuild 也同样如此,它就是作为 Rspack 官方生态系统的一部分诞生的,而官方生态系统中还包含静态博客框架 Rspress、测试框架 Rstest、包开发工具 Rslib 等。

那么为什么会呈现这样的趋势呢?

因为构建工具随着这几年前端的发展,已经不只是一个简单的前端文件的转换工具,而是前端工程的中枢管线。

构建工具也因为渗入到了前端项目开发的方方面面,自然可以从整个项目中解析出足够多的信息,这就使得各种工具自然而然地愿意依附于构建工具提供的 API 进行开发,以便更高效地消费这些内容。

基于构建工具的外围生态 ​

正如前面所说,构建工具贯穿并整合整个前端项目,在一过程中会获取和产生非常多有价值的项目相关数据。

在使用这些数据的基础上,如果构建工具提供了相关的 API,那就不只可以做语法降级、分包优化这种直接影响最终生产打包产物的事情,还可以实现很多开发相关的功能,虽然对最后的打包产物没有影响,但是可以极大地提升开发效率。

例如现代前端开发中必备的 HMR 就是利用了构建工具解析得到的模块依赖关系,以及使用构建工具动态植入热更新逻辑。

在过去 Webpack 这类构建工具,基本上只是提供相关的接口,而没有专门地进行设计和适配。

而近几年,基于过去开发配套的浏览器插件、VScode插件的经验,Vue 团队已经开始布局基于 Vite 为核心的开发者工具的生态

nuxt-devtools

Nuxt DevTools

具体说明可以查看 Anthony Fu 在 ViteConf 2025 上的分享——Vite DevTools 前瞻介绍(幻灯片链接)。

相信如果接触过 Vue 和 Nuxt 开发,一定能感受过这种配套的外围开发工具对开发效率的提升。相信这也是未来构建工具的发展趋势。

AI 时代的构建工具 ​

本质上来说,浏览器插件这类开发工具,就是把源码和框架运行时内部的信息进行整合转换,然后提供给开发者查看、阅读,并且通过图形界面提供对应的修改功能。

那么同样的,不仅开发人员可以阅读,AI 也可以阅读,并进行辅助开发。

像是现在已有的方案,比如 Google 提供的Chrome 开发者工具 MCP就是让 AI 直接阅读浏览器提供的信息。

但是直接阅读当前网页的 dom 结构和存储的变量,有几个问题,一是无效的信息太多(比如只是使用列表渲染生成的 dom 结构,AI 需要就只是源代码和用来遍历的数据),二是这些信息不好映射到源代码中,不好作为 AI 自动修改源代码的依据。

所以,作为整个开发环节中“最了解项目”的构建工具,理论上是可以想办法提供这些信息的,从而实现 AI 更高效地辅助开发。

比如 Dart 团队提供的MCP 服务器,就可以让 AI 帮忙分析当前的布局问题,并进行修复。

以此为出发,未来的前端构建构建工具会集成相关的特性,至少会出现相关的插件来实现这类功能。

总结 ​

最后总结一下,做好前端工程化,并且持续迭代优化,已经不是可选项,而是必选项了。并不是说,最多项目热更新慢点,打包慢点,但只要开发没问题,就万事大吉了。

未来,如果是一个做得足够好的前端工程化的前端项目,在开发效率上,可以甩开其他项目一大截。甚至在 AI 快速发展的今天,还能有更多的想象空间。

而作为前端开发人员,同样也应该多关注这些底层的基建,并且沉淀出自己的一套关于构建工具在技术选型、管理维护和迭代的方法论,让自己的项目能够更好地吃到这些基建的红利。