Polyfills And Helpers - How To Save Your Bundle Size
Tags: webdev, javascript, beginners, programming
Published: 2022-06-16 02:57:08

> Yoo, I'm nazha（那吒）

<p align="center">
  <samp>
    <a href="https://twitter.com/xiaokedada">twitter</a> .
    <a href="https://www.nazha.co/">blog</a> . 
    <a href="https://nazha-blog.notion.site/nazha-blog/625e1e53ccd2454980464a3d8e5dffd1">Notion</a> .
        <a href="https://github.com/maoxiaoke">Github</a> .
    <a href="https://www.zhihu.com/people/mao-xiao-ke">知乎</a>
  </samp>
</p>

As web programmers, we'd like to use most recent features. On the other hand, how to make our modern code work on older browsers that don't understand those feature?

There are two tools for that:

+ Polyfills
+ Transpilers

## Polyfills vs transpilers

### Polyfills

A polyfill is code that updates/adds new funtions or properties.

For example, `string.prototype.matchAll` was [first introduced in ES2020](https://www.freecodecamp.org/news/javascript-new-features-es2020/) and can not work on Chrome under 67 according to [can-i-use](https://caniuse.com/?search=string.matchAll). If your users are using Chrome under 67, there's no `string.prototyp.matchAll`, so such code will fail.

For this case, "polyfill" will "fills in" the gap and adds missing implmentation by modifying the prototype chain of `String`, without replacing the source code.

Write code like this:

```javascript
const matchString = 'abbbbc'.matchAll(/a/, 'd');
```

Then run it through a transpiler such as [Babel](https://babeljs.io/docs/en/):

```javascript
require("core-js/modules/es.string.match-all.js");

var matchString = 'abbbbc'.matchAll(/a/, 'd');
```

### Transpilers

A transpiler is a tool that translates your source code to another code. It parse modern syntaxs and rewrite it using older syntaxs to make them work in older browsers.

[spread operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax)，[async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function), [Class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) are some good examples of this case.

Do same thing with this code:

```javascript
const extendsObj = { ...{ a: 'a' } };
```

You will get:

```javascript
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2["default"])(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }

var extendsObj = _objectSpread({}, {
  a: 'a'
});
```

Notice that there are some functions imported from `@babel/runtime/helpers`, which helps to transpil code. They're what I refer to as "**Helpers**".

## Downsides

We're glad to see that our code can run on older browsers (like IE). On the other hand, polyfills and helpers may also make our application a big boat.

Take a React application (within thousands of modules in node_modules) for example, we divide it to two parts to discuss:

+ NPM React Component in node_modules directory
+ The Application itself

## Helpers should never be inlined

When transpiling, helper functions will be added to every file that requires it. This duplication is sometimes unnecessary.

In babel, this is where the [@babel/plugin-transform-runtime](https://babel.dev/docs/en/babel-plugin-transform-runtime) plugin comes in. And you should use it like this way:

```json
"presets": [
  ["@babel/preset-env", {
    // Disable useBuiltIns by default
    "useBuiltIns": false,
  }]
],
"plugins": [
  [
    "@babel/plugin-transform-runtime",
    {
      "absoluteRuntime": false,
      "helpers": true, // Helpers will not be inlined
    }
  ]
]
```

If you are using [swc](https://github.com/swc-project/swc), enable non-inlined helpers in your `.swcrc`:

```json
"jsc": {
  "externalHelpers": true
}
```

## Helpers version resolution

If a module is resolved to the same path, bundlers like [webpack](https://webpack.js.org/) and [rollup](https://rollupjs.org/guide/en/) will bundle it once.

Let's say you have [@swc/helpers@0.2.0](https://babel.dev/docs/en/babel-runtime) (the helpers be used by swc) installed in NPM package and `@swc/helpers@0.3.0` in your application root.

They are considered as different modules for different resolved paths. Sometimes, this duplication is unnecessary.

You can tackle with this by adding [resolutions](https://classic.yarnpkg.com/lang/en/docs/selective-version-resolutions/) (if you're using yarn), or `pnpm.overrides` (if you're using pnpm) field in your `package.json` file of application.

```json
"name": "react-application",
"pnpm": {
  "overrides": {
    "@swc/helpers": "^0.3.0"
  }
}
```

There also exists a more elegant way: creating modules aliasas for helpers. For example:

```js
// webpack.config.js
module.exports = {
  //...
  resolve: {
    alias: {
      "@swc/helpers": path.resolve(__dirname, 'node_modules'),
    }
  }
}
```

## Should my NPM package polyfilled

I prefer not to do this. A react application will not know whether all the NPM packages it depends are fully polyfilled or not. Even though all NPM packages are fully polyfilled, react applications must be fully polyfilled to prevent breakings.

There are two ways to make it more flexible:

+ Pre-bundling (I will cover this in another post)
+ Polyfilled all features based on environment

### Polyfilled all features

If you are using babel, it's convenient to add [@babel/preset-env](https://babeljs.io/docs/en/babel-preset-env#usebuiltins). And configure your `.babelrc` file like this:

```json
"presets": [
  ["@babel/preset-env", {
    "target": "79",
    "useBuiltIns": "entry",
    "corejs": "3.8"
  }]
]
```

For swc users, configure your `.swrrc` file like this:

```js
{
  "env": {
    "targets": {
      "chrome": "79"
    },
    "mode": "entry",
    "coreJs": "3.8"
  }
}
```

But it is not really perfect. There exists a case that a "polyfill" is not used but polyfilled.

To reduce the bundle size, you can only load these polyfills for browsers that require them, so that the majority of the web traffic globally will not download these polyfills.

## Further reading

- [Polyfills: everything you ever wanted to know, or maybe a bit less](https://medium.com/hackernoon/polyfills-everything-you-ever-wanted-to-know-or-maybe-a-bit-less-7c8de164e423)
- [babel-polyfills](https://github.com/babel/babel-polyfills)
- [tsdx issue](https://github.com/jaredpalmer/tsdx/issues/968#issuecomment-791817207)