• How To Make Tree Shakeable Libraries
  • Francois Hendriks
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: darkyzhou
  • Proofread by: Usualminds, KimYangOfCat

How to build a tree shakable library

At Theodo, we are committed to building reliable, fast applications for our customers. We have several projects to improve the performance of existing applications. In one project, we managed to reduce the size of the gzip compressed bundles of all pages by a whopping 500KB by tree shaking the internal libraries.

While doing this, we realized that tree shaking isn’t a technique that simply works on and off. There are many factors that can affect the tree shaking of a library.

The goal of this article is to provide detailed guidance for building libraries for tree shaker optimization. The steps are summarized as follows:

  • In a controlled environment, check if our library can be tree-shakable for a known application.
  • Using the ES6 module allows bundler to detect unreferenced filesexportStatements.
  • Optimize with Side Effects so that your library does not contain any side effects.
  • Divide the library’s code logic into several smaller modules while preserving the library’s module tree.
  • Do not lose module trees or ES modules characteristics when transpile libraries.
  • Use the latest version of the tree shaker packaging tool.

What is tree shaking? Why is it important?

Quoting from MDN documents:

Tree shaking is a term commonly used to describe the act of removing dead code from a JavaScript context.

It relies on the import and export statements in ES2015 to detect whether code modules are exported, imported, and used by JavaScript files.

Tree shaking is a method of dead code elimination by detecting which exports are not referenced in the application code. It is performed by packaging tools like Webpack and Rollup, originally implemented by Rollup.

So why is it called tree shaking? We can think of an application’s exports and imports as a tree. Healthy leaves and branches on the tree represent imported items that are referenced. Dead leaves represent unreferenced code that is separate from the rest of the tree. Shake the tree at this point, and all dead leaves are shaken down, meaning unreferenced code is removed.

Why is tree shaking important? It can have a huge impact on your browser applications. If the application packages more code, the browser will spend more time downloading, unpacking, converting, and executing it. Therefore, removing unreferenced code is critical to building the fastest application possible.

There are many articles and resources on the web that explain tree-shaking and unreferenced code removal. Here we will focus on libraries that are referenced in the application. A library is considered tree shakeable when an application that references it successfully removes unreferenced parts of the library.

Before trying to make a library tree shakable, let’s look at how to identify a tree shakable library.

Identify a non-shakable tree optimization library in a controlled environment

This may seem simple at first, but I’ve noticed that many developers consider their libraries tree-shakable simply because they use the ES6 module (more on that later), or because they have tree-shake-friendly configurations. Unfortunately, this doesn’t mean that your library is actually tree-shakable!

This brings us to the question: how can we efficiently check whether a library is tree shakable?

To do this, we need to understand two things:

  • Ultimately, it is the application’s packaging tool that removes unreferenced code from our library, not the library’s own packaging tool (if the library has one). After all, only the application knows which parts of the library are being used.
  • The library’s job is to ensure that it can be tree shaken by the final packaging tool.

To check if our library is tree-shakable, we can put it in a controlled environment and test it with an application that references it:

  1. Create a simple application (call it a “reference application”) with a packaging tool you know how to configure that supports tree shakers (such as Webpack or Rollup).
  2. Set the checked library as a dependency of the application.
  3. Import just one element of the library and check the output of the applied packaging tool.
  4. Check that the output contains only imported elements and their dependencies.

This strategy isolates testing from our existing applications. It allows us to play around with the library without breaking anything. It also allows us to ensure that problems are not caused by the configuration of the application packaging tool.

We will next apply this strategy to a library called User-Library and test it with user-App, an application packaged with Webpack. You can also use other packaging tools you prefer.

The user-library code looks like this:

export const getUserName = () = > "John Doe";

export const getUserPhoneNumber = () = > "* * * * * * * * * * *";
Copy the code

It simply exports two functions in the index.js file, which can be used through the NPM package.

Let’s write a simple user-app:

package.json

{
  "name": "user-app"."version": "1.0.0"."description": ""."main": "index.js"."scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"."build": "webpack"
  },
  "author": ""."license": "ISC"."devDependencies": {
    "webpack": "^ 5.18.0"."webpack-cli": "^ 4.3.1." "
  },
  "dependencies": {
    "user-library": "1.0.0"}}Copy the code

Note that we use user-library as a dependency.

webpack.config.js

const path = require("path");

module.exports = {
  entry: "./src/index.js".output: {
    filename: "main.js".path: path.resolve(__dirname, "dist"),},mode: "development".optimization: {
    usedExports: true.innerGraph: true.sideEffects: true,},devtool: false};Copy the code

To understand the Webpack configuration above, we need to understand how Webpack does tree shaking. The steps for tree shaking are as follows:

  • Identify the application’s entry file (specified by the Webpack configuration file)
  • Create the application’s Module tree by iterating through all the dependencies imported by the entry file and their respective dependencies.
  • For each module in the tree, identify what it isexportThe statement was not imported by another module.
  • Use minification tools such as UglifyJS or Terser to remove unreferenced exports and their associated code.

These steps are performed only in production mode.

The problem with production is minification. It makes it hard to tell if the tree shaker is working because we can’t see the original named function in the packaged code.

To get around this problem, we will have Webpack run in development mode, but still have it recognize which code is not referenced and will be removed in production mode. We set optimization in the configuration as follows:

  optimization: {
    usedExports: true.sideEffects: true.innerGraph: true,}Copy the code

The usedExports attribute enables Webpack to identify which module’s exports are not referenced by other modules. The other two properties are discussed later. For now, we’ll assume that they can improve tree shaker optimization for our application.

Our user-app entry file: SRC /index.js

import { getUserName } from "user-library";

console.log(getUserName());
Copy the code

Once packaged, let’s analyze the output:

/ * * * / "./node_modules/user-library/dist/index.js":
/ *! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./node_modules/user-library/dist/index.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ * * * / ((__unused_webpack_module, exports) = > {

var __webpack_unused_export__;

__webpack_unused_export__ = ({ value: true });

const getUserName = () = > 'John Doe';

const getUserPhoneNumber = () = > '* * * * * * * * * * *';

exports.getUserName = getUserName;
__webpack_unused_export__ = getUserPhoneNumber;
/ * * * / })
Copy the code

Webpack reorganizes all of our code into one file. Take a look at the getUserPhoneNumer export and notice that Webpack marks it as unreferenced. It will be removed in production mode and getUserName will be retained because it is used by our entry file index.js.

Our library is tree shaker optimized! You can write some more imports, repeat the above steps and see the output code. Our goal is to know that Webpack marks code in our library as unreferenced if it is not referenced.

For our simple User-library, things looked good. Let’s make it a little more complicated, and at the same time look at some of the conditions and optimizations for tree shaking.

Use the ES6 module to enable the packaging tool to identify unused exports

This requirement is very common, and many documents explain it in detail, but they seem misleading to me. I often hear developers say that we should use the ES6 module to make our libraries tree-shakable. While this statement is completely true in itself, it contains the false notion that using ES6 modules alone is enough to make tree shaker work well. Alas, if it were that easy, you wouldn’t be reading this article!

However, using the ES6 module is one of the necessary conditions for tree shaking optimization.

There are many packaging formats for JavaScript code: ESM, CJS, UMD, IIFE, etc.

For simplicity, we will consider only two formats: ECMA Script modules (ESM or ES6 modules) and CommonJS modules (CJS), as they are most widely used in application libraries. Most libraries use CJS modules because it allows them to run in Node.js applications (although Node.js now supports ESM as well). In 2015, long after CJS, the ES module came along with ECMAScript 2015 (also known as ES6), which is considered the standard module system for JavaScript.

Examples of CJS format:

const { userAccount } = require("./userAccount");

const getUserAccount = () = > {
  return userAccount;
};

module.exports = { getUserAccount };
Copy the code

Examples of ESM formats:

import { userAccount } from "./userAccount";

export const getUserAccount = () = > {
  return userAccount;
};
Copy the code

There is a big difference between the two formats: ESM imports are static, while CJS imports are dynamic. This means that we can do the following things in CJS, but not in ESM:

if (someCondition) {
  const { userAccount } = require("./userAccount");
}
Copy the code

While this may seem more flexible, it also means that the packaging tool cannot construct a valid tree of modules during compilation or packaging. The someCondition variable has a value that is only known at run time, causing the packaging tool to import userAccount at compile time regardless of someCondition’s value. This also makes it impossible for the packaging tool to check whether the imports are actually being used, so all CJS imports are packaged into bundles.

Let’s modify the user-Library code to reflect this. Also, to make the library feel more realistic, it now has two files:

src/userAccount.js

const userAccount = {
  name: "user account"};module.exports = { userAccount };
Copy the code

src/index.js

const { userAccount } = require("./userAccount");

const getUserName = () = > "John Doe";

const getUserPhoneNumber = () = > "* * * * * * * * * * *";

const getUserAccount = () = > userAccount;

module.exports = {
  getUserName,
  getUserPhoneNumber,
  getUserAccount,
};
Copy the code

We leave the user-app entry file unchanged so that we still don’t use the getUserAccount function and its dependencies.

/ *! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./node_modules/user-library/dist/index.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ * * * / ((module, __unused_webpack_exports, __webpack_require__) = > {

const { userAccount } = __webpack_require__(/ *! ./userAccount */ "./node_modules/user-library/dist/userAccount.js")

const getUserName = () = > 'John Doe'

const getUserPhoneNumber = () = > '* * * * * * * * * * *'

const getUserAccount = () = > userAccount

module.exports = {
  getUserName,
  getUserPhoneNumber,
  getUserAccount
}
/ * * * / }),

/ * * * / "./node_modules/user-library/dist/userAccount.js":
/ *! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./node_modules/user-library/dist/userAccount.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ * * * / ((module) = > {

const userAccount = {
  name: 'user account'
}

module.exports = { userAccount }
/ * * * / })
Copy the code

All three exports appear in the packaged output and are not marked as unreferenced by Webpack. The same is true for the source file userAccount.

Now, let’s look at the results of transforming the above example into an ESM. The modification we made is to change all the syntax of require and exports to the corresponding ESM syntax.

/ *! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./node_modules/user-library/dist/index.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ * * * / ((__unused_webpack_module, __webpack_exports__, __webpack_require__) = > {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "getUserName": () = > /* binding */ getUserName
/* harmony export */ });
/* unused harmony exports getUserAccount, getUserPhoneNumber */
/* harmony import */ var _userAccount_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/ *! ./userAccount.js */ "./node_modules/user-library/dist/userAccount.js");

const getUserName = () = > 'John Doe';

const getUserPhoneNumber = () = > '* * * * * * * * * * *';

const getUserAccount = () = > userAccount;

/ * * * / }),
/ * * * / "./node_modules/user-library/dist/userAccount.js":
/ *! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./node_modules/user-library/dist/userAccount.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ * * * / ((__unused_webpack_module, __webpack_exports__, __webpack_require__) = > {

/* unused harmony export userAccount */
const userAccount = {
  name: 'user account'
};
/ * * * / })
Copy the code

Notice that getUserAccount and getUserPhoneNumber are marked as unreferenced. And the userAccount in the other file is marked. Thanks to the innerGraph optimization, Webpack can attach the getUserAccount import necklace from the index.js file to the userAccount export. This allows Webpack to recursively traverse all of its dependencies, starting with the entry file, to know which exports of each module are not referenced. Since Webpack knows that getUserAccount is not in use, it can do the same check on getUserAccount’s dependencies in the userAccount file.

The ES module allows you to find referenced and unreferenced exports in your application code, which explains why this modular system is so important for tree shaking. It also explains why we should export the dependencies of ESM-compliant builds like LoDash-es. Here the LoDash-ES library is the ESM version of the popular LoDash library.

Having said that, using the ES module alone is still not the best approach for tree shaker optimization. In our example, we found that Webpack recursively checks each file to see if the exported code is referenced. For our example, Webpack can simply ignore the userAccount file because its only export is unreferenced! This brings us to the discussion of side effects in the rest of the article.

This section of the paper is summarized as follows:

  • ESM is one of the conditions of tree shaking optimization, but it is not enough to achieve the desired result.
  • Make sure your library always provides a compilation in ESM format! If users of your library want compiled products in ESM and CJS format, they can do so through the main and Module properties in package.json.
  • If possible, make sure to always use ESM format dependencies, otherwise they cannot be tree shaker optimized.

Use side effects optimization to keep your libraries free of side effects

According to the Webpack documentation, tree shaking can be divided into the following two optimization measures:

  • Reference Exports: Determines which exports of a module are referenced or not referenced.
  • SideEffects: skip modules that do not contain any referenced exports and do not contain sideEffects.

To illustrate what side effects mean, let’s look at the example we used earlier:

import { userAccount } from "./userAccount";

function getUserAccount() {
  return userAccount;
}
Copy the code

If getUserAccount is not in use, can the packaging tool consider the userAccount module to be removed from the packaging output? The answer is no! UserAccount can do all sorts of things that affect other parts of the application. It can inject variables into globally accessible values, such as DOM. It can also be a CSS module that will inject styles into the document. But I think the best example is Polyfill. We usually introduce them as follows:

import "myPolyfill";
Copy the code

Now this module must have side effects, because once it is imported into other modules, it affects the entire application code. The packaging tool will see this module as a candidate for possible deletion, since we are not using any of its exports. However, removing it would break the normal operation of our application.

likeWebpack 和 RollupSuch a packaging tool would therefore, by default, treat all modules in our library as containing side effects.

But in the previous example, we know that our library does not contain any side effects! Therefore, we can tell the packaging tool this. Most packaging tools can read sideEffects properties in package.json files. If this property is not specified, it is set to true by default (indicating that all modules in the package have side effects). We can set it to false (to indicate that none of the modules contain side effects) or we can specify an array that lists the source files with side effects.

We add this property to the package.json of the User-library library:

{
  "name": "user-library"."version": "1.0.0"."description": ""."sideEffects": false."main": "dist/index.js"."scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": ""."license": "ISC"
}
Copy the code

Then re-run Webpack for packaging:

/ *! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./node_modules/user-library/dist/index.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ * * * / (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
    /* harmony export */ getUserName: () = > /* binding */ getUserName,
    /* harmony export */
  });
  /* unused harmony exports getUserAccount, getUserPhoneNumber */

  const getUserName = () = > "John Doe";

  const getUserPhoneNumber = () = > "* * * * * * * * * * *";

  const getUserAccount = () = > userAccount;
  / * * * /
};
Copy the code

We found that the source file userAccount has been removed from the packaged output. We can still see the getUserAccount function referencing userAccount, but this function has been marked by Webpack as unreferenced code and will be removed during code minimization.

The sideEffects option is especially important for libraries that export apis from other internal sources via an index file. Without side effect optimization, the packaging tool must parse all source files that contain exported items.

As Webpack notes: “sideEffects is very efficient because it allows the packaging tool to skip the entire module or source file as well as its entire subtree.”

As for the differences between the two optimization measures introduced above in the intervention packaging process, it can be simply described as follows:

  • sideEffectsHave the packaging tool skip an imported module if nothing imported from that module is used.
  • usedExportsHave the packaging tool remove exported items that are not referenced in a module.

So, the above two measures, one is “skip the file”, one is “mark the exported item as not used”. How is the packaged output under the influence of the former different from the latter?

In most cases, tree shaking of a library, with and without side effects, produces exactly the same output. The final bundle contains the same amount of code. However, in some cases, if the process of analyzing the code associated with an unreferenced export is too complicated, the results of optimization with and without side effects may be different. The following sections of this article will include examples of both cases, and we will see that only the combination of small modules and turn-on side effects optimizations produces the best packaging.

The summary of this section is as follows:

  • Tree shaker consists of two parts: used exports optimization and Side effects optimization.
  • Side Effects optimization is more efficient than detecting unused export items in each module.
  • Don’t introduce any side effects into your library.
  • Be sure to passpackage.jsonIn the filesideEffectsProperty tells the packaging tool that your library does not contain any side effects.

Keep the library’s module tree and divide the code into smaller modulesside effectsFull benefit from optimization

You may have noticed that our user-library from the previous example in this article is not packaged into a separate file, but instead directly exposes the manually added.js source files.

Typically, a library is packaged for the following reasons:

  • Some custom ones are usedimportThe path.
  • They use languages like Sass or TypeScript that need to be converted to languages like CSS or JavaScript.
  • Needs to be satisfied to provide multiple module formats (ESM, CJS, IIFE, etc.).

Popular packaging tools like Webpack, Rollup, Parcel, and ESBuild are designed to provide a bundle that can be transferred to the browser for use. They also tend to create a single file and then reassemble and export all your code to that file, so that only a single.js file needs to be transferred over the network.

From a tree shaker perspective, this leads to a problem: side effect optimization is no longer available because no modules can be skipped.

We will list two cases to illustrate: for tree shaker optimization, split module with side effect optimization is necessary.

A library module imports a dependency in CJS format

To demonstrate this problem, we will use Rollup to package our library. In the meantime, we’ll have one of the library modules import a dependency in CJS format: Lodash.

rollup.config.js

export default {
  input: "src/index.js".output: {
    file: "dist/index.js".format: "esm",}};Copy the code

userAccount.js

import { isNil } from "lodash";

export const checkExistance = (variable) = >! isNil(variable);export const userAccount = {
  name: "user account"};Copy the code

Note that we will now export checkExistance and import it into our library’s index.js file.

Here’s the output file dist/index.js:

import { isNil } from "lodash";

const checkExistance = (variable) = >! isNil(variable);const userAccount = {
  name: "user account"};const getUserAccount = () = > {
  return userAccount;
};

const getUserPhoneNumber = () = > "* * * * * * * * * * *";

const getUserName = () = > "John Doe";

export { checkExistance, getUserName, getUserPhoneNumber, getUserAccount };
Copy the code

All the files are packed into a single file. Note that Lodash is also imported at the top of the file. We are still importing the same functions in user-app as before, which means that the checkExistance function is still not referenced. However, after running Webpack to package user-app, we found that even though the checkExistance function was marked as unreferenced, the entire Lodash library was still imported:

/ * * * / "./node_modules/user-library/dist/index.js":
/ *! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./node_modules/user-library/dist/index.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ * * * / ((__unused_webpack_module, __webpack_exports__, __webpack_require__) = > {

"use strict";
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "getUserName": () = > (/* binding */ getUserName)
/* harmony export */ });
/* unused harmony exports checkExistance, userAccount, getUserPhoneNumber, getUserAccount */
/* harmony import */ var lodash__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/ *! lodash */ "./node_modules/user-library/node_modules/lodash/lodash.js");
/* harmony import */ var lodash__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(lodash__WEBPACK_IMPORTED_MODULE_0__);

const checkExistance = (variable) = >! isNil(variable);const userAccount = {
  name: "user account"};const getUserPhoneNumber = {
  number: '* * * * * * * * * * *'
};

const getUserAccount = () = > {
  return userAccount
};

const getUserName = () = > 'John Doe';

/ * * * / }),

/ * * * / "./node_modules/user-library/node_modules/lodash/lodash.js":
/ *! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./node_modules/user-library/node_modules/lodash/lodash.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ * * * / (function(module.exports, __webpack_require__) {

/* module decorator */ module = __webpack_require__.nmd(module);
var __WEBPACK_AMD_DEFINE_RESULT__;/ * * *@license* Lodash <https://lodash.com/> * Copyright OpenJS Foundation and other contributors <https://openjsf.org/> * Released Under the MIT license < https://lodash.com/license > * -based on the Underscore. Js 1.8.3 < http://underscorejs.org/LICENSE > * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors */
// ...
Copy the code

Webpack cannot do tree shaking for Lodash because its module format is CJS. This was disappointing, since we had obviously organized our library so that Lodash was imported only in the userAccount module, which was not referenced by our application. If the module structure is preserved, Webpack can benefit from side effect optimization, detecting that none of the userAccount exports are referenced and skipping the module so that Lodash is not packaged.

In Rollup, we can use the preserveModules option to preserve the module structure of the library. Other packaging tools have similar options.

export default {
  input: "src/index.js".output: {
    dir: "dist".format: "esm".preserveModules: true,}};Copy the code

Rollup now preserves the original file structure. We run Webpack again and get the following package output:

/ * * * / "./node_modules/user-library/dist/index.js":
/ *! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./node_modules/user-library/dist/index.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ * * * / ((__unused_webpack_module, __webpack_exports__, __webpack_require__) = > {

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "getUserName": () = > (/* binding */ getUserName)
/* harmony export */ });
/* unused harmony export getUserAccount */

const getUserAccount = () = > {
  return userAccount
};

const getUserName = () = > 'John Doe';

/ * * * / })
Copy the code

Lodash is now skipped along with the userAccount module.

The code segment

Preserving the segmented module structure and turning on side effects optimization also helps Webpack code segmentation. It is a key optimization measure for large applications and is widely used in Web applications with multiple pages. Frameworks like Nuxt and Next configure code splitting for individual pages.

To demonstrate the benefits of code splitting, let’s take a look at what happens if our library is packaged into a single file.

user-library/src/userAccount.js

export const userAccount = {
  name: "user account"};Copy the code

user-library/src/userPhoneNumber.js

export const userPhoneNumber = {
  number: "* * * * * * * * * * *"};Copy the code

user-library/src/index.js

import { userAccount } from "./userAccount";
import { userPhoneNumber } from "./userPhoneNumber";

const getUserName = () = > "John Doe";

export { userAccount, getUserName, userPhoneNumber };
Copy the code

To split the code for our application, we will use Webpack’s import syntax.

user-app/src/userService1.js

import { userAccount } from "user-library";

export const logUserAccount = () = > {
  console.log(userAccount);
};
Copy the code

user-app/src/userService2.js

import { userPhoneNumber } from "user-library";

export const logUserPhoneNumber = () = > {
  console.log(userPhoneNumber);
};
Copy the code

user-app/src/index.js

const main = async() = > {const { logUserPhoneNumber } = await import("./userService2");
  const { logUserAccount } = await import("./userService1");

  logUserAccount();
  logUserPhoneNumber();
};

main();
Copy the code

The resulting files from the package now have three: main.js, src_userService1_js.main.js, and src_userService2_js.main.js. Looking closely at the contents of src_userService1_js.main.js, we can see that the entire user-library is packaged:

(self["webpackChunkuser_app"] = self["webpackChunkuser_app"] || []).push([
  ["src_userService1_js"] and {/ * * * / "./node_modules/user-library/dist/index.js":
      / *! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./node_modules/user-library/dist/index.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
      / * * * / (__unused_webpack_module, __webpack_exports__, __webpack_require__) = > {
        "use strict";
        /* harmony export */ __webpack_require__.d(__webpack_exports__, {
          /* harmony export */ userAccount: () = > /* binding */ userAccount,
          /* harmony export */ userPhoneNumber: () = >
            /* binding */ userPhoneNumber,
          /* harmony export */
        });
        /* unused harmony export getUserName */
        const userAccount = {
          name: "user account"};const userPhoneNumber = {
          number: "* * * * * * * * * * *"};const getUserName = () = > "John Doe";

        / * * * /
      },

    / * * * / "./src/userService1.js":
      / *! * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./src/userService1.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
      / * * * / (__unused_webpack_module, __webpack_exports__, __webpack_require__) = > {
        "use strict";
        __webpack_require__.r(__webpack_exports__);
        /* harmony export */ __webpack_require__.d(__webpack_exports__, {
          /* harmony export */ logUserAccount: () = >
            /* binding */ logUserAccount,
          /* harmony export */
        });
        /* harmony import */ var user_library__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
          / *! user-library */ "./node_modules/user-library/dist/index.js"
        );

        const logUserAccount = () = > {
          console.log(user_library__WEBPACK_IMPORTED_MODULE_0__.userAccount);
        };

        / * * * /}},]);Copy the code

While getUserName is marked as unreferenced, userAccount is not marked, even though userService2 only uses userPhoneNumber. Why is that? (The above code is for userService1, not userService2)

We need to remember that used exports optimizations check at the module level when checking whether the exported item is referenced. Only at this level can Webpack remove unused code. For our library module, both userAccount and userPhoneNumber are actually used. In this case, Webpack can’t tell the difference between userService1 and userService2 on an import, as shown below (you can see that userAccount and userPhoneNumber are both highlighted in green) :

This means thatWebpackUnder the condition of reference export optimization only, tree shaking optimization cannot be independently carried out for each chunk.

Now, let’s keep the module structure when packaging the library so that side effect optimization can work:

Webpack still outputs three files, but this time src_userService2_js.main.js just contains the code in userPhoneNumber:

(self["webpackChunkuser_app"] = self["webpackChunkuser_app"] || []).push([
  ["src_userService2_js"] and {/ * * * / "./node_modules/user-library/dist/userPhoneNumber.js":
      / *! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./node_modules/user-library/dist/userPhoneNumber.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
      / * * * / (__unused_webpack_module, __webpack_exports__, __webpack_require__) = > {
        "use strict";
        /* harmony export */ __webpack_require__.d(__webpack_exports__, {
          /* harmony export */ userPhoneNumber: () = >
            /* binding */ userPhoneNumber,
          /* harmony export */
        });
        const userPhoneNumber = {
          number: "* * * * * * * * * * *"};/ * * * /
      },

    / * * * / "./src/userService2.js":
      / *! * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./src/userService2.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
      / * * * / (__unused_webpack_module, __webpack_exports__, __webpack_require__) = > {
        "use strict";
        __webpack_require__.r(__webpack_exports__);
        /* harmony export */ __webpack_require__.d(__webpack_exports__, {
          /* harmony export */ logUserPhoneNumber: () = >
            /* binding */ logUserPhoneNumber,
          /* harmony export */
        });
        /* harmony import */ var user_library__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
          / *! user-library */ "./node_modules/user-library/dist/userPhoneNumber.js"
        );

        const logUserPhoneNumber = () = > {
          console.log(
            user_library__WEBPACK_IMPORTED_MODULE_0__.userPhoneNumber
          );
        };

        / * * * /}},]);Copy the code

Src_userservice1_js.main.js is similar to the above, containing only the userAccount module in our library.

As you can see in the figure above, userAccount and userPhoneNumber are still recognized as exported items that are referenced, after all, they are both referenced at least once in the application. This time, however, the side effect optimization allowed Webpack to skip the userAccount module because it was never imported by userService2. The same thing happens between userPhoneNumber and userService1.

We now understand that it is important to preserve the original module structure in the library. However, if the original module structure contains only one module, such as the index.js file, and contains all the code, then it is useless to keep the module structure. To build a library that is well-suited for tree shaking, we must divide the library code into several smaller modules, each of which is responsible for a portion of our code logic.

To use the tree metaphor, we need to treat each leaf on the tree as a module. Smaller, weaker leaves are more likely to fall when the tree is shaken! If there were fewer and stronger leaves on the tree, shaking the tree might have a different result.

The summary of this section is as follows:

  • To take full advantage of side effect optimization, we should preserve the library’s modular structure.
  • The library should be divided into smaller modules, each exporting only a small part of the entire library’s code logic.
  • Only with the help of side effects optimization can we perform tree shaking optimization on referenced libraries in the application.

Do not lose the characteristics of the module tree and ES modules when translating library code

Packaging tools are not the only thing that can affect how your libraries are tree shaken. ** Translation tools can also have a negative impact on tree shaker because they remove ES modules or lose module trees.

One of the purposes of the translation tool is to make your code work in browsers that do not support the ES module. However, it is important to remember that our libraries are not always loaded directly by the browser, but are imported by the application. Therefore, we cannot translate our library code for a particular browser for two reasons:

  • When we write library code, we don’t know which browsers our library will be used in, only the application that uses the library does.
  • Translating our library code makes them untree shakable.

If your library does need to be translated for some reason, you need to ensure that the translation tool does not remove the syntax of the ES module and does not remove the original module structure, for the reasons described above.

As far as I know, there are two translation tools that remove both of these.

Babel

Babel can use Babel Preset -env to make your code compatible with specified target browsers. This plugin removes ES modules from the library code by default. To avoid this, we need to set the modules option to false:

module.exports = {
  env: {
    esm: {
      presets: [["@babel/preset-env",
          {
            modules: false,},],],},},};Copy the code

TypeScript

When compiling your code, TypeScript converts your modules according to the target and module options in the tsconfig.json file.

To avoid this, set the target and Module options to at least ES2015 or ES6.

The summary of this section is as follows:

  • Make sure your translator and compiler don’t remove ES module syntax from library code.
  • If you need to check for any of these problems, you can check for ES module import syntax in the translation/compilation artifacts of the library.

Use the latest version of the tree shaker packaging tool

JavaScript tree shakers have become popular thanks to Rollup. Webpack has supported tree shaking since V2. Packaging tools are getting better and better at tree shaking.

Remember the innerGraph optimization we talked about earlier? It enables Webpack to associate exports of a module with imports of other modules. This optimization was introduced in Webpack 5. Although we’ve been using Webpack 5 throughout this article, it’s important to recognize that this optimization has changed the industry. It allows Webpack to recursively find unused exported items!

To show how it works, consider the index.js file in user-library:

import { userAccount } from "./userAccount";

const getUserAccount = () = > {
  return userAccount;
};

const getUserName = () = > "John Doe";

export { getUserName, getUserAccount };
Copy the code

Our user-app just uses getUserName:

import { getUserName } from "user-library";

console.log(getUserName());
Copy the code

Now, let’s compare how the output is packaged with and without innerGraph optimization. Note that both usedExports and sideEffects optimization are turned on.

Without innerGraph optimization (such as using Webpack 4) :

/ *! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./node_modules/user-library/dist/index.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ *! exports provided: userAccount, userPhoneNumber, getUserName, getUserAccount */
/ *! exports used: getUserName */
/ * * * / (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a".function() { return getUserName; });
/* unused harmony export getUserAccount */
/* harmony import */ var _userAccount_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/ *! ./userAccount.js */ "./node_modules/user-library/dist/userAccount.js");

const getUserAccount = () = > {
  return _userAccount_js__WEBPACK_IMPORTED_MODULE_0__[/* userAccount */ "a"]};const getUserName = () = > 'John Doe';

/ * * * / }),

/ * * * / "./node_modules/user-library/dist/userAccount.js":
/ *! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./node_modules/user-library/dist/userAccount.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ *! exports provided: userAccount */
/ *! exports used: userAccount */
/ * * * / (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a".function() { return userAccount; });
const userAccount = {
  name: 'user account'
};

/ * * * / }),
Copy the code

With innerGraph optimization (e.g. using Webpack 5) :

/ * * * / "./node_modules/user-library/dist/index.js":
/ *! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./node_modules/user-library/dist/index.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ * * * / ((__unused_webpack_module, __webpack_exports__, __webpack_require__) = > {

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "getUserName": () = > (/* binding */ getUserName)
/* harmony export */ });
/* unused harmony export getUserAccount */

const getUserAccount = () = > {
  return userAccount
};

const getUserName = () = > 'John Doe';

/ * * * / })
Copy the code

Webpack 5 can completely remove the userAccount module, but Webpack 4 can’t, even though getUserAccount is marked as unreferenced. This is because inngergraph-optimized algorithms enable Webpack 5 to link an unreferenced export item in a module to its corresponding import item. In our example, the userAccount module is only used by the getUserAccount function, so it can be skipped directly.

Webpack 4 does not have this optimization. Developers should therefore be vigilant when using this version of Webpack and limit the number of exported items in a single source file. If a source file contains more than one export, Webpack contains all the corresponding imports, even if some are unnecessary for the exports that are actually needed.

In general, we should make sure to always use the latest version of the packaging tool so that we can benefit from the latest tree shaker optimizations.

conclusion

Tree shaking of a library doesn’t work just by adding a line to a configuration file. The quality of its optimization depends on a number of factors, only a few of which are listed in this article. However, no matter what the problem is, the following two things that have been done in this article are important for anyone who wants to tree-shake libraries:

  • To determine the extent of tree shaking in our library, we need to test it in a controlled environment using packaging tools we know about.
  • To check the configuration of our library for problems, we need to check not only the various configuration files, but also the packaged output.We’ve been doing that in this articleuser-libraryuser-appExamples of doing this kind of thing.

I really hope this article helps you make your ongoing task of building the most optimized library possible!

The extensions to read

  • Tree-shaking versus dead code elimination
  • Webpack 5 release
  • Tree shaking in JavaScript

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.