Webpack 5: Hot Module Replacement compiles whole app + does full reload

814 Views Asked by At

We are moving from webpack 4 to webpack 5 for the 'apps/admin' package in our lerna monorepo. After the upgrade we noticed that Hot Module Replacement is 'working' but has two problems:

  • It always does a full reload
  • It rebuilds the whole app on every change, taking about 60s

Also, the general build time is 40+% slower (for both development and production builds).

Why is it taking so long, and can we prevent it from completely rebuilding every single time?

Any input would be appreciated; we have looked at many StackOverflow questions by now... :)

Webpack config:

const path = require('path');
const webpack = require('webpack');
const sass = require('sass');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin');
// const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

const {
  ADMIN_SENTRY_DSN = 'xxx',
  API = 'xxx',
  APP = 'admin',
  GA_ID = 'xxx',
  ENV = 'development',
  NODE_ENV = 'development',
  PUBLIC_PATH = '/',
  ZENDESK = 'xxx',
  CALENDLY = 'xxx',
} = process.env;

const DEV = NODE_ENV === 'development';
const build = (ENV === 'staging' || ENV === 'live') ? 'production' : NODE_ENV;

const plugins = [
  new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
  // new BundleAnalyzerPlugin(),
  new CopyWebpackPlugin({
    patterns: [
      {
        from: path.resolve(__dirname, 'apps', APP, 'public', '*.*'),
        to: path.resolve(__dirname, 'apps', APP, 'dist'),
        context: path.resolve(__dirname, 'apps', APP, 'public'),
      },
      {
        from: path.resolve(__dirname, 'apps', APP, 'public', '_redirects'),
        to: path.resolve(__dirname, 'apps', APP, 'dist'),
        context: path.resolve(__dirname, 'apps', APP, 'public'),
      },
    ],
  }),
  new webpack.DefinePlugin({
    'process.env.ADMIN_SENTRY_DSN': JSON.stringify(ADMIN_SENTRY_DSN),
    'process.env.API': JSON.stringify(API),
    'process.env.APP': JSON.stringify(APP),
    'process.env.ENV': JSON.stringify(ENV),
    'process.env.GA_ID': JSON.stringify(GA_ID),
    'process.env.NODE_ENV': JSON.stringify(build),
    'process.env.PUBLIC_PATH': JSON.stringify(PUBLIC_PATH),
    'process.env.ZENDESK': JSON.stringify(ZENDESK),
    'process.env.CALENDLY': JSON.stringify(CALENDLY),
  }),
  new HTMLWebpackPlugin({
    filename: path.resolve(__dirname, 'apps', APP, 'dist', 'index.html'),
    template: path.resolve(__dirname, 'apps', APP, 'public', 'template.ejs'),
    favicon: path.resolve(__dirname, 'apps', APP, 'public', 'favicon.ico'),
    inject: 'body',
    cache: false,
    minify: !DEV ? {
      collapseWhitespace: true,
      removeComments: true,
      removeRedundantAttributes: true,
      removeScriptTypeAttributes: true,
      removeStyleLinkTypeAttributes: true,
      useShortDoctype: true,
    } : false,
  }),
  new MiniCssExtractPlugin({
    filename: '[name].css',
    chunkFilename: '[id].css',
  }),
];

if (!DEV) {
  plugins.push(
    new CompressionWebpackPlugin({
      filename: '[path].[base].gz',
      algorithm: 'gzip',
      test: /\.js$|\.css$/,
      minRatio: 1,
    })
  );
}

module.exports = {
  mode: build,
  target: DEV ? 'web' : 'browserslist',
  entry: {
    app: path.resolve(__dirname, `./apps/${APP}/src/index.jsx`),
  },
  output: {
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].chunk.[chunkhash].js',
    path: path.resolve(__dirname, 'apps', APP, 'dist'),
    publicPath: PUBLIC_PATH,
  },
  resolve: {
    extensions: ['.json', '.js', '.jsx'],
    fallback: {
      net: false,
      tls: false,
      dns: false,
    },
  },
  plugins,
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|svg|woff|woff2)$/,
        use: [
          {
            loader: 'file-loader',
          },
        ],
      },
      {
        test: /\.less$/,
        use: [
          DEV ? {
            loader: 'style-loader',
          } : MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
          },
          {
            loader: 'postcss-loader',
          },
          {
            loader: 'less-loader',
            options: {
              modifyVars: {
                'primary-color': '#0c99cc',
                'link-color': '#0c99cc',
              },
              javascriptEnabled: true,
            },
          }],
      },
      {
        test: /\.scss$/,
        exclude: /(node_modules)/,
        use: [
          DEV ? {
            loader: 'style-loader',
          } : MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
          },
          {
            loader: 'postcss-loader',
          },
          {
            loader: 'sass-loader',
            options: {
              implementation: sass,
            },
          },
        ],
      },
      {
        test: /\.(css)$/,
        use: [
          DEV ? {
            loader: 'style-loader',
          } : MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
          },
          {
            loader: 'postcss-loader',
          },
        ],
      },
      {
        test: /\.(js|jsx)$/,
        exclude: /(node_modules)/,
        use: ['babel-loader'],
      },
    ],
  },
  devServer: {
    publicPath: PUBLIC_PATH,
    historyApiFallback: true,
    contentBase: path.resolve(__dirname, 'apps', APP, 'dist'),
    hot: true,
    host: '0.0.0.0',
    stats: 'minimal',
    port: 3000,
    watchOptions: {
      ignored: ['**/node_modules/**'],
    },
  },
  devtool: 'source-map',
  optimization: {
    concatenateModules: true,
    usedExports: true,
    sideEffects: true,
    moduleIds: 'deterministic',
    chunkIds: 'named',
    removeAvailableModules: true,
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: Infinity,
      minSize: 0,
      cacheGroups: {
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react',
        },
        vendor: {
          test: /node_modules/,
          name(module) {
            let packageName = module.context.match(
              /[\\/]node_modules[\\/](.*?)([\\/]|$)/
            )[1];

            if (packageName.startsWith('@carl')) {
              packageName = module.rawRequest;
            }

            packageName = packageName.replace('@', '').replace('/', '-');

            return `module.${packageName}`;
          },
        },
      },
    },
    minimize: !DEV,
    minimizer: [
      '...',
      new CssMinimizerPlugin(),
    ],
  },
};

Our package.json

{
  "name": "carl-frontend",
  "private": true,
  "workspaces": [
    "lib/*",
    "apps/*"
  ],
  "scripts": {
    "admin:dev": "ENV=development API=${API:-xxx} APP=admin webpack serve",
    "admin:staging": "ENV=staging API=${API:-xxx} APP=admin webpack serve",
    "admin:live": "ENV=live API=xxx APP=admin webpack serve",
    "admin:build": "rm -rf ./apps/admin/dist && NODE_ENV=production APP=admin webpack",
    "admin:serve": "http-server ./apps/admin/dist -g",
    "teaser:dev": "cd ./apps/teaser && API=${API:-xxx} yarn dev",
    "teaser:staging": "cd ./apps/teaser && ADMIN_URL=xxx API=${API:-xxx} yarn dev",
    "teaser:live": "cd ./apps/teaser && API=xxx yarn dev",
    "teaser:build": "cd ./apps/teaser && yarn build",
    "teaser:serve": "cd ./apps/teaser && API=${API:-xxx} yarn start",
    "lint": "eslint --cache --cache-location .cache/.eslintcache --ext js --ext jsx lib apps",
    "test": "cross-env NODE_ICU_DATA=node_modules/full-icu API=${API:-xxx} jest --bail --maxWorkers=2",
    "cover": "yarn test --coverage",
    "teaser:cypress": "./cypress/scripts/test-teaser.sh",
    "connect:cypress": "./cypress/scripts/test-connect.sh",
    "connect:backend": "./cypress/mocks/api/run.sh tmp/cypress/mocks/api ${CONTAINER_CTRL:-docker-compose}"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "linters": {
      "*.{js,jsx}": [
        "eslint"
      ]
    }
  },
  "dependencies": {
    "@babel/runtime": "^7.4.4",
    "numbro": "^2.3.2",
    "tailwindcss": "^2.1.2"
  },
  "devDependencies": {
    "@babel/core": "^7.4.4",
    "@babel/plugin-proposal-class-properties": "^7.4.4",
    "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
    "@babel/plugin-proposal-object-rest-spread": "^7.4.4",
    "@babel/plugin-proposal-optional-chaining": "^7.8.3",
    "@babel/plugin-syntax-dynamic-import": "^7.2.0",
    "@babel/plugin-transform-async-to-generator": "^7.4.4",
    "@babel/plugin-transform-runtime": "^7.4.4",
    "@babel/preset-env": "^7.4.4",
    "@babel/preset-react": "^7.0.0",
    "@testing-library/react-hooks": "^3.2.1",
    "@virtuous/eslint-config": "^2.0.0",
    "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
    "babel-jest": "^27.0.1",
    "autoprefixer": "^10.2.6",
    "babel-loader": "^8.0.6",
    "babel-plugin-import": "^1.13.1",
    "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
    "cache-loader": "^3.0.1",
    "compression-webpack-plugin": "^8.0.0",
    "copy-webpack-plugin": "^8.1.1",
    "cross-env": "^7.0.2",
    "css-loader": "^2.1.1",
    "css-minimizer-webpack-plugin": "^3.0.0",
    "cssnano": "^4.1.10",
    "cypress": "^7.1.0",
    "enzyme": "^3.11.0",
    "jest-enzyme": "~7.1.2",
    "jest-environment-jsdom": "^27.0.1",
    "eslint": "^5.16.0",
    "file-loader": "^3.0.1",
    "full-icu": "^1.3.1",
    "html-webpack-plugin": "^5.3.1",
    "http-server": "^0.11.1",
    "husky": "^2.3.0",
    "jest": "^27.0.1",
    "jest-svg-transformer": "^1.0.0",
    "lerna": "^3.13.4",
    "less": "^3.9.0",
    "less-loader": "^5.0.0",
    "lint-staged": "^8.1.7",
    "mini-css-extract-plugin": "^1.6.0",
    "postcss": "^8.3.0",
    "postcss-loader": "^5.3.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "sass": "^1.27.0",
    "sass-loader": "^7.1.0",
    "style-loader": "^2.0.0",
    "webpack": "^5.37.1",
    "webpack-bundle-analyzer": "^4.4.2",
    "webpack-cli": "^4.7.0",
    "webpack-dev-server": "^3.11.2"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ]
}
0

There are 0 best solutions below