what is Webpack?

by Stephanie Coates

Like Babel, Webpack was one of the terms that was sometimes thrown around in conversation during my coding bootcamp, but I had no idea what its purpose was.

One of the flaws of a fast-paced coding bootcamp is the lack of substantial time spent on foundational concepts. As students, we learned React before any of us really understood how JavaScript functioned without a framework/library. So, when Webpack was thrown into the mix, it created even more confusion.

I’ve spent a lot of time post-bootcamp revisiting foundational concepts and building my understanding from the ground up, filling in the prior gaps I had, including my understanding of Webpack.

What is Webpack?

At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.

…whaaa? What is a static module bundler? What is a module, and what is a bundler? What is a dependency graph?

Let’s start at the very beginning. Webpack was created to solve a problem. As JavaScript grew in popularity and use of frameworks/libraries like React, Angular and Vue became mainstream, frontend applications became more and more complex. Gone are the days of a single index.html file, a few imported css stylesheets, and a little bit of JavaScript between the <script> tags for interactivity.

As the need for more JS and CSS files in applications grew, it because difficult to organize them in a non-cumbersome way. Programmers wanted modularized, better-organized code in separate files, but there wasn’t a clear way on how to deploy this type of code structure for a web browser to read.

One way is with multiple script tags, one for each file. But the files would have to be placed in the correct order so variables were available before they were needed. Another option was to concatenate all the files into one bundled .js file, but:

  • global scoping also posed a threat to variables being mistakenly overwritten
  • it wouldn’t be clear whether third-party libraries were bundled in your code or if they needed to be imported via a separate script tag
  • writing unit tests was a nightmare

Now, we rarely bring <script> and <style> tags directly into an html file ourselves. Instead, more commonly we rely on workflows (like webpack!) which compile and transpile our various code and files so it can be read by the browser. This solved file organization issues, and then some.

Webpack can:

  • take all your application’s web of interconnected files (SASS, LESS, CSS, JS modules and their dependencies, images, etc) and output bundled static asset files (.js, .css, .jpg, .png) that can be deployed to a server and read by a browser
  • transpile code
  • minify and optimize code
  • provide a development server to host your frontend application over http protocol

Webpack is for development purposes only. It’s installed into your devDependencies and used as a tool to bundle and build your code before it’s deployed to a server.

Webpack as a build tool

Webpack’s most prominent functionality is acting as a build tool and bundling your code. You tell webpack where to enter your app, and from there, it creates a dependency graph of all the different pieces, how they’re connected, and how they’re dependent on one another.

Any time one file depends on another (say, function A is defined in file B but invoked in file C), webpack treats it as a dependency. Both code and non-code assets (such as images or web fonts) can be dependencies for your application.

From the entrypoint, webpack recursively traverses through your application and builds a dependency graph of all the modules your application needs to function. These necessary modules are then bundled into one or more bundle files to be loaded by the browser.

A module can be defined as a grouping of code that is exported from one location and imported into another location. For example:

hello.js

const hello = (name) => {
  console.log(`hello, ${name}!`);
}

export default hello;

world.js

import hello from 'hello';

hello();

The hello function is a module and a dependency of world.js. If we tell Webpack to enter at world.js, then it will inspect the imports on that file and add them to the dependency graph, which is just an interconnected web of all of your modules. If hello.js had dependencies of its own, these would be added to the dependency graph too. This is an overimplified example, but hopefully it helps illustrate how Webpack bundling works.

When you use npm run build on your React application, Webpack bundles your application code and outputs it in a newly generated build folder, which is what’s deployed to the server. (NOTE: If you’re using create-react-app, you won’t see Webpack directly in your app’s devDependencies. This is because it’s brought in via react-scripts, a library that handles all the configuration and brings most of the dependencies of the project.)

Once deployed, the entrypoint to your app is build/index.html, the minified file that’s initially read by the browser once a request is made.

In here, you’ll see a <div id='root'> tag, and three script tags below which render all the JavaScript (organized and bundled by Webpack!) your app needs. /static/js/main._____.chunk.js contains the document.getElementById("root") logic to inject your JavaScript into the HTML.

npm scripts for Webpack

How do you kick off Webpack bundling once it’s brought into your project? create-react-app abstracts this away with react-scripts - npm run build triggers react-scripts build, which executes the build.js file in the react-scripts node_module. Everything is pre-configured and taken care of already. Just run npm run build to get the outputted build folder ready for deployment.

If you aren’t using create-react-app, want to customize your Webpack configuration, or just want to better understand how it works under the hood, keep reading:

You’ll need your build script in package.json to trigger the Webpack node_module. (Before this, make sure Webpack is installed as a devDependency.) The script, at a minimum, should look something like this:

"build": "webpack <entrypoint> <output file>

Rather than declaring this in the script, you can also place this logic (along with additional configuration, such as plugins) in a config file and then link to it with the --config argument.

"build": "webpack --config webpack.prod.js"

Notice how the file webpack.prod.js is passed in with the build script, defining the configuration settings for a production build of your app. You’ll likely also a have webpack.dev.js file, which contains Webpack settings when bundling a development build while using npm start script with webpack-dev-server . More on this later.

A simple webpack.prod.js file will look something like this:

const path = require('path');
module.exports = {
    entry: './src/index.js',
    mode: 'production',
    output: {
        path: path.resolve(__dirname, './dist/'),
        filename: 'bundle.js'
    }
};

The webpack.dev.js file will look similar with identical entry and output objects, but mode set to development and additional settings for devTools and a link to the development server. More on this, along with ways to keep your code DRY with webpack-merge, see the Webpack docs.

Webpack will still work if no configuration is provided. 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.

In addition to bundling modules during the production build process, Webpack also transpiles (using Babel and minifies your code.

webpack dev server

With a simple HTML application, you might serve it over file system protocol (right click on HTML file —> open in Finder —> right click on file in Finder —> open with Chrome). The file system protocol loads files locally, no internet required, rather than making http calls over the network to access files.

With a React application, though, using the file system protocol poses issues:

  • <BrowserRouter> from react-router doesn’t play nice with it and the application fails silently
  • Networking speeds (located in the devTools network tab) will be 0B since the files aren’t served over http, so you don’t get a realistic picture of application performance

On the contrary, deploying to a rented server every time you make a change is cumbersome.

Webpack has a nifty feature, the webpack-dev-server - a local development server that spins up a Node.js runtime, hosting your frontend application over http protocol (at localhost:3000) to mimic how it’ll behave once deployed. Within create-react-app, the webpack-dev-server starts up by default when you run npm start.

The development server also provides the ability to do hot-module-replacement. Whenever you make a change in your code and save the file, Webpack will replace the module without needing to rebuild your whole app and/or reload the entire webpage.

Webpack loaders and plugins

By default, Webpack only understands JavaScript. If you want to bring in additional libraries/tools like ES6+, Typescript, or CSS preprocessors like SASS, you’ll need to tell webpack how to process them.

Let’s use ES6 code in your React app as an example. In order to transpile the code into ES5-friendly code for Webpack to bundle, a loader from Babel is necessary. More specifically, four of them:

  • @babel-core: understands how to read & parse code, and generate corresponding output
  • babel-loader: the interface between Babel and Webpack
  • babel-preset-env: for compiling Javascript ES6 code down to ES5 (babel-preset-es2015 is now deprecated)
  • babel-preset-react: for compiling JSX and other stuff down to Javascript

Loaders go in the webpack config file. It should look something like this - each loader has its own object within a loaders array, within a module object at the root:

const path = require('path');
module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, './dist/'),
        filename: 'bundle.js'
    },
    module: {
        loaders: [
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/,
                query: {
                    presets: ['es2015']
                }
            }
        ]
    }
};

In addition to the Webpack config, you’ll need to create a .babelrc file with the following (this is a babel thing, not a webpack thing, but they work in conjunction):

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

Plugins are used for tools that apply an effect to all the code, rather than specific files. plugins is also placed in the root level of the config file:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  module: {
        loaders: [
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/,
                query: {
                    presets: ['es2015']
                }
            }
        ]
    }
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Production',
    }),
  ],
  output: {
      path: path.resolve(__dirname, './dist/'),
      filename: 'bundle.js'
  },
};

For the most updated details on how to customize your webpack configuration file, see the webpack configuration docs.