最近基于 ES module 的 bundleless 构建工具很火,特别是尤大在到处推广 Vite ,号称“下一代前端构建工具”,在前不久的 VueConf 2021 上,尤大可谓狠狠地宣传了一下。以至于一度有很多人认为,有了 Vite 之后就不再需要 Webpack 了。虽然 Vite 现在确实很火,也有很多人在尝鲜,但现实恰好与大家的观念相反,目前几乎没人把 Vite 用于实际的项目开发。
首先先要先明确一个问题,为什么 Webpack 需要打包?个人认为主要有三点原因:
第一,在过去由于 HTTP/1.1 受网络延迟和浏览器并行请求限制等原因,将资源合并压缩有助于减少 HTTP 请求从而提升页面加载性能。
第二,JS 本身并没有提供模块系统。虽然 Node.js 中基于 CommonJS 规范实现了一套模块化机制,但是浏览器环境并不支持。而且虽然 ES2015 标准中提供了语法层面的模块化规范,但是现在仍然是 implementation-defined 的状态,无论是 Node.js 还是浏览器均不支持。直到 2017 年 Chrome 61 发布,浏览器才具有了对 JavaScript Module 的原生支持(Node.js 在 14.x 版本也正式支持了 ES Module)。因此 Webpack 实际上实现了一套模块系统,可以在不支持 JavaScript Module 的环境使用,同时解析模块依赖,将模块打包为一份文件。
第三,前端工程化的核心。Webpack 核心功能就是模块打包器,并且最开始只能处理 JS 模块。但是随着生态的发展,出现了各种 loader
、plugin
,使得 Webpack 可以处理 CSS 、图片、字体等静态资源。而且 Webpack 在打包过程中还可以对模块内容进行处理,典型的例子就是 @babel/preset-env
配合 core-js
对 ES2015+ 的语法进行转换,编译兼容老版本浏览器,此外还可以编译转换 .less
、.vue
、.jsx
、.tsx
等在浏览器无法识别的文件格式。不仅如此,Webpack 还提供了 webpack-dev-server
,配合 HMR 极大地提升了开发效率。
当然 Webpack 的痛点也不少,由于 Webpack 在打包时需要构建模块依赖图,因此需要递归遍历所有的模块,最明显的就是随着项目体积的增长,构建的时间也线性增长,启动时间也相应增加。第二,当我们修改了其中的文件之后,Webpack 需要重新打包。第三,由于 Webpack 在打包后会对源码进行编译和压缩,代码的可读性大大降低,因此在开发环境下,我们需要借助 SourceMap 进行调试,而开启 SourceMap 无疑也会降低打包速度。第四,随着项目体积增加,打出来的包也越来越大,过大的包会延长页面首屏加载时间,对此 Webpack 也有解决方案,即使用 Tree Shaking 移除没有用到的代码(主要针对第三方库),和分包策略,包括 lazy-loading、SplitChunksPlugin 等。
那么为什么现在开始尝试不打包了?首先最主要的原因是 HTTP/2.0 实现了多路复用和首部压缩,解决了 HTTP/1.1 中队头阻塞的问题,因此通过资源合并压缩减少 HTTP 请求对页面加载性能不会有显著提升了。其次,随着 Chrome 61 发布,现在各大浏览器逐一支持 ESM ,直接浏览器便可解析 imports ,无需自己再实现模块化了。通过浏览器原生 ESM 最大的好处就是可以实现按需加载,不用打包了,构建时间复杂度 O(1),启动很快,只需要启动 devServer 即可。
和 Snowpack 类似,Vite 也是利用浏览器原生的 ESM 去解析 imports ,然后向 devServer 请求模块,在服务器端使用 esbuild 对模块进行即时编译后返回(主要是转换 .jsx
、.tsx
、.vue
、.less
等浏览器无法处理的文件,而不是编译为 ES5 兼容老旧浏览器)。那么为什么 Vite 使用 esbuild 进行编译?一个字:快!我们可以看一组对比,Webpack5 打包需要 54.5s,而 esbuild 仅需 0.37s。这是因为 esbuild 是用 Golang 编写的,自然比用 JS 编写的构建工具快很多。同时,esbuild 中的算法经过精心设计,大量使用了并行操作,可以充分利用 CPU 资源。另外,esbuild 没有使用第三方依赖,和内存的高效利用,也是打包快的原因。
尽管 esbuild 优势十分明显,但是它仍然无法取代 Webpack ,主要有两点原因:第一,esbuild 虽然有 loader ,但是没有插件机制;第二,esbuild 没有热更新。那么 Vite 在 esbuild 的基础上解决了热更新的问题,是不是就可以干掉 Webpack 了。
现在回到开头那个问题,为什么现在几乎没人把 Vite 用于实际项目的开发?首先一个就是,虽然 Vite 在开发环境使用原生 ESM,但是在生产环境仍然需要使用 Rollup 打包。因为嵌套 import 会导致发送大量网络请求,即使使用 HTTP/2.0 ,直接使用未打包的 ESM 仍然效率低下。所以为了在生产环境中获得最佳的加载性能,最好将代码 bundle 一遍(结合 Tree Shaking,lazy-loading 和 SplitChunksPlugin 等技术手段)。所以现在看来,Vite 更像是一个开发工具,而不是用于生产环境的构建工具。另外,开发环境使用浏览器 ESM 解析模块,而生产环境使用 Rollup 打包,这就导致本地和线上环境跑的代码不一致,线上环境出了 bug,本地很难排查。那有的同学会问,为什么 Vite 不用 esbuild 打包?esbuild 速度非常快,已经是一个非常强大的Bundler,但一些构建应用所需的重要特性仍在开发中——特别是 代码分割
和 CSS处理
。目前,Rollup 在这些方面更加成熟和灵活。也就是说,当 esbuild 在将来稳定这些特性时,不排除在生产版本中使用 esbuild 的可能性。
然后实际上 Webpack 也并非每次构建都是 O(n)
复杂度。对于不经常更新的第三方库,完全可以采取预构建策略,即对 lodash
、moment
、react
等依赖提前进行打包,缓存到磁盘上,这样每次只要对业务代码进行打包,构建速度得到显著提升。Webpack 4 的 DllPlugin
就可以实现这个需求。然后你可能会说这样处理的粒度还不够细,对于没有修改的业务组件,也可以跳过打包,直接使用缓存,在 Webpack 4 中提供了 cache-loader
就可以实现模块的缓存,Webpack 5 直接提供了 cache
的配置项,可以缓存到磁盘上。另外,Webpack 的分包策略,可以使得打出来的 Bundle 不至于过大,假如配置了路由懒加载,当路由变化时,就可以通过开发者工具的 Networks 面板观察到异步请求的模块。
反观 Vite ,其实很难真正做到 O(1)
构建效率。Vite 在首次启动的时候会进行依赖预构建,进行粗略的打包,一方面为了兼容 CommonJS 模块,另一方面是减少网络请求,如果完全不打包会导致大量网络请求。
综上,Vite 其实和 Vue-cli 一样,可以说是 Vue 官方给个人开发者和中小企业提供的脚手架工具,毕竟不用自己造轮子了。正如 Vue-cli 就是对 Webpack 的封装一样,Vite 其实就是对 esbuild 的封装,因此 “Vite 能干掉 Webpack” 这句话本身就是不对等的,充其量作为下一代脚手架工具还差不多。而且 Vite 现在还只能在开发环境用 ESM ,生产环境还是需要打包,基本上只有浏览器支持完全和基于 QUIC 协议的 HTTP/3 普及之后才可能大规模使用 ESM 。更何况 Webpack 发展至今 loader 和 plugin 生态已经十分丰富,而 bundleless 构建工具的生态还在起步阶段。长远来看 ESM 确实极具发展潜力,不排除以后 bundleless 构建工具可能会超过 Webpack ,但是 Webpack 在现在的前端工程化中仍然扮演着非常重要的角色。
参考
Babel 官方网站
为什么选择 webpack - webpack 官方文档
Vite.js 中文网
「 不懂就问 」 为什么 esbuild 这么快 ?
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。