We, as Javascript developers need to use libraries in our day to day, it simplifies our work a lot. Nowadays we mostly do it in this way:
import * as lib from 'amazing-lib'
import { func1 } from 'amazing-lib'
If we quickly analyze it, in the first statement we are importing everything and putting it on a variable called lib
, on the second, we are importing everything again and destructuring only what we need; what about all the other code that I'm not using?
Will not all the unused library's code end up on the final bundle making my application unnecessary heavier?
Today you will learn how to improve your bundle size just by changing the way you import. After this, you are going to be able to detect a simple optimization opportunity for your bundle!
TL; DR
Verify if the library has support for ES6 and you can import freely, you will get always the best result ๐โโ๏ธ. If it doesn't โ ๏ธ, you need to import using cherry-picking.
Can we import as we want without consequences?
When we compile our front-end apps, there is a process that Webpack applies called Tree Shaking. Basically, is code elimination, the code that is not being used by anyone. This process prevents having dead code in our final bundle, making it lighter and the application is going to load faster for our users!
Let's analyse this:
import * as lib from 'amazing-lib'
import { foo } from 'amazing-lib'
In both cases all the library content is being imported, the first place is the easiest to spot, all the library's content is being assigned to the variable lib
, in the second case we are just applying destructuring to the library's content to get what we need. Thanks to Tree Shaking all the unused code doesn't end up on our bundles.
So, thanks to Tree Shaking I'm excused and I can import however I want and all the unused code imported from the library will be removed automagically?
Is not always the case
There is a scenario when Tree Shaking is not going to be able to detect what is dead code having as consequence to remove nothing.
Scenarios
ES6
ECMAScript 2015(aka ES6) Module Syntax; it sounds complex, but it's something really popular nowadays. It's just a syntax to import a JS module, it looks like this:
import { foo } from 'awesome-lib'
import { bar } from '../utils'
import * as lib from '../utils'
export const justAConst = 'foobar'
When you are using a library that has ES6 Module Syntax compatibility, you don't need to worry, import as will, Tree Shaking have you covered ๐. In fact, is the only module syntax that Tree Shaking supports, let's take a look at the documentation:
Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. It relies on the static structure of ES2015 module syntax, i.e.
import
andexport
...The webpack 2 release came with built-in support for ES2015 modules (alias harmony modules) as well as unused module export detection...
If you are completely new about ๐ JS Modules
No ES6 module syntax
A library can be delivered (packaged) with other module systems different than ES6, even though if its source code uses ES6 module syntax, a compilation process could be implemented to only support CommonJS for example. The projects written using pure JS (with no transpilation process (Babel, TypeScript)) that uses the CommonJs module system is another example.
So, no ES6 module syntax present = no Tree Shaking. The only way to have a healthy bundle when dealing with libraries with no ES6 is importing using a technique called cherry-picking, you need to specify the absolute path to the file that contains the info needed.
import { small } from 'common-js-lib/small';
Downsides of cherry-picking
- You need to know the path to the module needed. (Your IDE could help in this quest)
-
You need to specify each one of the imports that you need, ex:
import has from 'lodash/has'; import capitalize from 'lodash/capitalize'; import lastIndexOf from 'lodash/lastIndexOf';
As a maintainer, you may want to have a nice and easy-to-use scaffolding design to detect with ease something in your lib. This needs to be designed, implemented, and maintained.
You may forget to do it, making your bundle unnecessary heavier. ESLint could help you to import correctly.
Performance Test
Having the theory learned I decided to prove all this. What I did was create some libraries with different module support, create several front-end apps on React and Angular1 to test if Tree Shaking really does its job.
The libraries created were simple, they export two variables small
and big
. small
contains one dog ๐ (small = '๐'
), but big
has 1646400 dogs (big = '๐๐๐๐๐๐๐๐๐...'
). This is going to make big
to be 6.3 megabytes of weight.
Only small
is going to be use at all time, so if big
sneaks into the final bundle we are going to notice it on sight!.
Healthy Bundle
This how a healthy bundle looks like:
Smelly Bundle ๐คข
The smelly one! You can notice a big white box that represents big
and represents 96.7% of the application size:
The Results
The results were as expected, if your library has to offer ES6 module syntax, Tree Shaking will do its job. If not, cherry-picking was the only way to get a healthy bundle.
Here is the repo if you are curious dianjuar/how-to-import. All this was created in a Monorepo using Nx, the library's npm publishing was mocked using yalc. The bundle analysis was made using source-map-explorer.
Also, I wanted to make this test with popular libraries, so this is what I got, importing as import { whatINeed } from 'popular-lib'
Library | Healthy Bundle |
---|---|
lodash | โ |
moment | โ |
rxjs | โ |
lodash-es | โ |
date-fns | โ |
@angular/core | โ |
@angular/material | โ |
react | โ |
react-dom | โ |
@material-ui/core | โ |
@fortawesome/react-fontawesome | โ |
Useful Tools
Along with this experiment, I was using the VsCode extension Import Cost and was precise along with the result. With the extension, you will be able to see right away how much an import will cost to the bundle. It will not tell you when you are getting a healthy or smelly bundle, but you can tell when an import cost is sus.
Gif extracted from the README of Cost Import
Conclusion
Tree Shaking have you covered! You can import however you want and you will be getting a healthy bundle if and only if the library has support for ES6 Module Syntax (import
and export
).
You can make your bundle unnecessary heavy if you don't cherry-pick the imports on libraries without ES6 module support, like lodash.
Import Cost can help you to spot an import that needs some refinement.
-
The experiment is agnostic to the Framework or Library used, if webpack is involved in the bundling it would take the same effect.ย โฉ
Top comments (2)
what about lazy import?
Definetly could help to load your app faster but if you don't apply this advices to your modules lazy loaded they could being heavier than they should be.