Moving from Webpack 3 to Webpack 4 — Now with some exciting updates

Written by: Mario Grunitz

Icon of a hypercube

By Dmitry Shishko, PHP developer at WeAreBrain.


The Latest Updates:

We had such a great response to our article about moving from Webpack 3 to Webpack 4 and so many of you had questions and comments that we decided to do an update. If you haven’t read the original article, you really should but if you have and you’re curious about our new findings scroll down to Appendix A for our latest updates.

Introduction — Moving from Webpack 3 to Webpack 4

Webpack 4.0, was recently released, and in this article, I’d like to share my experience with moving from Webpack 3 to Webpack 4.

Previously I had two Webpack config files for development and production in the root folder. I decided to move the New configs into separate folders/configs.

├──configs
│   ├──webpack.common.js
│   ├──webpack.config.dev.js
│   ├──webpack.config.prod.js

To start setting up Webpack config for development we’ll need to install Webpack as follows:

yarn add webpack webpack-cli --dev
or
npm i webpack webpack-cli --save-dev

I’ve created a common config for development and production ./config/webpack.common.js

const path = require('path');
const webpack = require('webpack');
const ROOT_DIR = path.resolve(__dirname, '../');
const SRC_DIR = path.resolve(ROOT_DIR, 'src');
module.exports = {
  entry: [
    'babel-polyfill',
    path.join(SRC_DIR, 'index'),
  ],
  resolve: {
    modules: [
      'src',
      'node_modules',
      'theme',
      'theme/fonts',
    ],
    extensions: ['.js', '.css', '.scss'],
    alias: {
      theme: path.resolve(ROOT_DIR, 'theme'),
    },
  },
  module: {
    rules: [
      // js
      {
        test: /\.js$/,
        use: 'babel-loader',
        exclude: [
          path.resolve(ROOT_DIR, 'node_modules'),
        ],
      },
      // images
      {
        test: /\.(png|ico|gif|svg|jpe?g)(\?[a-z0-9]+)?$/,
        use: 'url-loader',
      },
      // fonts
      { test: /\.(woff|woff2|eot|ttf|otf)$/, use: ['url-loader'] }
    ]
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV),
        CUSTOM_HOST: JSON.stringify(process.env.CUSTOM_HOST),
        HTTPS: JSON.stringify(process.env.HTTPS),
        RUBY_BACKEND: JSON.stringify(process.env.RUBY_BACKEND),
      }
    }),
  ]
};

And then I created the config for development ./config/webpack.config.dev.js

const webpack = require('webpack');
const autoprefixer = require('autoprefixer');
const merge = require('webpack-merge');
const path = require('path');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const common = require('./webpack.common.js');
const ROOT_DIR = path.resolve(__dirname, '../');
const DIST_DIR = path.resolve(ROOT_DIR, 'dist');
module.exports = merge(common, {
  mode: 'development', //
  devtool: 'eval',
  entry: [
    require.resolve('react-dev-utils/webpackHotDevClient'),
  ],
  output: {
    path: DIST_DIR,
    publicPath: '/dist/',
    filename: 'bundle.js',
  },
  module: {
    rules: [
      // css
      {
        test: /\.css$/,
        include: /node_modules/,
        loader: [
          'style-loader',
          'css-loader',
        ]
      },
      // sass
      {
        test: /\.scss$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              sourceMap: true,
            },
          },
          {
            loader: 'postcss-loader',
            options: {
              sourceMap: true,
              plugins() {
                return [autoprefixer('last 2 version')];
              }
            }
          },
          {
            loader: 'sass-loader',
            options: {
              sourceMap: true,
            },
          }
        ]
      },
    ]
  },
  plugins: [
    new ProgressBarPlugin({
      format: 'Build [:bar] :percent (:elapsed seconds)',
      clear: false,
    }),
    new webpack.NamedModulesPlugin(),
    new webpack.HotModuleReplacementPlugin(),
  ]
});

Execute npm run start, which runs the script.

package.json
...
"start": "NODE_ENV=dev webpack --config ./configs/webpack.config.dev.js",
...

And I’ve included one Deprecation Warning.

DeprecationWarning: Tapable.plugin is deprecated. Use new API on `.hooks` instead

add process.traceDeprecation = true; to webpack.common.js and find where this warning has been invoked. It’s in webpack-dev-server and an issue is open. So we wait for an update.

Create config for production:

./config/webpack.config.prod.js

const webpack = require('webpack');
const path = require('path');
const merge = require('webpack-merge');
const autoprefixer = require('autoprefixer');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const OfflinePlugin = require('offline-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const common = require('./webpack.common.js');
const ROOT_DIR = path.resolve(__dirname, '../');
const DIST_DIR = path.resolve(ROOT_DIR, 'dist');
const prodConfig = {
  mode: 'production',
  devtool: 'source-map',
  target: 'web',
  output: {
    path: DIST_DIR,
    publicPath: '/',
    filename: '[name].[chunkhash].js',
    chunkFilename: '[name].[chunkhash].js',
  },
  module: {
    rules: [
      // sass
      {
        test: /\.scss$/,
        use: ExtractTextPlugin.extract({
          use: [
            {
              loader: 'css-loader',
              options: {
                sourceMap: true
              }
            },
            {
              loader: 'postcss-loader',
              options: {
                plugins: [autoprefixer('last 2 version')],
                sourceMap: true
              }
            },
            {
              loader: 'sass-loader',
              options: {
                sourceMap: true
              }
            }
          ]
        }),
      },
    ],
  },
  optimization: {
    runtimeChunk: false,
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
    minimizer: [
      new UglifyJsPlugin({
        cache: true,
        parallel: true,
        sourceMap: true
      })
    ]
  },
  plugins: [
    // clean dist folder
    new CleanWebpackPlugin(['dist'], { root: ROOT_DIR }),
    new webpack.optimize.OccurrenceOrderPlugin(),
    new ExtractTextPlugin({
      filename: 'styles.[hash].css',
      allChunks: false,
    }),
    new HtmlWebpackPlugin({
      template: 'src/index.html',
      favicon: 'theme/img/favicon.ico',
      inject: true,
      sourceMap: true,
      chunksSortMode: 'dependency'
    }),
    new CompressionWebpackPlugin({
      asset: '[path].gz[query]',
      algorithm: 'gzip',
      test: new RegExp('\\.(js|css)$'),
      threshold: 10240,
      minRatio: 0.8
    }),
    new OfflinePlugin({
      caches: 'all',
      AppCache: false,
    }),
  ],
};
if (process.env.NODE_ANALYZE) {
  prodConfig.plugins.push(new BundleAnalyzerPlugin());
}
module.exports = merge(common, prodConfig);

Execute npm run build, witch run script.

package.json
...
"build": "NODE_ENV=production webpack --config ./configs/webpack.config.prod.js",
...

Oops! There’s an error…

Error: webpack.optimize.UglifyJsPlugin has been removed, please use config.optimization.minimize instead.

Update ./config/webpack.config.prod.js

...
new OfflinePlugin({
      caches: 'all',
      AppCache: false,
      ServiceWorker: {
        minify: false, 
      },
    }),
...

Then I run the build script and it appears I have two DeprecationWarnings.

DeprecationWarning: Tapable.plugin is deprecated. Use new API on `.hooks` instead
DeprecationWarning: Tapable.apply is deprecated. Call apply on the plugin directly instead

Check trace deprecation and ind, where this warning is invoked. It’s in html-webpack-plugin, and the issue is open. So we wait for an update again.

Check our build performance.

Webpack 3, in develop mode, first build:

Compiled successfully for  0 min 18 sec 158 ms

Webpack 3, in production mode:

Version: webpack 3.11.0
Time: 37173ms

Webpack 4, in develop mode, first build:

Compiled successfully for  0 min 16 sec 269 ms

Building process improved by 10%!

Webpack 4, in production mode:

Version: webpack 4.1.0
Time: 27402ms

Building process became faster by 26%!

I hope this was helpful!

Appendix A — the latest updates

First of all, thank you to all of you for comments and questions. It’s great to engage with everyone. It’s been a little over 6 months since Webpack released a number of updates, and I decided that I’d really like to share what’s changed.

1. I’ve updated libs in package.json

"webpack": "^4.23.1",
"webpack-cli": "^3.1.2",

2. As mentioned by Dave Thompson in a comment, the ExtractTextPlugin has been deprecated.

Updated config for production:

./configs/webpack.config.prod.js

const webpack = require('webpack');
...
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OfflinePlugin = require('offline-plugin');
...
const common = require('./webpack.common.js');
const ROOT_DIR = path.resolve(__dirname, '../');
const DIST_DIR = path.resolve(ROOT_DIR, 'dist');
const MODULES_DIR = path.resolve(ROOT_DIR, 'node_modules');
const prodConfig = {
  mode: 'production',
  ...
  module: {
    rules: [
     // sass
    {
      test: /\.(sa|sc|c)ss$/,
      use: [
         MiniCssExtractPlugin.loader,
         {
           loader: 'css-loader',
           options: {
             sourceMap: true
           },
          },
          {
            loader: 'postcss-loader',
            options: {
              plugins: [autoprefixer('last 2 version')],
              sourceMap: true,
             },
           },
           {
             loader: 'sass-loader',
             options: {
               sourceMap: true,
             },
           },
        ]
      },
    ],
  },
  optimization: {
   ...
  },
  plugins: [
   ...
   new MiniCssExtractPlugin({
    // Options similar to the same options in webpackOptions.output
    // both options are optional
    filename: '[name].[hash].css',
    chunkFilename: '[id].[hash].css'
  }),
  ...
 ],
};
if (process.env.NODE_ANALYZE) {
 prodConfig.plugins.push(new BundleAnalyzerPlugin());
}
module.exports = merge(common, prodConfig);

3. Building process

  • In development mode, the first build is:
Compiled successfully for  0 min 15 sec 358 ms
  • In production mode:
Version: webpack 4.23.1
Time: 22502ms

So, the building time has been decreased a little.

Would be great to hear all your thoughts!

(Visited 2,199 times, 1 visits today)
Mario Grunitz
Author info
Mario Grunitz
Mario is a WeAreBrain Co-founder. With more than 15 years of experience in the tech space, he has worked all over Europe and held countless leadership positions in corporate, startup and agency spheres.
Close