Keep Your Bundle Size Under Control with Import-Cost VSCode Extension

Written by yairhaimo | Published 2017/08/14
Tech Story Tags: javascript | typescript | webpack | visual-studio-code | performance

TLDRvia the TL;DR App

Keep Your Bundle Size Under Control

(with Import-Cost VSCode Extension)

Our industry has been buzzing a lot in the last year about the bloat of modern web applications and websites. Some have even resorted to public shaming of websites in order to raise awareness of this issue.

While a desktop computer with a high speed internet connection in a first world country can chew up almost any website easily, this statement falls short once you change one of its variables. Mobile devices and unreliable internet connections are much more sensitive to the size of your application, whether it’s the network throughput, the time it takes to parse the code, the CPU labor, the memory footprint and the battery.

We all know this fact but few of us really keep it in mind when we develop our awesome, state-of-the-art applications on our state-of-the-art machines running the state-of-the-art browser.

body[data-twttr-rendered="true"] {background-color: transparent;}.twitter-tweet {margin: auto !important;}

@yairhaimo @code @WixEng @shahata This is really awesome! Even if you don't use Code and/or this extension, the good practice is only import what you need!

— @formvalidation

function notifyResize(height) {height = height ? height : document.documentElement.offsetHeight; var resized = false; if (window.donkey && donkey.resize) {donkey.resize(height); resized = true;}if (parent && parent._resizeIframe) {var obj = {iframe: window.frameElement, height: height}; parent._resizeIframe(obj); resized = true;}if (window.location && window.location.hash === "#amp=1" && window.parent && window.parent.postMessage) {window.parent.postMessage({sentinel: "amp", type: "embed-size", height: height}, "*");}if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.resize) {window.webkit.messageHandlers.resize.postMessage(height); resized = true;}return resized;}twttr.events.bind('rendered', function (event) {notifyResize();}); twttr.events.bind('resize', function (event) {notifyResize();});if (parent && parent._resizeIframe) {var maxWidth = parseInt(window.frameElement.getAttribute("width")); if ( 500 < maxWidth) {window.frameElement.setAttribute("width", "500");}}

Understanding the Cost Early

Since this is a hot topic many people already tried to tackle it with awesome tools such as webpack-bundle-analyzer, cost-of-modules, command line webpack warnings and techniques like code splitting and lazy loading.

What we felt is that, while those are great tools, they are easily overlooked if you are not conscious enough. We wanted to create a tool that will smack you right in the face and let you know immediately if you imported a hefty package that will hurt your users.

body[data-twttr-rendered="true"] {background-color: transparent;}.twitter-tweet {margin: auto !important;}

@fistynuts @yairhaimo @tkadlec @code @WixEng I think it's helpful b/c I typically don't analyze the network tab until later, not during implementation. Understanding the cost early ftw.

— @justinbmeyer

function notifyResize(height) {height = height ? height : document.documentElement.offsetHeight; var resized = false; if (window.donkey && donkey.resize) {donkey.resize(height); resized = true;}if (parent && parent._resizeIframe) {var obj = {iframe: window.frameElement, height: height}; parent._resizeIframe(obj); resized = true;}if (window.location && window.location.hash === "#amp=1" && window.parent && window.parent.postMessage) {window.parent.postMessage({sentinel: "amp", type: "embed-size", height: height}, "*");}if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.resize) {window.webkit.messageHandlers.resize.postMessage(height); resized = true;}return resized;}twttr.events.bind('rendered', function (event) {notifyResize();}); twttr.events.bind('resize', function (event) {notifyResize();});if (parent && parent._resizeIframe) {var maxWidth = parseInt(window.frameElement.getAttribute("width")); if ( 500 < maxWidth) {window.frameElement.setAttribute("width", "500");}}

Enter Import-Cost

Import-Cost is a Visual Studio Code extension that shows you the size of an imported 3rd party library the moment you import it (or several moments thereafter).

This extension is not intended as a bundle analysis tool — there are better tools for that, some of them are stated above. We believe this extension will help you find obvious pain points and prevent shipping massive bundles to your customers

Instant feedback

In the past couple of days we received numerous questions about the details of this extension and how it works. We hope to answer some of these questions in the following segment.

body[data-twttr-rendered="true"] {background-color: transparent;}.twitter-tweet {margin: auto !important;}

Really dig this! @code extension showing how much code you're pulling in when you import a package from @WixEng https://t.co/Af9yMu3xlH

— @tkadlec

function notifyResize(height) {height = height ? height : document.documentElement.offsetHeight; var resized = false; if (window.donkey && donkey.resize) {donkey.resize(height); resized = true;}if (parent && parent._resizeIframe) {var obj = {iframe: window.frameElement, height: height}; parent._resizeIframe(obj); resized = true;}if (window.location && window.location.hash === "#amp=1" && window.parent && window.parent.postMessage) {window.parent.postMessage({sentinel: "amp", type: "embed-size", height: height}, "*");}if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.resize) {window.webkit.messageHandlers.resize.postMessage(height); resized = true;}return resized;}twttr.events.bind('rendered', function (event) {notifyResize();}); twttr.events.bind('resize', function (event) {notifyResize();});if (parent && parent._resizeIframe) {var maxWidth = parseInt(window.frameElement.getAttribute("width")); if ( 500 < maxWidth) {window.frameElement.setAttribute("width", "500");}}

How it Works

AST

Import-Cost listens to changes in the text of the active editor window. Whenever it detects a change (debounced, of course) it will analyze the code of the current window using the Typescript and Babylon AST parsers and compile a list of valid import or require candidates. A valid candidate is a 3rd party, locally installed library. For example, the following code, will compile a list of three candidates (react, react-dom and lodash/uniqueId) and ignore the rest.

Valid Candidates

We intentionally decided to only calculate 3rd party libraries since they have a clear boundary and they are at the root of all the bloated bundles that we ship to our users.

Bundle

Next, the extension takes the entire line of code of the import/require candidate (more on this later) and puts it in a temporary file. The extension then runs webpack configured with the temporary file as the entrypoint as well as the babili-webpack minifier plugin. Webpack will then pull all the necessary dependencies of the library and bundle them all together.

The entire line of code of the import/require candidate is taken into consideration in order to leverage webpack’s tree shaking mechanism. The final size of the bundle is affected by what you actually import and not the size of the whole library itself.

Tree-shake everything except “func”

func1 is a doozy!

Another reason to look at the entire line of code is to support submodule imports:

Submodule Imports

In addition to placing the entire import line into the entrypoint file, we also print the imported object using console.log in order to stop webpack from tree shaking our own import.

The extension runs multiple webpack instances using the worker-farm library which runs parallel child processes in order to parallelize the calculations of the different libraries.

Cache

Once the bundlings are done we save the results in a file-based cache. The package sizes are saved to the cache while taking into account the version of the libraries. The reason behind that is that the same piece of code might have a different weight depending on the version of the imported library.

lodash version 3.10.1

lodash version 4.17.4

Decorations

All that is left to do is to decorate the editor in the right place. Since the package size calculation is an asynchronous task that might take a while we run into a problem that the user might have switched to a different tab in the meantime and the returned results are not relevant. The extension knows to decorate the correct code based on the active window that the sizes are relevant for. If and when the user comes back to a piece of code that was calculated in an earlier time the extension will read from an in-memory cache and add the correct decorations in the correct places.

Thats it!

body[data-twttr-rendered="true"] {background-color: transparent;}.twitter-tweet {margin: auto !important;}

@yairhaimo @code @WixEng @shahata This is interesting! Does the calculated cost use uncompressed or gzipped numbers? (noted you're babili-fying imports either way)

— @addyosmani

function notifyResize(height) {height = height ? height : document.documentElement.offsetHeight; var resized = false; if (window.donkey && donkey.resize) {donkey.resize(height); resized = true;}if (parent && parent._resizeIframe) {var obj = {iframe: window.frameElement, height: height}; parent._resizeIframe(obj); resized = true;}if (window.location && window.location.hash === "#amp=1" && window.parent && window.parent.postMessage) {window.parent.postMessage({sentinel: "amp", type: "embed-size", height: height}, "*");}if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.resize) {window.webkit.messageHandlers.resize.postMessage(height); resized = true;}return resized;}twttr.events.bind('rendered', function (event) {notifyResize();}); twttr.events.bind('resize', function (event) {notifyResize();});if (parent && parent._resizeIframe) {var maxWidth = parseInt(window.frameElement.getAttribute("width")); if ( 500 < maxWidth) {window.frameElement.setAttribute("width", "500");}}

FAQ

Does the plugin support tree shaking?

Yes, we utilize webpack’s tree shaking mechanism.

Do you display the minified size? Gzipped size?

You can configure the extension to display the minified size, the gzipped size or both. The default is both.

What about common dependencies between packages?

Common dependencies (both package A and package B use package C) will be counted twice, both for package A and for B. As I stated earlier, this extension is not meant to be treated as an analysis tool, rather it’s to be treated as a “scare-you-straight” tool.

Is there an Import-Cost extension for other IDEs?

Our original plan was to make everyone migrate to VSCode, but that didn’t pan out (yet!). Instead, we split the extension into two subrepos:

The split was done in order to ease the consumption of the logic from IDEs other than VSCode. We would love to see people develop an Import-Cost extension for their favorite IDE.

We hope you enjoy this extension and hope you open issues or even better send some pull requests. Also, don’t forget this project is fueled by stars 🌟.

Yair Haimovitch and Shahar Talmi


Published by HackerNoon on 2017/08/14