最近接到一个需求是将前端应用的 node_modules
瘦身,目的是减少安装依赖所需时间。
下面记录下我的处理过程:
一、确定衡量标准及测量工具
目的很明确,减少安装依赖所需时间。目标平台是无SSD硬盘的Windows系统,使用自建npm镜像。
减少依赖安装时间有两个主要的策略,一是镜像加速,二是减少需要安装的包的数量,即瘦身。
镜像加速之前已经有自建镜像了,因此本次只做安装包的数量方面的瘦身。
衡量标准有以下几个:
uu
标准 | 稳定性 | 相关性 | 单位 | 初始值 | 当前值 |
---|---|---|---|---|---|
npm包数量 | 稳定可重测 | 中 | 个 | 1617 | 1617 |
node_modules体积 | 稳定可重测 | 强 | MB | 473 | 473 |
安装时间 | 不稳定,多次测试之间有误差 | 目标本身 | 秒 | 183.42 | 183.42 |
安装时间是目标本身,但是不够稳定,受干扰比较多。所以要多次测量求平均值,同时使用其他的稳定标准(如需要安装的包的数量、node_modules体积等)辅助参考。
使用测试工具如下:
- npm包数量和安装时间:删除
node_modules
和package-lock.json
之后,执行npm install --registry
http://ynpm.yonyoucloud.com/repository/ynpm-all/
,执行完成后会有一条信息报告安装包的数量和所耗费的时间。如 added 1617 packages from 1047 contributors in 183.42s。 - node_modules体积:
du -h -d 0 node_modules
二、观察现状
如上所述,待优化的工程目前的npm包数量是1617,安装所耗费的时间是183.42s。知道这个信息能提供很好的衡量标准,但是还不足以指导优化方向。
2.1 提出假设
我们可以提个假设,npm包体积与下载时间正相关,也与安装时间正相关,它大概率上是成立的。基于这个假设,我们可以通过降低 node_modules
的体积,来达到减少安装时间的目的。
分析下现在 node_modules
中各个子目录的体积:
使用如下的命令:du -h -d 1 node_modules | sort -h
就可以得到按文件体积从小到大排列的子目录列表,如下图所示:
从图中可以看出各个包(或scope)占用的体积。上图中 antd、echarts、antd-mobile等包占据了较多的体积。
2.2 验证假设
为了验证之前的假设有效,我们在package.json中将比较大的几个包删除掉,再重新npm install试一下,看看结果是不是符合预期。
操作:从package.json中删除 antd、echarts、react-multi-crops、@icons、caniuse-db、antd-mobile、@mdf、@antv、rxjs(共占据199MB的体积,约47%),然后重新安装。
期望结果:安装时间下降 30% ~ 60% 之间。
实际结果:added 1314 packages from 828 contributors in 97.407s,安装时间下降了46%。
结论:实际结果符合期望,且表现出很强的相关性,假设成立。
2.3 小结
因为node_modules的体积与其安装时间有较强相关性(2.2中验证),所以我们的优化方向可以转变为更好测量的 node_modules体积上,且应该先从单包体积较大的入手。
实际上有些包不是项目直接依赖,而是某个依赖包二次依赖的的,如rxjs是因为被concurrently工具依赖的才会安装上的。这些包在衡量的时候,应该算到依赖方上。不过这个暂时不用过多考虑,具体情况具体分析即可。
三、可能的优化思路
既然现在优化的方向是减少包的体积,那么就自然地会有如下的一些优化方向:
3.1 提前打包依赖
以antd举例,它的包的体积达到了39MB,而一般来说一个前端应用构建后的bundle体积在5M以内,甚至更小,即使加上 source-map,也少于 antd 的体积。再算上 antd-mobile 等,预先打包 bundle 可能就能节省 (39 + 13 - 5) = 43M 这样的体积。
如果这个方式可行,能节省出较大的空间,大大降低node_modules的体积及下载所需时间。
这种方式可能还可以考虑像 electron-prebuilt 那样,直接发布二进制包,不过对项目的要求比较高,应该也会丧失较多的灵活性,所以暂时不考虑这方面。
3.2 Cli工具全局安装
上面提到的rxjs,占据了11M的空间,其实只是 concurrently 这个小工具的一个依赖项。我们项目对concurrently工具的依赖不太强烈,基于属于可有可无的状态。为此耗费这么多的空间,似乎不是那么值得。
另外像rimraf,cross-env等跨系统兼容cli工具,也可以考虑全局安装。
3.3 删除无用依赖
有些依赖项,可能在应用中已经不再使用了,但是由于历史遗留原因,没有清除。
使用类似 depcheck 之类的工具,可以检测出哪些依赖没有被使用,然后删除掉它们(需要注意不要误删一些cli工具,像webpack之类的)。
可以重点检查下babel插件、babel preset、webpack插件等。
3.4 依赖替换
这就涉及到代码上的变更了。对于一些体积较大的包,可以考虑使用同质的更小的包来实现。
比如用dayjs替换moment。
四、动手操作
从之前的分析结果可以看出,占据主要体积的是组件库相关的包,如antd、antd-mobile、@antv等,这部分会是优化中的重点,同时它也存在难点。
同时占据一定的体积,又比较好实现的,是一些cli工具包。柿子先挑软的捏,我们先从cli工具下手。
4.1 删除一些cli包,改为全局安装
可以先删除一些明显的、不需要本地安装的cli包,然后再查看 node_modules/.bin 目录中有哪些命令,看看有没有遗漏。
在示例项目中,我找到了 concurrently、cross-env、nodemon这几个包,删除掉改成全局安装。
类似babel、webpack之类的,因为不同版本之间差异比较大,不适合全局安装。
删除这几项之后,node_modules体积变化为 473MB -> 450MB。
优化不明显。
4.2 检测和删除无用包
使用depcheck工具可以检测出未使用的包。当然其中也会包括一些cli工具,这部分要忽略掉。
在示例项目中,使用depcheck工具查找到有下列包未被使用:
- eslint
- eslint-plugin-react
- SvgIcon
- axios
- css-format
- crypto-js
- react-multi-crops
- react-sortablejs
- sortablejs
- css-sourcemaps-webpack-plugin
删除这些包之后,node_modules的体积变为化 450MB -> 442MB,变化不大。
看来还是应该从大头出发。
4.3 测试打包到dll中的文件可否不再安装
尝试将已经在dll中的文件删除掉(react、react-dom等),然后尝试构建。
但是构建失败了,所以这种方式不可行,即使文件已经打包进了dll,还是需要本地安装的。
4.4 尝试将部分包打包成bundledDependencies
npm有提供一种机制,叫bundledDependencies,可以在发布npm包的时候,将指定依赖包同时打入package中。
这样虽然没有降低整体的体积,但是压缩成一个文件后下载可能会快一些?
4.5 使用webpack、rollup等提前构建依赖
这种方法适用于减少依赖包的附属依赖。
比如 A 包依赖了 antd,其他包都没有。那么可以考虑将A提前build一下,出来一个 library.js、library.min.js。然而它有一些局限性:
- 不能解决主工程依赖的问题。如antd既在A中依赖,也在主工程中依赖,因为主工程不能打包,还是需要安装antd。
- 一般提供包的时候,library.js、library.min.js是和源代码一同发布的,有些dependency一般还是安装的,方便调试。如果只提供library打包后的文件,不提供源码,或虽然提供源码但是需要单独安装依赖,都不太便于调试。
4.6 将固定依赖提交到代码仓库
可以将依赖压缩成一个zip提交到代码仓库,这样不会污染git diff。
但是它也有较大的成本,git仓库的体积可能会变很大,尤其在这个zip文件经常发生变化的时候。4.7 使用externals,将比较大的依赖换成CDN引用
可以将一些关键包替换掉,但是需要库本身支持这种使用方式,像react/react-dom之类的库很容易就可以实现externals,其它的包就得分别验证了。
同时externals不能使用dll中打包的资源,要从html link中引入,有可能存在命名空间冲突等问题。
首先分析下有哪些包可以通过CDN引入:
- react
- react-dom
-
antd -
antd-mobile -
echarts-for-react -
echarts -
async-validator - cookies-js
-
classnames - immutable 可使用CDN,但是原来在使用多个版本,有一定风险
-
isomorphic-fetch -
fixed-data-table-2 - ~~ keymirror~~
- ~~ ‘redux’~~
- ~~ ‘echarts-for-react’~~
- ~~ ‘rc-calendar’~~
- ~~ ‘rc-form’~~
- ~~ ‘rc-input-number’~~
- ~~ ‘rc-notification’~~
- ~~ ‘rc-pagination’~~
- ~~ ‘rc-select’~~
- ~~ ‘rc-slider’~~
- ~~ ‘rc-table’~~
- ~~ ‘rc-tooltip’~~
- ~~ ‘rc-tree’~~
- ~~ ‘rc-tree-select’~~
除了明确支持 external 的包,比如react/react-dom,其余的都不太适合这种方式。
另外有可能因为使用这种方式,使原来不同的版本强制转换成了一个相同的版本,有可能引入bug。
还得考虑到,使用这种方式后,不容易更新依赖包的版本。
综上:这种方式无法大规模推广。
4.8 替换一些相似的包
比如使用 dayjs 代替 moment,还有将项目中同时依赖 moment / dayjs 这种情况优化掉,类似这种思路。
这样需要修改源码,有可能影响功能,需要慢慢调整。而且可预期效果不会特别理想。
未完待续