Webpack 打包太慢?来试试 Bundleless

一 引言

Webpack 最初是为了解决前端模块化以及使用 Node.Js 生态的问题而出现,在过去的 8 年时间里,Webpack 的能力愈来愈强大。前端

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

但由于多了打包构建这一层,随着项目的增加,打包构建速度愈来愈慢,每次启动都要等待几十秒甚至几分钟,而后启动一轮构建优化,随着项目的进一步增大,构建速度又会下降,陷入不断优化的循环。node

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

在项目达到必定的规模时,基于 Bundle 的构建优化的收益变得愈来愈有限,没法实现质的提高。咱们从另外一个角度思考,webpack 之因此慢,主要的缘由仍是在于他将各个资源打包整合在一块儿造成 bundle,若是咱们不须要 bundle 打包的过程,直接让浏览器去加载对应的资源,咱们将有可能能够跳出这个循环,实现质的提高。react

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

在 Bundleless 的架构下,咱们再也不须要构建一个完整的 bundle,同时在修改文件时,浏览器也只须要从新加载单个文件便可。因为没有了构建这一层咱们将可以实现如下的目标:webpack

  • 极快的本地启动速度,只须要启动本地服务。
  • 极快的代码编译速度,每次只须要处理单个文件。
  • 项目开发构建的时间复杂度始终为 O(1),使得项目可以持续保持高效的构建。
  • 更加简单的调试体验,再也不强依赖 sourcemaps 便可实现稳定的单文件的 debug。

基于以上的可能性 Bundleless 将从新定义前端的本地开发,让咱们从新找回前端在 10 年前修改单个文件以后,只须要刷新便可即时生效的体验,同时叠加上前端的 HotModuleReplace 相关技术,咱们能够把刷新也省去,最终实现保存即生效。es6

实现 Bundleless 一个很重要的基础能力是模块的动态加载能力,这一主要的思路会有两个:web

  • System.js 之类的 ES 模块加载器,好处是具备较高的兼容性。
  • 直接利用 Web 标准的 ESModule,面向将来,同时总体架构也更加简单。

在本地开发过程当中兼容性的影响不是特别大,同时 ESModule 已经覆盖了超过 90% 的浏览器,咱们彻底能够利用 ESModule 的能力让浏览器自主加载须要的模块,从而更加低成本同时面向将来实现 Bundleless。chrome

社区中在近一两年也出现了不少基于 ESModule 的开发工具,如 Vite、Snowpack、es-dev-server 等。本文将主要分享基于浏览器的 ESModule 能力实现 Bundless 本地开发的相关思路、核心技术点以及 Vite 的相关实现和在供应链 POS 场景下的落地实践。json

二 从资源加载看 Bundle 和 Bundleless 的不一样

下面以你们最熟悉的 create-react-app 默认项目为例,从实际的页面渲染资源的加载过程对比 Bundle 和 Bundleless 的区别。前端工程化

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

基于 Webpack 的 bundle 开发模式浏览器

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

上面的图具体的模块加载机制能够简化为下图:

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

在项目启动和有文件变化时从新进行打包,这使得项目的启动和二次构建都须要作较多的事情,相应的耗时也会增加。

基于 ESModule Bundleless 模式

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

从上图能够看到,已经再也不有一个构建好的 bundle、chunk 之类的文件,而是直接加载本地对应的文件。

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

从上图能够看到,在 Bundleless 的机制下,项目的启动只须要启动一个服务器承接浏览器的请求便可,同时在文件变动时,也只须要额外处理变动的文件便可,其余文件可直接在缓存中读取。

对比总结

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

Bundleless 模式能够充分利用浏览器自主加载的特性,跳过打包的过程,使得咱们能在项目启动时获取到极快的启动速度,在本地更新时只须要从新编译单个文件。下面将分享如何基于浏览器 ESModule 的能力实现 Bundleless 的开发。

三 如何实现 Bundleless

如何使用 ESModule 模块加载

实现 Bundleless 的第一步是要让浏览器自主加载对应的模块。

使用 type="module" 开启 ESModule

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

利用 import-maps 支持 bare import

分享一个在 chrome 中已经实现了的 import-maps 的标准 ,可让咱们直接用 import React from 'react' 这样的写法,将来咱们能够利用此能力实现线上的 Bundleless 部署。

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

以上咱们介绍了浏览器中原生的 ESModule 是如何使用的。面向本地开发的场景,咱们只须要启动一个本地的 devServer 承载浏览器的请求映射到对应的本地文件,同时动态地将项目中 import 的资源路径指向咱们的本地地址,便可让浏览器直接加载本地的文件,好比可使用下面的写法,将入口 JS 文件直接指向本地的路径,而后 devServer 再拦截相应的请求返回对应的文件。

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

如何加载非 JS 的文件资源

经过 ESModule 咱们借助浏览器的能力实现了 JS 的自主加载,但实际的项目代码中咱们不只仅会 import JS 文件,也会有下面的写法:

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

而浏览器在处理文件时是依据 Content-Type 的,不关心具体的文件类型,因此咱们须要在浏览器发起请求时,将对应的资源转化为 ESModule 格式,同时设置对应的 Content-Type 为 JS,返回给浏览器执行,浏览器就会按照 JS 的语法进行解析处理,总体的流程可见下图:

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

如下是 Vite 的相关实现,在请求返回的过程当中,对不一样的文件进行动态处理:

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

如何实现 HotModuleReplace

HotModuleReplace 可以在咱们修改代码后,不须要刷新页面,直接在当前场景下生效,结合 Bundleless 极快的生效速度,咱们可以实现几乎没有延迟的保存即生效的体验。对于 React,在 Webpack 场景下目前只能经过使用 react-hot-loader 来实现,但这一块受限于具体的实现,有一些场景会存在 bug,做者也建议迁移到 React 团队实现的 react-refresh,而这一块在 Webpack 中尚未相应的实现。在 Bundleless 场景下,由于咱们的每一个组件都是独立加载的,因此要集成 react-refresh,咱们只须要在浏览器请求返回时在文件的顶部和底部加上相应的脚本便可完成集成。

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

要完整的实现 HotModuleReplace 会比上面画得更加复杂,还须要有一套依赖分析机制来判断当一个文件发生变动以后要替换哪些文件以及是否须要 reload。在 Bundleless 的场景下,由于再也不须要打包为一个完整的 bundle,同时咱们也能更加灵活地对单个文件进行修改,这一块相关的实现会更加容易。

如下是在 Vite 中的相关实现:

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

如何优化大量请求致使页面加载慢 Bundleless 的模式再也不打包,提高了启动的速度,但对于一些有较多外部依赖或者自身文件数量较多的模块,须要发起大量请求才能获取到所有的资源,这个会下降开发过程当中页面加载的时间。好比下面是直接在浏览器中 import lodash-es 会并发出大量的请求:

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

在这一块上咱们能够作相应的优化,将外部的依赖提早打包成单个文件来减小在开发过程当中因为外部依赖过多而发起过多的网络请求。

在 Vite 的启动流程中有一个 vite optimize 的过程会自动将 package.json 中的 depenencies 借助 Rollup 打包成 ES6 Module。

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

提早打包带来的好处除了可以提高页面的加载速度,借助 @rollup/plugin-commonjs 咱们可以将 commonjs 的外部依赖打包为 ESModule 的形式引入,进一步扩大 Bundleless 的适用范围。

四 在供应链 POS 场景下落地实践

咱们团队负责的供应链 POS 业务主要可分为面向建材家居的家装行业和线下小店的零售行业,在技术架构上采用了各个域 bundle 独立开发,而后最终借助底层的 sdk 合并为一个大的 SPA 的形式。因为项目的复杂性,在平常开发过程当中,有如下的一些痛点:

  • 项目的启动和耗时相对较长。
  • 改动后二次编译时间长。
  • 缺乏稳定的 HMR 能力,开发过程当中须要重复造场景。
  • debug 依赖 sourcemaps 能力,有时会出现不稳定的状况。

基于以上的问题,借助 Vite 的相关实现,咱们对本地开发环境进行了 Bundleless 的尝试和落地,在实验的一些项目中对于本地的开发体验有了很大的提高。

在启动以及修改生效的速度上带来极大的提高

目前已实现单 bundle 维度的开发,打包构建速度:

Vite Bundleless

在启动单个 bundle 时,Webpack 须要 10s 左右的时间,而基于 Bundleless 的 Vite 只须要 1s 左右,提高 10 倍。

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

总体的页面加载时间在 4s 左右,仍然比 Webpack 的打包构建时间要短,同时从上面的视频中也能够看到 HMR 的速度达到了毫秒级的响应,实现了基本无感的保存即生效。

不依赖 sourcemap 调试单个文件

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

落地过程当中遇到的问题和解决

在实际落地过程当中,遇到的问题主要是相关模块不符合 ESModule 规范以及一些写法上的标准化:

  • 部分模块没有 ESModule 的打包。
  • less 依赖 node_modules 的写法的规范。
  • jsx 文件后缀规范。
  • babel-runtime 的处理。

部分模块没有 ESModule 的打包

对于没有 ESModule 打包输出或者输出的错误的包,根据不一样的类型使用不一样的策略:

  • 内部的包:经过升级脚手架,发布带有 ESModule 的包的新版本。
  • 外部依赖:经过 issue、pull request 等形式,推进了 number-precision 等模块的升级。
  • 同时有一些因为历史缘由没法打出 ESModule 的包能够借助 @rollup/plugin-commonjs 打包为 ESModule。

less 依赖 node_modules 的写法的规范

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

JSX 文件后缀规范

Vite 在运行的过程当中会依据文件不一样的后缀名进行对应的编译处理,而在 Webpack 模式下咱们一般会将 JSX、JS 等文件都丢给 babel-loader 进行处理,这使得有一些本来是 JSX 的文件没有写 JSX 后缀。Vite 只会对 /.(tsx?|jsx)$/ 的文件进行 esbuild 编译,对于纯 JS 会直接跳过 esbuild 的过程。对于这种状况咱们是逐步将错误的原先没有写 JSX 的文件迁移为 JSX 文件。

babel-runtime 的处理

在使用了 babel-plugin-transform-runtime 以后,打包的输出结果会是下面这样:

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

上面所引用的 @babel/runtime/helpers/extends 是 commonjs 的格式没法直接使用,针对这个状况,有两种解法:

1)针对内部本身打包的模块,能够在进行 es6 打包时添加 useModules 配置,这样打包出来的代码就会是直接引用@babel/runtime/helpers/esm/extends :

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

2)针对从新打包成本较高的模块,能够经过 Vite 的插件机制进行转换,将 @babel/runtime/helpers 在运行时替换为 @babel/runtime/helpers/esm 能够经过 alias 配置实现:

9989c5b90f96dedb20d3e717592eeed2c54bdb86.jpeg

以上是在 Vite 开发环境的迁移过程当中遇到的一些问题和处理的分享,这一块的更大范围的落地还在进行中。Bundleless 的落地不只仅是为了适配 Vite 的开发模式,同时也是面向将来规范各个模块代码的过程,将咱们的模块进行标准的 ESModule 化,在有新的工具和思想出现时能够用更低成本进行落地。

五 直接使用 Bundleless 进行部署的可行性

受限于网络请求和浏览器的解析速度,对于较大型的应用,bundle 在加载速度上仍是可以带来较大的收益。V8 在 2018 年也给出了相关性能上的建议:在本地开发和小型的 Web 应用中使用。在今天的场景下,随着浏览器和网络性能的不断提高,结合 ServiceWorker 之类的缓存能力,网络加载的影响和愈来愈小,对于一些不须要考虑兼容性问题的场景能够进行内部的尝试,直接部署经过 ESModule 加载的代码。

六 总结

本文主要分享了 Bundleless 架构下,如何提高前端的研发效率、实现思路以及在具体业务场景下落地实践。Bundleless 本质上是将原先 Webpack 中模块依赖解析的工做交给浏览器去执行,使得在开发过程当中代码的转换变少,极大地提高了开发过程当中的构建速度,同时也能够更好地利用浏览器的相关开发工具。

站在当前的背景下,Web 各个领域 JavaScript/CSS/HTML 相关的标准都已成熟,同时浏览器内核也趋于统一,前端工程化的核心重点已逐步迁移到研发提效上,而 Bundleless 的模式可以带来长效的启动和 HMR 的速度,是将来的一大发展趋势。随着浏览器内核和 Web 标准的不断统一,前端的代码能够再也不打包直接运行将成为可能,这将进一步提升总体的研发效率。

最后很是感谢 ESModule、Vite、Snowpack 等标准和工具的出现,让前端的开发体验往前跨了一大步。

相关文章
相关标签/搜索