Why Alley loves Webpack

“If you haven’t used Webpack…. oh, man.” – What I hear from everyone who has converted to Webpack.

Developer tools change quickly, and in the past year the frontend team at Alley adopted Webpack as a central part of our frontend application toolchain. It has emerged and continued to grow as a powerful tool which not only allows us to refactor some of our long-held best practices to a cleaner and more modular approach, but to rethink the way we approach problems on the frontend.

Consider this: as recently as the middle of 2014, RequireJS was arguably still the dominant module loader for frontend Javascript. If you’re like me, you’d prefer not to remember those days of web development. RequireJS enabled us to declare dependencies, load modules on demand, and compile a minified bundle. I also know it was possible to lose entire afternoons sorting out issues with esoteric module types, module scope conflicts, static file paths, or the vagaries of the requirejs API itself.

After requirejs (and overlapping — I know I was a little slow to pick up some of these) we got a bunch of pretty good tools with compile-to-js options like CoffeeScript and Traceur, generators like Yeoman and, of course, endless task runners. Taken separately, these made big but still incremental advances in the build stack. We had client-side Javascript modules! And build scripts to bundle and optimize assets! And a generator to take the pain out of gluing it all together! We maybe even had a little express server that could livereload if we set it up right!

This was wonderful, but we were still developing the frontend of webpages in largely the same way we always had: with separation at every level. There was still a wall between the backend and the client, and each Javascript module, DOM component, style and asset stayed in their own metaphorical bucket. This still wasn’t a system. Then, two things happened: React and Webpack. As a pair, these two technologies have revolutionized our approach to writing web applications and sites. Each has spurred the other beyond what we thought was possible. The oft-derided JSX reimagined the early days of the web by inlining and localizing styles and assets at a component level, which turned out to be a game-changing approach. Webpack generalized the component concept to a holistic modular system, in which everything becomes a composable module.

I’m not going to get into the details of how Webpack does its magic; Instead I’ll give an overview usability comparison if you’re coming from Grunt and illustrate some of the amazing things it can do.


The Tools You Know, Webpack’d

The Basics

With Grunt, you declare everything statically, including the order Grunt operates on your files. It might look something like this:

grunt.initConfig({
  concat: {
    src: ['foo/bar.js', 'baz/*.js'], // ordered by hand
    dest: 'build/output.js',
  },
  uglify: {
    targetOutput: {
      files: {
        'build/output.min.js': ['build/output.js'],
      },
    },
  },
});

 

This is a trivial example; there are certainly tools to abstract away some of the hardcoding typically involved in Grunt config files, but even so this configuration handles only CSS. All your other asset types will need their own processors, watchers, files and options declared in your config and, at the end, you have a bunch of pieces that (hopefully) fit together. Unless you are using a compiler plugin, fitting together isn’t even a guarantee for your scripts without painstaking work on the config.

On the other hand, Webpack config only requires a couple options to get going with your whole application, and leaves the job of connecting assets to your code.

webpack.config.js
{
  entry: 'entry.js',
  output: {
    path: 'build',
    filename: 'output.js',
  },
}


entry.js
require('./foo/bar'); // webpack follows dependencies, of any type
require('./baz');

 

As your app and config grow, the biggest additions you will have to make to the config will be to tell Webpack how to handle other asset types like styles or images. The shape of your output bundle is determined by the shape of your dependency tree, not by a config file you have to maintain.


Compilation/Optimization

You’re probably used to running a preprocessor on your stylesheets, concatenating and minifying your scripts, or copying other assets to the public path, usually as part of a watch in your task runner. This is the first place people usually get confused by Webpack. You don’t need a task runner. You don’t need to run these preprocessors separately. Webpack handles all your files as a system. It walks your entire dependency tree and transforms the modules it finds according to loaders you declare. For example, including a stylesheet in your build process would require two steps: requiring the scss file, and telling Webpack how to handle that type of file. Here, Webpack will process a stylesheet, and transform it into a reusable Javascript module to be referenced by require in the compiled bundle instead of the source file

main.js
require('styles/main.scss')
webpack.config.js
{
  …
  module: {
    loaders: [
      { test: /\.scss$/, loader: 'style!css!sass' },
    },
  },
  …
}

 

Each additional asset type requires the appropriate loader, an object in the loaders array with a regex to test for the file type, and the chain of loaders that adds the file to the Javascript bundle. From there Webpack can do lots of magic for you.


Concatenating Javascript

JS concatenation can be brittle and limiting — most of you already know that. Webpack solves this by outputting modules that you can require only where you need them. No polluting the global namespace, no timing issues, no manual static ordering in config. With npm3 and some levels of optimization, you can actually “shake” the tree so that only the parts of the files that you actually use are included. Minifying is built in and can be used with a CLI flag, and there are other plugins to dedupe and optimize your code.


Image/SVG Optimization

Spriting, optimizing and making sure your image assets are properly in the public path was often a task shared by SASS and the task runner. Webpack can handle all this for you, and then some. Because guess what, images are modules too! You just have to tell Webpack how you want them handled. At Alley we’ve used webpack-svgstore-plugin to produce SVG symbol sprites, but there are numerous options for how to handle images of various types.


Linting/Testing

Webpack has what are called pre-loaders, which can be built in to allow you to do all kinds of static analysis before your build compiles. We use ESlint for our Javascript, and wrote sasslint-loader (still in development) to leverage sass-lint.


Complex or Chained tasks for different build stages 

What if you have bunch of scripts like develop, deploy, build, serve, etc? Turns out node has a pretty good task runner already in npm.


Live Reloading

For Javascript applications that don’t depend on another backend (or even some that do), webpack-dev-server spins up an express server for you that includes live reloading and feedback from the build output in the browser console. If that doesn’t work for your needs, there’s also a plugin that injects a simple live reloading script for you.


The Things You Didn’t Even Know You Needed


Modules Everywhere

There’s no overstating how much modules enable everything else in Webpack. We’ve long held out for the promise of Web Components, but this is the next best thing. Components no longer have to be a template over here in this directory, a stylesheet in this SASS component structure, a Javascript file somewhere else, and links to some images in the public path. You can treat components as components, with all their requisite parts included. (You can even approximate almost all the benefits of Web Components, but we’ll get to that!) At a basic level Webpack accomplishes this by wrapping each asset with a Javascript loader; relative file paths get converted to absolute static URIs, CSS is loaded and parsed by Javascript, some data can be encoded and injected as a string, etc.


Module Systems Support

There are almost as many Javascript module formats as there are frameworks, which can cause problems if you want to use them together. You almost never have to pay attention to this with Webpack. Any modules it can’t handle can usually be coerced with a very small transform. You can even wrap free globals at runtime, and use them as you would any other module. For example, instead of the well known jQuery closure pattern, we can treat jQuery like a module as any other. The dependency becomes an explicit require that can be checked at compile time, and the closure is handled by Webpack.

before-webpack.js
(function($) {
  $(document).ready();
})(jQuery);


webpack.config.js
{
  externals: {
    'jquery': 'jQuery',
  },
}

after-webpack.js
import $ from 'jquery';

$(document).ready();

 


Compile-to-Js

Treating Javascript as a compile target opens the door to new and better tools for writing modular, readable code. With Webpack, transpiling is just another step in the build process. You can write in (almost) any language you like (ES6, Coffeescript, et al), it will run in the browser, and you can still use all the other transforms you like.


Dynamic Compile-time Injection

While we’re putting together components, maybe we want to build a srcset attribute for images required via a relative path and also prepend a small, inlined dataurl version from which to transition for improved time to interaction. Webpack (and server-side rendering) loaders are built for stuff like this. Have a preference about how you’d like to inject SVGs or fonts? There’s probably a loader to handle it.


Automatic Chunking

Now that we’ve got all these components, how do we manage loading them in the right place? Not to fear, Webpack does almost all of this for you. You can specify how you’d like your bundles to be chunked either by defining entry points (often roughly equivalent to a file per page), or by declaring points right in your Javascript where new chunks should start. These chunks can then be loaded lazily or on-demand. Webpack can optimize further by separating out any code that gets reused across modules and making sure you only load it once.


Hot Module Replacement

If you’re using React, prepare to have your mind blown. Remember the first time you used a live reloader with styles, and the page would change without you having to reload? Hot module replacement is like that, but for Javascript. Or, as they like to say, working on the engine of a car driving 100mph. This is a game changer for the iterative process of building UIs.

CSS-modules 

This is one of the things that only became possible because of Webpack (it was spec’d and written by Webpack core devs). Because stylesheets are just modules, Webpack turns style classes into hashes that can be injected dynamically into templates, guaranteeing localized, no-conflict styles, like a Web Component would! Combined with chunking strategies, it also allows us to easily optimize certain types of loading performance. This is so new we’re still working on the best ways to use it, but coupled with PostCSS, it gives us a chance to fix problems and developmental struggles with CSS that have existed since the dawn of the web.

This only scratches the surface of what Webpack can do and what we use it for, but hopefully provides some insight into how you might be able to leverage it in your frontend stack.