How to Create a Production-Ready Webpack 4 Config From Scratch

How to Create a Production-Ready Webpack 4 Config From Scratch

Webpack is a powerful and flexible tool for bundling and optimizing your front-end assets. It can handle not only JavaScript, but also CSS, images, fonts, and more. Webpack also allows you to easily split your code into multiple bundles for better performance and caching.

Webpack can seem intimidating at first with its huge array of configuration options, plugins, and loaders. But once you understand the core concepts, you can build up a config file tailored to your specific needs.

In this comprehensive tutorial, we‘ll go through all the steps needed to create a production-ready webpack 4 config file from scratch. Here are the main topics we‘ll cover:

  • Setting up a basic webpack config with entry and output
  • Using plugins like CleanWebpackPlugin and HtmlWebpackPlugin
  • Creating separate development and production configurations
  • Utilizing loaders for handling CSS, JavaScript, images, etc.
  • Splitting code for lazy loading chunks
  • Generating source maps for easier debugging
  • Minifying assets and implementing long-term caching

To help solidify these concepts, we‘ll be building a simple demo app as we progress through each section. Let‘s get started!

Setting up the Project

First, make a new directory and initialize the project:

mkdir webpack-demo
cd webpack-demo 
npm init -y

Install webpack and webpack-cli as dev dependencies:

npm install webpack webpack-cli --save-dev

Now create the following directory structure and files:

webpack-demo
|- package.json
|- /dist
  |- index.html
|- /src  
  |- index.js
|- webpack.config.js

In dist/index.html, add the following:

<!doctype html>
<html>
  <head>
    <title>Webpack Demo</title>
  </head>
  <body>
    <script src="main.js"></script>
  </body>
</html>

And in src/index.js, add a simple console log:

console.log(‘Hello from webpack!‘);

Basic Webpack Config

Open up webpack.config.js and add a basic config:

const path = require(‘path‘);

module.exports = {
  entry: ‘./src/index.js‘,
  output: {
    path: path.resolve(__dirname, ‘dist‘),
    filename: ‘main.js‘    
  }
};

This tells webpack to start bundling from src/index.js and output the result to dist/main.js.

Now add an npm script to package.json to run the build:

"scripts": {
  "build": "webpack"
}

Run it:

npm run build

If you open dist/index.html in a browser, you should see "Hello from webpack!" logged to the console.

Using Plugins

Let‘s add a couple useful plugins to our config.

First, install them:

npm install --save-dev html-webpack-plugin clean-webpack-plugin

Update webpack.config.js:

const path = require(‘path‘);
const HtmlWebpackPlugin = require(‘html-webpack-plugin‘);
const { CleanWebpackPlugin } = require(‘clean-webpack-plugin‘);

module.exports = {
  entry: ‘./src/index.js‘,
  plugins: [
    new CleanWebpackPlugin(), 
    new HtmlWebpackPlugin({
      template: ‘./dist/index.html‘
    })
  ],
  output: {
    path: path.resolve(__dirname, ‘dist‘),
    filename: ‘main.js‘
  }
};

CleanWebpackPlugin removes the dist directory before each build to keep it clean.

HtmlWebpackPlugin simplifies the creation of HTML files to serve your webpack bundles. It will automatically inject tags into our HTML.

Now we can remove the tag from dist/index.html:

<!doctype html>
<html>
  <head>
    <title>Webpack Demo</title>
  </head>
  <body>
  </body>
</html>

Rerun the build and open dist/index.html. It should still log "Hello from webpack!" as before.

Development vs Production Configs

For local development, we‘d like to use webpack-dev-server for live reloading. For production builds, we want to generate minified output and source maps.

Let‘s split our config into two separate files using webpack-merge.

Install the dependencies:

npm install --save-dev webpack-dev-server webpack-merge

Create webpack.dev.js with the following:

const { merge } = require(‘webpack-merge‘);
const common = require(‘./webpack.config.js‘);

module.exports = merge(common, {
  mode: ‘development‘,
  devtool: ‘inline-source-map‘,
  devServer: {
    contentBase: ‘./dist‘,
    port: 8000
  },
});

Create webpack.prod.js with:

const { merge } = require(‘webpack-merge‘);
const common = require(‘./webpack.config.js‘);

module.exports = merge(common, {
  mode: ‘production‘,
  devtool: ‘source-map‘  
});

Both configs merge in the common webpack.config.js file. The dev config enables source maps and webpack-dev-server. The prod config sets the mode to ‘production‘ for minified output.

Update package.json with two new scripts:

"scripts": {
  "start": "webpack-dev-server --config webpack.dev.js",
  "build": "webpack --config webpack.prod.js"
}

Now we can use npm run start for local development and npm run build for production builds.

Using Loaders

Webpack loaders allow you to preprocess files as you import or "load" them. Let‘s set up loaders to handle our CSS and JavaScript.

CSS Loaders

To import CSS files from within our JavaScript, we‘ll use css-loader and style-loader:

npm install --save-dev css-loader style-loader

Add the following to your common webpack.config.js:

module: {
  rules: [
    {
      test: /\.css$/,
      use: [‘style-loader‘, ‘css-loader‘] 
    }
  ]
}

This tells webpack to use the css-loader and style-loader for any files that match the .css file extension.

For production builds, we‘ll extract the CSS into separate files using MiniCssExtractPlugin.

Install it:

npm install --save-dev mini-css-extract-plugin

Update webpack.prod.js:

const MiniCssExtractPlugin = require(‘mini-css-extract-plugin‘);

module.exports = merge(common, {
  mode: ‘production‘,
  devtool: ‘source-map‘,
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          ‘css-loader‘
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: ‘[name].[contenthash].css‘
    })
  ]
});

MiniCssExtractPlugin removes the CSS from the JavaScript bundles and outputs them to separate .css files. The [contenthash] in the filename enables long-term caching.

JavaScript Loaders

To transpile our modern JavaScript to ES5 for wider browser support, we‘ll use Babel with babel-loader.

Install the dependencies:

npm install --save-dev @babel/core @babel/preset-env babel-loader

Create a .babelrc file in the root directory:

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

Add babel-loader to webpack.config.js:

module: {
  rules: [
    {
      test: /\.js$/, 
      exclude: /node_modules/,
      loader: ‘babel-loader‘
    }  
  ]
}

Now we can use modern JavaScript features like arrow functions and classes without worrying about browser support.

Code Splitting

Splitting your code into multiple bundles that can be loaded on demand can greatly improve the performance of your application.

Let‘s create an example module that we‘ll lazy load.

In src/math.js:

export function add(a, b) {
  console.log(`Adding ${a} and ${b}`);
  return a + b;  
};

Update src/index.js:

console.log(‘Hello from webpack!‘);

import(‘./math‘).then(math => {
  console.log(math.add(16, 26));
});

When we import the math module using import(), webpack will create a separate bundle that is only loaded when the import() is called.

Rerun npm run build and look in your dist directory. You should see two bundles now – main.[contenthash].js and 1.[contenthash].js (chunk names are incrementing numbers by default).

Source Maps

Source maps allow you to debug your original source code in the browser instead of the compiled and minified bundle code.

We already set up the devtool option in our dev and prod configs, so source maps should already be working if you run the dev server with npm start or do a prod build with npm run build.

The development config uses inline-source-map, which embeds the source map as a data URL in the JavaScript bundle.

The production config uses source-map, which generates the source maps as separate files. This is usually the best option for production.

Minification

To reduce the size of our bundles, webpack 4 automatically minifies our code in production mode (which we set in webpack.prod.js).

However, we can go a step further and use additional plugins for better minification.

Minify CSS

Install OptimizeCssAssetsPlugin:

npm install --save-dev optimize-css-assets-webpack-plugin 

Update webpack.prod.js:

const OptimizeCssAssetsPlugin = require(‘optimize-css-assets-webpack-plugin‘);

optimization: {
  minimizer: [
    new OptimizeCssAssetsPlugin()
  ]
},

This plugin will use cssnano under the hood to optimize and minify your CSS.

Minify JS

Webpack 4 uses TerserPlugin by default to minify JavaScript in production mode, so we don‘t need to install anything extra.

However, we can pass some additional configuration options to TerserPlugin if needed:

optimization: { 
  minimizer: [
    new TerserPlugin({
      parallel: true,
      cache: true,
    })
  ]
}

Here we‘ve enabled parallelization and caching for faster builds.

Conclusion

We‘ve now covered all the fundamental concepts needed to create a production-ready webpack 4 config from scratch. To recap, we‘ve learned how to:

  • Set up a basic webpack config with entry and output
  • Use plugins like CleanWebpackPlugin and HtmlWebpackPlugin
  • Create separate development and production configs
  • Use loaders for handling CSS, JavaScript, and other assets
  • Split code for lazy loading
  • Generate source maps for debugging
  • Minify CSS and JS for smaller bundle sizes

There are many more features and options we haven‘t covered here – this is just the tip of the iceberg. I encourage you to read through the official webpack guides to continue your learning.

You can find the complete source code for the demo app in this GitHub repo.

I hope this tutorial has been helpful in demystifying webpack for you. Now go forth and bundle!

Similar Posts