在 Ant Design v5 中有一个非常大的变化,就是弃用 less,采用 CSS-in-JS。官方文档还提到,CSS-in-JS 本身具有按需加载的能力,不再需要 babel-plugin-import 进行按需引入。这里就有一个问题,antd 原生按需引入是如何实现的。有意思的是,babel-plugin-import 作者云谦大佬,还对 antd@v5 进行了打包测试:
发布稿说「新的 CSS-in-JS 方案原生支持 Tree Shaking,在 v5 你不在需要使用 babel-plugin-import 摘除未使用到的样式。新的方案将自动按需加载样式。」功能上没问题,我试过 dev 和 build 模式下,用
import { Button } from 'antd'和import Button from 'antd/es/button'的产物尺寸是完全一致的。但是在构建速度上会有差异,因为 tree-shaking 是有消耗的。我用 hyperfine benchmark 了下,排除了所有其他内容包括 react 和 react-dom,只有 antd 的 Button 的场景,用 babel-plugin-import 相比不用会快 38%(根据用的 antd 组件数量会有不同)。所以基于此,Bigfish 框架里还是会默认带上 babel-plugin-import。
个人看法,Tree-Shaking 确实可以干掉未使用导出,实现按需引入效果,但是 Tree-Shaking 属于代码优化,应该只在生产环境下做(标记未使用导出,然后依赖 Terser 删除,因此必须要启用代码压缩),开发环境按需打包应该是另外一套机制。
通过研究 antd 发包的内容,node_modules/antd/es 目录下存放所有组件,通过 index.js 做 re-export。因此我们在业务代码中 import { Button } from "antd",首先会先引入这个 index.js,然后再转发到 Button 组件。
那么为啥 Webpack 不打包这个 index.js,而是直接打包 Button 组件呢。我们知道,Webpack 5 是针对 re-export 场景做了优化的,可以通过依赖分析发现 index.js 做了 re-export。但是想要不打包 index.js 还需要做一件事,就是判断 index.js 有没有副作用。我们知道在 Webpack 中有一个 optimization.sideEffects 配置(可以在 npm 包的 package.json 中配置),用于提供 hint 告诉 Webpack 该依赖包没有副作用,或者某些模块存在副作用,当存在未使用导出,且该模块不存在副作用,Webpack 就可以安全地把该模块删除。通过查看发包内容,antd 确实在 package.json 中配置了 sideEffect 数组,其中 JS 模块都不存在副作用,因此 Webpack 可以安全地把 index.js 以及其他无用 export 都排除掉。
我们可以用下面的代码做一个实验:
- src
- components
- Home.jsx
- UserInfo.jsx
- index.jsx
main.jsx
webpack.config.js
其中 components/index.jsx 内容如下:
export { default as Home } from "./Home";
export { default as UserInfo } from "./UserInfo";
在 main.jsx 中引入 Home 组件:
import { Home } from "./components";
为避免生产环境下 Tree-Shaking 影响,我们用开发模式进行打包,查看打包产物,发现不仅 Home.jsx 被打包,UserInfo.jsx 和 index.jsx 也被打包了。我们在 components 目录下建一个 package.json,内容如下:
{
"name": "awesome npm module",
"version": "1.0.0",
"sideEffects": false
}
此时再用开发模式进行打包,发现产物中只有 Home.jsx,实现了和 antd 按需打包同样的效果。需要注意的是,此时模块包里面不能包含 sideeffect import(例如导入样式、polyfill 等),例如在 Home.jsx 里面用了 import "./style.css" 则会被 Webpack 直接排除掉。反过来,如果在 package.json 中配置 "sideEffects": ["*.css"],这样虽然 sideeffect import 可以打包了,但是按需打包的作用也失效了。例如在 UserInfo.jsx 中 import "./style.css",然后 main.jsx 中 import { Home } from "./components";,UserInfo.jsx 仍然会被打包进来。这也是 antd@v5 弃用 less,采用 CSS-in-JS 的部分原因。
查看 Webpack 文档,optimization.sideEffects 需要依赖 optimization.providedExports 配置,而 optimization.providedExports 配置主要作用就是优化 re-export 场景,而且该配置在开发环境也是默认开启的(optimization 有相当一部分配置在开发环境也会启用)。Webpack 文档中还提到,启用 optimization.providedExports 会对构建效率有一定影响,但是对减小产物体积有积极作用。从这个角度来说,如果用 babel-plugin-import 按需引入,无需借助复杂依赖分析,整体构建路径确实是最短的。
optimization.sideEffectsdepends onoptimization.providedExportsto be enabled. This dependency has a build time cost, but eliminating modules has positive impact on performance because of less code generation. Effect of this optimization depends on your codebase, try it for possible performance wins.
从 antd 总结模块包设计最佳实践:
- 平行输出保留原始目录结构,便于用户进行按需引入、有利于打包工具 Tree-Shaking
- 用 index.js 做 re-export,同时声明依赖包 sideeffect free,有利于 Webpack 做依赖分析,排除无用导出
- 现代化 CSS 方案,例如 antd@v5 弃用 less,采用 CSS-in-JS,可以更好地支持 Tree-Shaking,排除无用导出
参考: