Optimizing bundle size with TSLib helper functions
Aleksei Berezkin
Posted on January 27, 2021
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};
src/cd.ts
const c = {x: 1};
const d = {...c};
dist/ab.js
var __assign = /* Somewhat long function */
var a = { x: 1 };
var b = __assign({}, a);
dist/cd.js
var __assign = /* Damn! Literally the same function π₯ */
var c = { x: 1 };
var d = __assign({}, c);
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 = /* ... */
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 */
src/ab.ts
import { assign } from './helpers';
const __assign = assign;
const a = {x: 1};
const b = {...a};
dist/ab.js
var helpers_1 = require("./helpers");
var __assign = helpers_1.assign;
var a = { x: 1 };
var b = __assign({}, a);
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?
Posted on January 27, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024
October 16, 2024