npm依赖优化分析

542 阅读5分钟

在我们的项目中,往往通过npm依赖了大量的第三方包,npm的包管理机制能够确保同一个包的多个版本不冲突,但这样就造成了可能同一个包的多个版本都存在项目的node_modules中,最终打包出来的生产文件中也包含同一个包的多个版本,造成冗余,本文将对该问题进行探讨。

包的多版本问题

由于npm包含了大量的包,并且包与包之间会互相引用,形成了庞大的依赖树,以我们的其中一个项目为例,经统计,我们直接依赖的包,也就是写在package.json里面的包(dependencies和devDependencies,下文成为顶层依赖),有51个,而最终安装完成后记录在yarn.lock中的包数量有1788个。当不同的包依赖了同一个包的不同版本时,为了保证最终的代码能正常运行,npm会把同一个包的不同版本都进行安装,于是在包的数量变大时,同一个包的不同版本数量也会急剧变大。还是以我们的项目为例,经过统计,不考虑版本的不同,一共有1788个包,考虑到同一个包的不同版本,一共有2292个包。也就是说有额外多出了504个多版本的包,统计结果如下表所示。而这504个多版本包,是由323个包造成的,之所以数量不一致,是因为有的包的版本数多于2。比如有6个包存在了5个版本。

包的不同版本数包的数量
2258
351
48
56

这本是预期中的事情,毕竟同一个包的不同版本使用方式不同,如果包A依赖了包C的旧版本1,包B依赖了包C的新版本2,包的版本1和保本2就该同时存在,供包A和包B使用。最理想的情况是将包A进行改造,使用最新的版本2的包C,这样项目中就只需存在包C的一个版本了。然而现实中的情况是,很多包在项目中是以间接依赖引入的,我们的package.json里面没有直接引入它,这就导致除非我们能够修改引入的第三库的代码,否则这个重复依赖就没法去除。

举个例子,@babel/generator在依赖树中出现了多个版本,其中的两条依赖路径如下所示:

@babel/preset-env@7.12.1 ->
@babel/plugin-transform-async-to-generator@7.12.1 ->
@babel/helper-remap-async-to-generator@7.12.1 ->
@babel/helper-wrap-function@7.10.4 ->
@babel/traverse@7.11.5 ->
@babel/generator@7.11.6
@babel/preset-env@7.12.1 ->
@babel/plugin-transform-classes@7.12.1 ->
@babel/helper-replace-supers@7.12.1 ->
@babel/traverse@7.12.1 ->
@babel/generator@7.12.1

我们的项目中直接依赖的是@babel/preset-env@7.12.1,最终导致依赖了两个版本的@babel/generator。两个版本的分叉出现在@babel/helper-wrap-function@7.10.4上,最好的方式是@babel/helper-wrap-function@7.10.4进行更新,将它对@babel/traverse的依赖更新成7.12.1。

包的多版本分析

项目中的大多数依赖都如上面的分析那样,由第三方包造成,难道我们就束手无策了吗?在考虑如何进行优化前,首先得有个基本的认识,项目中的重复依赖到底有多大的量,对项目造成了多大的影响。

因此我先写了一个统计程序,这部分信息直接从项目里面的yarn.lock就分析得到。

包分析结果1.png

由第三方包造成造成的依赖,我们没法改变,但是项目中由我们主动依赖的包,我们可以决定它们的版本号,所以这里的思路是让我们的依赖包去迎合第三方依赖的包。

我写的分析程序也沿着这个思路进行了统计分析,在重复的包里面查找有哪些是和顶层依赖有冲突的。找到了重复包后,再找到是哪个顶层导致该重复包的引入。由下面分析结果的第一条@ant-design/icons为例子,项目里面直接引入了@ant-design/icons@^4.3.0,而引入的antd@^4.18.6又引入了@ant-design/icons@^4.7.0,导致存在了两个版本的@ant-design/icons。分析项目里面的代码,只是引用了@ant-design/icons的几个图标,对于@ant-design/icons的版本没有强依赖。

依赖冲突.png

解决方式有两个:项目里面不直接依赖@ant-design/icons,因为antd依赖了它,我们也可以使用;项目里面依赖@ant-design/icons@^4.7.0,向antd看齐。这样就只存在一份@ant-design/icons了。

从上面的图中可以看出,一共找到了12处类似这样可以优化的地方。按照类似的方式,进行改造。由于不同版本的包的使用方式可能不太一样,这样更改项目中的包的版本可能会造成项目运行出错,需要评估和测试这样替换会造成的影响。比如react-router-dom的5.X版本和6.X版本,接口发生了重大变化,更换版本会造成项目中的大量代码需要改动。我对影响不大的包进行优化之后,最后再分析的结果如下所示。

包分析结果2.png

包的总数量从2292降到了2132,顶层依赖还有4个可以依赖的地方。如果有精力去改动使用这4个包的代码的话,还有进一步下降的空间。

上述提到的分析程序可以参考我的 github

总结

本文分析了npm包多版本产生的原因,以及分析的方法,通过我的程序可以对分析过程给予指导。在分析完成后,还需要依靠开发人员手动进行优化,减少项目中的重复依赖,提升构建速度,减少打包出的文件大小。