BattlefyBlogHistoryOpen menu
Close menuHistory

Demystifying Webpack

Ronald ChenNovember 29th 2021

Webapp bundlers like Webpack are often categorized as dark magic, as few have spent the time to understand it. The fact that Webpack has the ability to transform every aspect of a webapp, I would argue that if one doesn’t understand Webpack, then they don’t know the HTML/CSS/JavaScript they wrote is correct or not.

But doesn’t deployment of the build to a staging environment and testing it there prove it is correct? Most of the time, sure, but what happens when the Webpack build goes wrong? Do you even know what symptoms point to a failure with Webpack? If not, you’ll end up with hacks instead of fixing the root cause.

What is Webpack even doing? What problem does it solve?

Road to Webapps

Before Webapps, everything was just HTML pages directly linked to each other and served by an HTTP server. This worked well enough for small static sites, but it didn’t scale.

Then we moved to dynamically generated HTML, which scaled a bit better as we could use a backend language and inject data as we generated the HTML. But this had its limits as well. We no longer could use a CDN and generating HTML was error-prone.

Webapps, in particular single-page applications, were invented to make things easier. We can now build real applications in the browser and have all the assets hosted on a CDN again!

Webapp problems

Early Webapps were pretty straightforward. One wrote JavaScript and loaded it into HTML with a <script> tag. If one had libraries like lodash, it would just be loaded before the main script and accessible via the window global.

<body>  
  <div id="root"></div>  
  <!-- lodash will assign itself to window._ -->  
  <script type="text/javascript" src="js/lodash.js"></script>  
    
  <!-- app uses window._ -->  
  <script type="text/javascript" src="js/app.js"></script>  
</body>

If libraries have dependencies of their own, then one would need to carefully order the scripts to ensure everything loaded correctly. This gets complicated quickly as transitive dependencies form a tree and we can only define a linear load order.

This was manageable if the library was written for the browser, but what about npm packages written for Node.js? There are many useful libraries that would work perfectly fine in the browser if it weren’t for the peaky requires.

This is the primary problem Webpack solves. It allows one to use CommonJS or Modules to sort out the dependency tree and concatenates everything into a single bundle. Webpack maintains the isolation of modules using IIFEs.

Input source

Here is a simple example where our app.js imports is-even, but is-even depends on is-odd, which in turn depends on is-number.

// npm is-number  
export default (n) => Number.isInteger(n);  
// npm is-odd  
import isNumber from 'is-number';
export default (n) => isNumber(n) && n % 2 === 1;  
// npm is-even  
import isOdd from 'is-odd';
export default (n) => !isOdd(n);  
// app.js  
import isEven from 'is-even';
if (isEven(42)) {  
  throw new Error('cant even');  
}

Using Webpack, it can combine all these files into a single bundle.js, which we can then load into our HTML.

Output bundle.js

The raw Webpack output isn’t intended for human consumption. I’ve tried to clean it up a bit and highlighted the source from the input files. Notice the import/export has been replaced with plain functions.

But there is more to webapps than just dependencies. How does one ensure the JavaScript written is supported on the desired browsers/devices? Heck, how does one solve this problem when importing libraries that could be written in any flavour of JavaScript or even TypeScript?

Moar Webapps, moar problems

ECMAScript (the formal standard behind JavaScript), evolves over time. In fact, it has seen far more radical changes than most programming languages. In a short decade, JavaScript has added let, const, Promise, Arrow functions, async functions, Modules, WebAssembly and even standardized left-pad. While some deride the upkeep required to stay up-to-date, in my opinion, this is what makes JavaScript interesting and fresh.

But what about browser/device support? There’s just no way Apple, Google, Microsoft and Mozilla are going to perfectly implement and release all these features at the same time across all their browsers/devices. (Well, except they did that one time for the launch of WebAssembly.)

In general, feature support is all over the place. We constantly use https://caniuse.com to see if we can use certain JavaScript syntax/APIs natively. How do we manage this chaos?

This is where Babel comes into the picture. Babel is a transpiler and is usually used to transpile modern JavaScript down to early versions that are supported by more browsers/devices.

Webpack has been integrated with Babel, which allows one to configure the Webpack output bundle to be any JavaScript version. Typically, the target JavaScript version is ES5, but that is now over a decade old! If one doesn’t need IE 11 support, then ES6 is much more suitable. In general, choosing a more modern version of JavaScript results in a smaller bundle (less code is rewritten by Babel) but at the cost of lower browser/device support.

But how does one even think about which browsers/devices should be supported? Ultimately, this is a business decision that needs to be balanced with the cost of supporting older browsers/devices to the business value it returns.

Let’s look at a simpler example and narrow the topic to mobile Apple devices. Here is the global market share by iOS version from May to Oct 2021. We can see iOS 15 was released in October.

Source https://gs.statcounter.com/ios-version-market-share/mobile-tablet/worldwide/

There is a clear drop-off at iOS 11 and older. We can then look up which devices had their end-of-life at iOS 11, and interestingly there were none. This means all devices that ran on iOS 11 were upgradable to iOS 12. It is actually iOS 10 that had a cut-off with iPad 4 and iPhone 5.

This means if we target iOS 11 and newer, that will achieve a 98% market share. But shouldn’t we try to hit 100%? Isn’t it unfair to leave behind those who cannot afford to upgrade? That is a value judgement the business would need to make. It could be the case that supporting that last 2% eats up the vast majority of development cost and be even more unfair to 98% of customers. It could also be the case the majority of the profits are made in that last 2%. This is completely dependent on the business.

We’ll assume a decision has been made to support iOS 11, how do we implement this? We can see what JavaScript is natively supported with iOS 11, but what we really need to do is configure Babel to transpile everything newer and unsupported down to iOS 11.

Babel has a rich ecosystem of plugins that allows one to exactly tune what to transpile, but fortunately, we don’t need to bother with all that. Babel preset-env integrates with browserslist which allows us to target iOS 11 directly. This will load the appropriate Babel plugins needed to transpile down to iOS 11.

Now we have all the puzzles pieces to get Webpack to transpile our JavaScript source down to iOS 11. The webpack.config.js snippet to achieve this would be,

{  
  ...  module: {  
    rules: [  
      {  
        resource: {  
          test: /.+\.js$/,  
        },  
        use: {  
          loader: 'babel-loader',  
          options: {  
            presets: [  
              ['[@babel/preset-env](http://twitter.com/babel/preset-env)', {  
                targets: 'ios 11'  
              }]  
            ]  
          }  
        }  
      }  
    ]  
  }  ...  
}

Such wow, many cool

This is just a surface-level introduction to Webpack. The vast majority of webapps use Webpack to solve the two major problems of

  1. managing and bundling dependencies
  2. transpiling sources to increase browser/device support

But Webpack can do so much more. Webpack loaders support importing of CSS and even transforming CSS to add browser prefixes. And there are additional Babel presets to enable support for React with JSX.

Adding TypeScript support can be done either as a Webpack loader or Babel preset, which is simultaneously extremely frustrating having more than one way of doing things but also liberating being able to make the right tradeoff.

For production builds one needs to carefully configure the minification, code splitting and output filenames. But that is a topic for another time.

Do you want to truly understand how to build webapps simply? You’re in luck, Battlefy is hiring.

Releasing sooner than MVP
November 22nd 2021

2022

Powered by
BATTLEFY