Webpack - The Most Powerful JavaScript Module Bundler

Webpack is a powerful JavaScript module bundler but seems complicated to most developers with tons of concepts and configurations.

It can be hard to set up a complex webpack configuration for the first time. Writing advanced configurations to optimize performance could be even more difficult. That’s why it is often pre-configured in project starters or JavaScript frameworks.

Good news is Webpack from v4 promised to work with zero to little configuration. This post is for someone who wants to play around with Webpack but don’t want to read through its intimidating documentation.

Using Webpack incorrectly can actually hurt performance, so be careful when using it.

What is Webpack

Prior to webpack, front-end developers would use task runners like Grunt and Gulp to bundle JavaScript libraries or websites. They still work until these days but community needs something works out of the box.

Webpack was born the ultimate JavaScript module bundler which can bundle, transform, and package JavaScript modules, asset files, and their dependencies into a few bundled static assets.

dependency graph

Webpack uses dependency graph as its internal representation, this is great because every module now explicitly states its dependencies and we’ll avoid bundling modules that aren’t in use.

Webpack is considered to be a low-level tool used not only individually but also layered beneath other awesome tools. Because of its flexibility, webpack isn’t always the easiest entry-level solution, however I do believe it is the most powerful:

  • Bundles ES Modules, CommonJS, and AMD modules (even combined).
  • Can create a single bundle or multiple chunks that are asynchronously loaded at runtime (to reduce initial loading time).
  • Dependencies are resolved during compilation, reducing the runtime size.
  • Loaders can preprocess files while compiling, e.g. TypeScript to JavaScript, Handlebars strings to compiled functions, images to Base64, etc.
  • Highly modular plugin system to do whatever else your application requires.

How Webpack works

Webpack was primarily designed to deal with the emerging trend of having more and more javascript in the client.

By mapping out a graph of the dependencies Webpack is able to load the different modules in the correct order asynchronously and in parallel. Internally Webpack has its own resolver for figuring out the dependency graph between different modules.

Any time one file depends on another, webpack treats this as a dependency. This allows webpack to take non-code assets, such as images or web fonts, and also provide them as dependencies for your application.

When webpack processes your application, it starts from a list of modules defined on the command line or in its configuration file. Starting from these entry points, webpack recursively builds a dependency graph that includes every module your application needs, then bundles all of those modules into a small number of bundles - often, just one - to be loaded by the browser.

Entry point — indicates which module webpack should use to begin building out its internal dependency graph. webpack will figure out which other modules and libraries that entry point depends on (directly and indirectly).

Output — tells webpack where to emit the bundles it creates and how to name these files. It defaults to ./dist/main.js for the main output file and to the ./dist folder for any other generated file.

Plugins — webpack has a rich plugin interface. Most of the features within webpack itself use this plugin interface. This makes webpack very flexible.

Loaders — webpack enables the use of loaders to preprocess files. This allows you to bundle any static resource way beyond JavaScript. You can easily write your own loaders using Node.js.

Mode — By setting the mode parameter to either development, production or none, you can enable webpack’s built-in optimizations that correspond to each environment.

webpack supports all browsers that are ES5-compliant (IE8 and below are not supported). webpack needs Promise for import() and require.ensure(). If you want to support older browsers, you will need to load a polyfill before using these expressions.

Built-in optimizations

Webpack can do many optimizations to reduce the output size of your JavaScript by deduplicating frequently used modules, minifying, and giving you full control of what is loaded initially and what is loaded at runtime through code splitting. It can also make your code chunks cache friendly by using hashes.

Code Splitting — webpack allows you to split your codebase into multiple chunks which can then be loaded on demand or in parallel at runtime. It can be used to achieve smaller bundles and control resource load prioritization which, if used correctly, can have a major impact on load time. You can config entry, use SplitChunksPlugin or using dynamic imports.

Tree Shaking — a term commonly used in the JavaScript context for dead-code elimination, relies on static structure of ES6 modules, originally popularized by Rollup, became built-in support in Webpack since version 2, since version 4 you can provide hints to webpack’s compiler on the pureness of your code by adding "sideEffects": false property in package.json.

Custom configurations

If you’ve never seen a webpack config file before, they may appear daunting at first glance; however, once you grasp the syntax and the core philosophies behind the bundler, reading them will feel like second nature.

Out of the box, webpack won’t require you to use a configuration file. However, it will assume the entry point of your project is src/index and will output the result in dist/main.js minified and optimized for production.

Example of webpack.config.js:

const path = require('path');

module.exports = {
  mode: "production", // "production" | "development" | "none"
  // Chosen mode tells webpack to use its built-in optimizations accordingly.
  entry: "./app/entry", // string | object | array
  // defaults to ./src
  // Here the application starts executing
  // and webpack starts bundling
  output: {
    // options related to how webpack emits results
    path: path.resolve(__dirname, "dist"), // string
    // the target directory for all output files
    // must be an absolute path (use the Node.js path module)
    filename: "bundle.js", // string
    // the filename template for entry chunks
    publicPath: "/assets/", // string
    // the url to the output directory resolved relative to the HTML page
    library: "MyLibrary", // string,
    // the name of the exported library
    libraryTarget: "umd", // universal module definition
    // the type of the exported library
    /* Advanced output configuration (click to show) */
    /* Expert output configuration (on own risk) */
  },
  module: {
    // configuration regarding modules
    rules: [
      // rules for modules (configure loaders, parser options, etc.)
      {
        test: /\.jsx?$/,
        include: [
          path.resolve(__dirname, "app")
        ],
        exclude: [
          path.resolve(__dirname, "app/demo-files")
        ],
        // these are matching conditions, each accepting a regular expression or string
        // test and include have the same behavior, both must be matched
        // exclude must not be matched (takes preferrence over test and include)
        // Best practices:
        // - Use RegExp only in test and for filename matching
        // - Use arrays of absolute paths in include and exclude
        // - Try to avoid exclude and prefer include
        issuer: { test, include, exclude },
        // conditions for the issuer (the origin of the import)
        enforce: "pre",
        enforce: "post",
        // flags to apply these rules, even if they are overridden (advanced option)
        loader: "babel-loader",
        // the loader which should be applied, it'll be resolved relative to the context
        options: {
          presets: ["es2015"]
        },
        // options for the loader
      },
      {
        test: /\.html$/,
        use: [
          // apply multiple loaders and options
          "htmllint-loader",
          {
            loader: "html-loader",
            options: {
              / ... /
            }
          }
        ]
      },
      { oneOf: [ / rules / ] },
      // only use one of these nested rules
      { rules: [ / rules / ] },
      // use all of these nested rules (combine with conditions to be useful)
      { resource: { and: [ / conditions / ] } },
      // matches only if all conditions are matched
      { resource: { or: [ / conditions / ] } },
      { resource: [ / conditions / ] },
      // matches if any condition is matched (default for arrays)
      { resource: { not: / condition / } }
      // matches if the condition is not matched
    ],
    /* Advanced module configuration (click to show) */
  },
  resolve: {
    // options for resolving module requests
    // (does not apply to resolving to loaders)
    modules: [
      "node_modules",
      path.resolve(__dirname, "app")
    ],
    // directories where to look for modules
    extensions: [".js", ".json", ".jsx", ".css"],
    // extensions that are used
    alias: {
      // a list of module name aliases
      "module": "new-module",
      // alias "module" -> "new-module" and "module/path/file" -> "new-module/path/file"
      "only-module$": "new-module",
      // alias "only-module" -> "new-module", but not "only-module/path/file" -> "new-module/path/file"
      "module": path.resolve(__dirname, "app/third/module.js"),
      // alias "module" -> "./app/third/module.js" and "module/file" results in error
      // modules aliases are imported relative to the current context
    },
    /* Alternative alias syntax (click to show) */
    /* Advanced resolve configuration (click to show) */
  },
  performance: {
    hints: "warning", // enum
    maxAssetSize: 200000, // int (in bytes),
    maxEntrypointSize: 400000, // int (in bytes)
    assetFilter: function(assetFilename) {
      // Function predicate that provides asset filenames
      return assetFilename.endsWith('.css') || assetFilename.endsWith('.js');
    }
  },
  devtool: "source-map", // enum
  // enhance debugging by adding meta info for the browser devtools
  // source-map most detailed at the expense of build speed.
  context: __dirname, // string (absolute path!)
  // the home directory for webpack
  // the entry and module.rules.loader option
  //   is resolved relative to this directory
  target: "web", // enum
  // the environment in which the bundle should run
  // changes chunk loading behavior and available modules
  externals: ["react", /^@angular/],
  // Don't follow/bundle these modules, but request them at runtime from the environment
  stats: "errors-only",
  // lets you precisely control what bundle information gets displayed
  devServer: {
    proxy: { // proxy URLs to backend development server
      '/api': 'http://localhost:3000'
    },
    contentBase: path.join(__dirname, 'public'), // boolean | string | array, static file location
    compress: true, // enable gzip compression
    historyApiFallback: true, // true for index.html upon 404, object for multiple paths
    hot: true, // hot module replacement. Depends on HotModuleReplacementPlugin
    https: false, // true for self-signed, object for cert authority
    noInfo: true, // only errors & warns on hot reload
    // ...
  },
  plugins: [
    // ...
  ],
  // list of additional plugins
  /* Advanced configuration (click to show) */
}

Conclusion

Webpack is becoming the standard in bundling JavaScript. Webpack exists to give us a feature of deploying code organized in multiple files to a user’s web browser.