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"
]
}