如何优化组件包体积

August 31, 2021

某天,抽取组件之后,顺利接入到项目内,想着终于搞定了。提交代码准备编译一波,给测试同学测试看看。没想到,编译之后直接收到了包体积增大告警。组件接入之后包体积增大了1.76MB。 amazing

虽然组件确实有那么点复杂, 但是不至于有这么多代码吧。怀揣着忐忑的心,看看到底是编了个啥进去。 经过使用webpack-bundle-analyzer分析,组件原来的代码确实不多。那肯定就是依赖的锅了。

依赖

引入组件之后很多的代码都是由依赖引起的。可以根据情况优化依赖关系。

考虑暴露给外部实现

这个组件有个依赖是「用户详情展示弹窗」,是个体积很大的组件。

一方面,业务方本身也会有这个弹窗的实现。组件内部再次依赖,可能导致打包多余的代码。

另一方面,这个弹窗本身也可能包含一些业务定制功能,放在组件内部也不一定合适。

所以,第一步先把「用户详情展示弹窗」弹出能力转包给业务方实现看看。

通过一个plugin的机制,把弹窗的能力注册进来,内部直接调用即可。

// 插件定义
interface BasePlugin {
  onCreate();
  onDestroy();
  run();
}

// 插件实现
class ReportPlugin extends BasePlugin {
  run(key, actions) {
    collectEvent(key, actions);
  }
}

// 插件使用
onclick() {
  getPlugin('report').run('xx', 'click');
}

// 插件维护
let pluginMap = {};

function registerPlugin(key, plugin) {
        pluginMap[key] = plugin;
}

function unregisterPlugin(key) {
        delete pluginMap[key];
}

function getPlugin(key) {
        return pluginMap[key];
}

编译之后,包体积从9.3MB到8.23MB,少了1MB,效果还是很明显。

当然,也不是所有体积大的依赖,都应该放在外面实现,还是要考虑组件使用的便利性。

peerDependence

有些依赖,在组件使用场景内,业务方基本都会有用到。组件内部可以直接使用peerDependence来依赖。这样相关的依赖就不会被安装在组件下的node_modules,而是和宿主使用同一个包。

尤其是对有些存在全局影响的npm包,更应该直接考虑使用peerDependence,比如antd这种基础组件可能会修改全局的样式,如果组件内版本和宿主版本不一致可能导致最终样式出现异常。

 "peerDependencies": {
    "xxx": ">=1.9.0"
  },

组件内部这样申明,在开发阶段也不会下载依赖,所以开发时需要手动安装。可以在devDependencies内申明一个具体的依赖:

 "peerDependencies": {
    "xxx": "1.9.2"
  },

依赖版本一致

依赖声明范围放宽

因为yarn会扁平化管理npm依赖,如果宿主和组件的依赖版本是一致,那么组件内部就会直接使用外部的依赖。所以,对于某些不同版本兼容性比较好的依赖库,可以把依赖声明放宽一些,比如使用^2.0.0而不是指定具体版本。

如果某个版本之后存在兼容性问题,也可以使用>=2.0.0 <=2.3.3的方式来指定范围。

monorepo组件库的依赖,尽量一致

对于使用monorepo方式维护组件的仓库,所有组件原则上应该依赖相同版本的依赖库。因为业务方很可能不止使用其中一个组件。当需要安装多个组件时,相同的依赖库版本,可以减少安装不同版本依赖的可能性。

但是这种情况也有个badcase,如果业务方本身依赖了一个1.0.0的库A,在组件库内部依赖了^1.1.0的库A,yarn安装时会导致每个组件下都有个库A的依赖,而且版本是一样的。之后webpack构建的时候就会认为这些依赖是不一样的,而把相同的库打包多次。

// 一个 badcase:
--node_modules
  --A@1.0.0
  --B
    --A@1.1.0
  --C
    --A@1.1.0

对于这种case还没有想到特别好的解法。而且这种问题比较隐蔽,如果没有监控,不一定能够及时发现。

一种思路是,使用webpack插件来监测如果存在这种情况,给业务方一个预警。但是这种需要业务方接入,不一定能够很好地执行。

组件内部依赖申明范围尽可能大一些,也可以一定程度规避这种问题。

国际化文案

目前我们国际化文案有16种语言,全量打包进去,对包体积会有一定的增大。而且大部分情况下都是使用一个语言,其他的资源都是无效的。

以我这个组件为例:整体包大小为25kb,文案大小4kb。占比16%.

可以考虑把多语言包动态加载,组件内部导出各个语言包的内容。在使用时,由调用者在外部传入需要使用的语言包。

Loadable({
    loader: () =>
      Promise.all([
        import(
          /* webpackChunkName:"my-component" */ '@com/my-component',
          ),
        import (`@com/my-component/es/i18n/locales/${locale}.js`)
      ]),
    loading: xx
    render: xxx
  });

TreeShaking

组件内部有些代码可能并不是必须的,如果没有设置好TreeShaking,会导致业务方编译时把所有的代码都打包进来。

关键是在package.json里的设置好SideEffect

// ...
"sideEffects": [
  "**/*.css",
  "**/*.scss",
  "./esnext/index.js",
  "./esnext/configure.js"
],
// ...

如果组件会存在多种样式,每次必然只会使用其中一种,也可以考虑导出多个入口。便于根据需要使用。

结语

如果上面的一些建议还是无法把代码缩小到可接受范围,就只能继续使用webpack-bundle-analyzer来逐步分析具体依赖了,GoodLuck!

有其他的思路也欢迎补充:)

本文首发于 一粟(https://www.zeyio.com),欢迎转载,但是必须保留本文的署名和链接。

本文永久链接:https://www.zeyio.com/how-to-slim-component-size/