How to Optimize Component Package Size

August 31, 2021

One day, after extracting a component and successfully integrating it into the project, I thought everything was finally done. I submitted the code and prepared to compile it for the testing team. Unexpectedly, after compilation, I immediately received a package size increase alert. The component integration had increased the package size by 1.76MB. amazing

Although the component was indeed somewhat complex, it shouldn't have that much code. With a nervous heart, I decided to investigate what exactly was compiled into it.

After analyzing with webpack-bundle-analyzer, it turned out that the original component code wasn't that much. So, it must be the dependencies' fault.

Dependencies

Much of the code introduced after importing the component is caused by dependencies. We can optimize the dependency relationships based on the situation.

Consider Exposing Implementation to the Outside

This component had a dependency called "User Details Display Modal", which was a large component.

On one hand, the business side might already have an implementation of this modal. Having the component depend on it internally could lead to redundant code being bundled.

On the other hand, this modal might contain some business-specific customizations, which may not be appropriate to keep inside the component.

So, the first step was to try transferring the "User Details Display Modal" popup capability to the business side for implementation.

Through a plugin mechanism, we can register the modal's capability and call it directly internally.

// Plugin definition
interface BasePlugin {
  onCreate();
  onDestroy();
  run();
}

// Plugin implementation
class ReportPlugin extends BasePlugin {
  run(key, actions) {
    collectEvent(key, actions);
  }
}

// Plugin usage
onclick() {
  getPlugin('report').run('xx', 'click');
}

// Plugin management
let pluginMap = {};
function registerPlugin(key, plugin) {
        pluginMap[key] = plugin;
}
function unregisterPlugin(key) {
        delete pluginMap[key];
}
function getPlugin(key) {
        return pluginMap[key];
}

After compilation, the package size decreased from 9.3MB to 8.23MB, reducing it by 1MB, which is quite significant.

Of course, not all large dependencies should be implemented externally. We still need to consider the convenience of using the component.

peerDependencies

Some dependencies are likely to be used by the business side in most scenarios where the component is used. The component can directly use peerDependencies for these. This way, the related dependencies won't be installed in the component's node_modules, but will use the same package as the host.

This is especially important for npm packages that have global effects. For example, basic component libraries like antd might modify global styles. If the component's version differs from the host's version, it could lead to style anomalies in the final result.

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

By declaring it this way in the component, dependencies won't be downloaded during development, so you need to install them manually. You can declare a specific dependency in devDependencies:

 "devDependencies": {
    "xxx": "1.9.2"
  },

Consistent Dependency Versions

Broaden Dependency Declaration Range

Since yarn manages npm dependencies in a flat structure, if the host and component dependency versions are the same, the component will directly use the external dependency. So, for some libraries with good compatibility across different versions, you can broaden the dependency declaration, for example, using ^2.0.0 instead of specifying an exact version.

If there are compatibility issues after a certain version, you can also use >=2.0.0 <=2.3.3 to specify a range.

Monorepo Component Library Dependencies Should Be Consistent

For repositories maintaining components using the monorepo approach, all components should, in principle, depend on the same version of dependency libraries. This is because the business side is likely to use more than one component. When multiple components need to be installed, having the same dependency library versions can reduce the possibility of installing different versions.

However, there's a bad case for this situation. If the business side depends on library A version 1.0.0, and the component library internally depends on ^1.1.0 of library A, yarn installation will result in each component having a dependency on library A, all with the same version. Later, when webpack builds, it will consider these dependencies as different and package the same library multiple times.

// A bad case:
--node_modules
  --A@1.0.0
  --B
    --A@1.1.0
  --C
    --A@1.1.0

I haven't found a particularly good solution for this case yet. Moreover, this problem is quite subtle and might not be discovered in time without monitoring.

One approach is to use a webpack plugin to detect such situations and give a warning to the business side. However, this requires the business side to integrate it, which may not always be feasible.

Making the dependency declaration range in the component as broad as possible can also help avoid this problem to some extent.

Internationalization Text

Currently, we have 16 languages for internationalization text. Packaging all of them increases the package size considerably. Moreover, in most cases, only one language is used, making other resources ineffective.

For my component, for example: the overall package size is 25kb, with text size being 4kb, accounting for 16%.

We can consider dynamically loading multi-language packs and exporting the contents of each language pack within the component. When using it, the caller can pass in the required language pack externally.

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

Tree Shaking

Some code within the component may not be necessary, and if Tree Shaking is not set up properly, it will cause the business side to package all the code during compilation.

The key is to set up SideEffects correctly in package.json.

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

If the component has multiple styles, and only one will be used each time, you can also consider exporting multiple entry points to facilitate usage based on needs.

Conclusion

If the above suggestions still can't reduce the code to an acceptable range, you'll need to continue using webpack-bundle-analyzer to analyze specific dependencies step by step. Good luck!

If you have any other ideas, feel free to add them :)

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

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