DEV Community

Aleksei Berezkin
Aleksei Berezkin

Posted on

Optimizing bundle size with TSLib helper functions

Who may find it useful

The post describes techniques useful for libs creators. For client apps importHelpers (see below) is just fine.

An issue with helper functions

When transpiling a modern TS code into ES5, the compiler emits helper functions like __assign, __extend, __importStar etc. to emulate ES6+ features in ES5.

Good news 🙂

A helper function defined once may be used several times in a file.

Bad news 😥

If multiple files need the same function, it's inserted into each of them. Let's see:

src/ab.ts

const a = {x: 1};
const b = {...a};
Enter fullscreen mode Exit fullscreen mode

src/cd.ts

const c = {x: 1};
const d = {...c};
Enter fullscreen mode Exit fullscreen mode

dist/ab.js

var __assign = /* Somewhat long function */
var a = { x: 1 };
var b = __assign({}, a);
Enter fullscreen mode Exit fullscreen mode

dist/cd.js

var __assign = /* Damn! Literally the same function 😥 */
var c = { x: 1 };
var d = __assign({}, c);
Enter fullscreen mode Exit fullscreen mode

So what to do with helper functions? First, official ways

1. Import helpers

With this option you need declaring a (peer-)dependency on tslib. Now, instead of emitting function bodies, TS will just import them from tslib.

The lib is quite small yet it's tree-shakeable which means good bundler won't use any unneeded functions. But if you are authoring the lib you can't be sure: perhaps the user doesn't have any bundler at all 😕 Yet, for small libs having no deps looks more attractive.

2. No emit helpers

With this option TS emits neither helpers bodies nor their imports — it implies all needed helpers are available globally, i.e. you have somewhere like

var globalObj = typeof window === 'object' ? window : global;
globalObj.__assign = /* ... */
Enter fullscreen mode Exit fullscreen mode

That's not fun 😥 What good is polluting the global scope with your lib internals?

Defining helpers in a module — not official way

I find this way the best for libs; however, because it's not official, I should warn you it's a bit fragile — it may stop working in some TS version. Hope that times there will be a better official way 😉

The trick works with noEmitHelpers but you don't put anything into the global object — you just create module-scoped variable with the name TS expects, and assign it the function imported from wherever you want.

src/helpers.ts

export const assign = /* implementation */
Enter fullscreen mode Exit fullscreen mode

src/ab.ts

import { assign } from './helpers';
const __assign = assign;
const a = {x: 1};
const b = {...a};
Enter fullscreen mode Exit fullscreen mode

dist/ab.js

var helpers_1 = require("./helpers");
var __assign = helpers_1.assign;
var a = { x: 1 };
var b = __assign({}, a);
Enter fullscreen mode Exit fullscreen mode

Voila!

  • Helper function is defined exactly once
  • Your lib doesn't have any deps and doesn't imply bundler
  • You don't pollute the global scope

Notes

Renaming imported object

It would be nice to have just import { __assign } from './helpers' or import __assign from './helpers' but, unfortunately, this doesn't work for CommonJS — objects are imported under generated names. So, const __x = x is unavoidable.

When it may break?

The trick works because TS doesn't change the name of const __assign. Is it reliable? I don't know. Do you?

Where to take helper functions implementations?

You may just copy them from tslib. It's 0BSD-licensed which doesn't even require attribution; but having the link somewhere in your comments is at least polite 🙂

Example

I applied this approach in my Fluent Streams lib, and that's not the only bundle-size-trick I used here. The next post is coming 😄


Thanks for reading this. Do you know some tricks to easily reduce bundle size?

Discussion (0)