Optimizing Webpack project setup on CRA

Hi all! One of the projects at work we have was originally created on the create-react-app utility (by the way, I have an article about what is happening with CRA now and what awaits it in the future). The question arose as to whether it is possible to somehow optimize the assembly for the speed and weight of the compressed project, since there are big plans for the growth of the project and I would not want something to slow down, and I did this accordingly. I want to tell you about how everything went, what steps were taken and what happened in the end. Also at the end I will attach the code of the entire configuration.

I would like to say right away that this is another article on webpack with its settings given, and some may not see anything new, since the sequence of actions and settings performed will be described here. Please do not throw stones, as I just want to share my experience, maybe even get advice on better settings and, hopefully, this will help someone.

It’s time to eject

The first step, of course, was to eject the project. After that, I received a bunch of folders and files that were under the hood of CRA.

To keep track of build speed and compressed weight, I immediately put Statoscope into the project. It is set up very simply. The plugin is installed in the project, imported into the webpack file and added to the list of plugins. It can be customized in every possible way, but in this case I did not need it.

npm i --save-dev @statoscope/webpack-plugin
...
const StatoscopeWebpackPlugin = require('@statoscope/webpack-plugin').default;

module.exports = {
  ...
  plugins: [
    ...
    new StatoscopeWebpackPlugin()
  ],
  ...
}

Further, when starting the project build, we will additionally open a page with project statistics. Here is the stat that was received after ejecting the project.

Statistics without project customization

Statistics without project customization

The weight of the project in compressed form is 371.35 kb, the build speed is 36.4 seconds, 1 chunk.

Removing the excess

After the eject, I started looking at installed plugins and packages.

Here is a list of what I cleaned up:

1) camelcase package (used in jest file)
2) Case sensitive paths webpack plugin that monitors case sensitivity in imports
3) Package prompts
4) Identity obj proxy (mainly needed for testing css modules, since you need to define the name of the object as a class, and in our Styled-components project)
5) Sass-loader
6) Tailwind
7) Semver (to compare the React version, I only needed to check if version 16 is higher, in the project I raised the version to 18 and the check is definitely not needed anymore)
8) Sevents, since it’s for subscribing notifications on MacOS
9) Removed a bunch of react-dev-utils plugins, but left the package, because some things, including displaying labels of different colors in the cmd console, did not work (informative messages during hot reload, which are configured in create react app in the console)

As a result, the statistics showed me a decrease in the compressed assembly by 0.02 kb, the assembly speed did not change.

Then I got into the webpack configuration files, project launch, build, etc. and I realized that without a bottle, it would not be possible to quickly understand these files completely and most likely it doesn’t even make sense.

And I made a strong-willed decision to completely demolish this assembly and put my own ..

Starting over

On my github about a year ago, I created a project with webpack build, lint settings, etc. The webpack build there is quite standard, with settings for sass, css-modules, etc., but without settings for React Router and strong optimization. I took it as the basis for the new assembly. If someone is interested in this assembly, then by link You can view.

I immediately put a statoscope, check the statue, I’m not happy)

New build stats

New build stats

I started to wind up plugins for compression and optimization of the assembly.

Here’s what I put in addition to what I already had:

1) Terser Plugin. This plugin minifies and compresses the code, which allows you to reduce the size of the assembly well. (I even immediately measured the status and got a result that pleased me)

Stat with tercer

2) Css Minimizer Webpack Plugin to optimize CSS in build.
The first two plugins are placed in the minimizer section of the webpack

module.exports = {
  ...
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin(),
      new CssMinimizerWebpackPlugin()
    ],
    ...
  }
  ...
}

3) React Refresh Webpack Plugin. It is needed more for development, so that during hot-reload the page does not reload if only the visual component changes. Helps to keep the state of the application.
4) Hashed Module Ids Plugin. It constructs hashes of the modules in the assembly based on their relative paths. More is also needed for convenience.

After this I:
– slightly modified the assembly to work with Styled-Components
– added a loader to work with svg @svgr/webpack so you can import the svg normally
– put the necessary alias for the project
– added historyApiFallback: trueso that React Router works, otherwise the paths are perceived as get requests to the server
– prescribed client: { overlay: false } so that linter errors do not climb over the screen, as this is very infuriating.
– slightly improved tscoinfig

As a result, I got the following statistics

312.01 kb compressed assembly weight, 28.1 seconds assembly and also 2 chunks.

Given that the project itself is small, in principle the optimization was successful. The only thing I decided to add is the splitting of all packages into separate chunks. For this, the optimization block was redesigned

optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin(),
      new CssMinimizerWebpackPlugin()
    ],
    runtimeChunk: 'single',
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: Infinity,
      minSize: 0,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/) ?? 'package';
            return `npm.${packageName[1].replace('@', '')}`;
          }
        }
      }
    },
  },

After that we get the following

The weight has increased to 334.46 kb, the assembly has become a little faster and all packages are in separate chunks.

What is the result?

In the end, I can say that the CRA build does have extra plugins and complex code to support webpack, but it seems to be not so bad.

The optimization turned out, although not super duper effective, but still it is, the project began to weigh less in compressed form (I would like to note that the weight in uncompressed form is still higher than in CRA) and now everything is divided into a bunch of chunks. Now the results are not very big, but I think that when the project grows, this setting will help the project.

I am also attaching the full code of webpack.config.js and tsconfig.json that are used for this build.

webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const StatoscopeWebpackPlugin = require('@statoscope/webpack-plugin').default;
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
const webpack = require('webpack')
const Dotenv = require('dotenv-webpack');
require('dotenv').config({path: path.resolve(__dirname, '.env')})

const mode = process.env.NODE_ENV || "development";
const port = process.env.PORT || 3000;
const devMode = mode === "development";
const target = devMode ? 'web' : 'browserslist';
const devtool = devMode && 'source-map';

module.exports = {
  mode,
  target,
  devtool,
  devServer: {
    port,
    open: true,
    hot: true,
    historyApiFallback: true,
    client: {
      overlay: false
    },
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': '*',
      'Access-Control-Allow-Headers': '*',
    },
    onBeforeSetupMiddleware(devServer) {
      devMode && require(path.resolve(__dirname, 'src/setupProxy.js'))(devServer.app);
    },
  },
  entry: path.resolve(__dirname, 'src', 'index.tsx'),
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'js/[name].[contenthash].bundle.js',
    chunkFilename: 'js/[id].[contenthash].js',
    assetModuleFilename: 'assets/[hash][ext]',
    publicPath: '/'
  },
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin(),
      new CssMinimizerWebpackPlugin()
    ],
    runtimeChunk: 'single',
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: Infinity,
      minSize: 0,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/) ?? 'package';
            return `npm.${packageName[1].replace('@', '')}`;
          }
        }
      }
    },
  },
  resolve: {
    extensions: ['.json', '.jsx', '.tsx', '.ts', '.js', '.mjs'],
    alias: {
      '~components': path.resolve(__dirname, 'src/components'),
      '~hooks': path.resolve(__dirname, 'src/hooks'),
      '~types': path.resolve(__dirname, 'src/types'),
      '~pages': path.resolve(__dirname, 'src/pages'),
      '~utils': path.resolve(__dirname, 'src/utils'),
      '~constants': path.resolve(__dirname, 'src/constants'),
      '~helpers': path.resolve(__dirname, 'src/utils/helpers'),
      '~layouts': path.resolve(__dirname, 'src/layouts'),
      '~api': path.resolve(__dirname, 'src/api'),
    },
  },
  plugins: [
    new Dotenv({ path: path.resolve(__dirname, '.env'), systemvars: true }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'public', 'index.html'),
    }),
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash].bundle.css',
    }),
    new CleanWebpackPlugin(),
    new ESLintPlugin({
      extensions: ['ts', 'tsx'],
      exclude: ['/node_modules/', '/.idea/', '/.vscode/'],
    }),
    new ReactRefreshWebpackPlugin({
      overlay: false,
    }),
    new webpack.ids.HashedModuleIdsPlugin(),
    new StatoscopeWebpackPlugin()
  ],
  module: {
    rules: [
      {
        test: /\.(c|sa|sc)ss$/i,
        use: [
          devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              esModule: true,
              importLoaders: 1,
              modules: {
                mode: 'icss'
              },
            },
          },
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [require('postcss-preset-env')],
              },
            },
          },
        ],
      },
      {
        test: /\.woff2?$/i,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[name].[ext]',
        },
      },
      {
        test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 10000,
          },
        },
      },
      {
        test: /\.svg$/,
        use: [
          {
            loader: require.resolve('@svgr/webpack'),
            options: {
              prettier: false,
              svgo: false,
              svgoConfig: {
                plugins: [{ removeViewBox: false }],
              },
              titleProp: true,
              ref: true,
            },
          },
          {
            loader: require.resolve('file-loader'),
            options: {
              name: 'static/media/[name].[hash].[ext]',
            },
          },
        ],
        issuer: {
          and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
        },
      },
      {
        test: /\.tsx?$/,
        use: {
          loader: 'ts-loader',
          options: {
            transpileOnly: true
          }
        },
        exclude: /node_modules/,
      },
      {
        test: /\.m?jsx?$/i,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react'],
          },
        },
      },
    ],
  },
};
tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "lib": [
      "dom",
      "dom.iterable",
      "es2016"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "downlevelIteration": true,
    "emitDeclarationOnly": false
  },
  "include": [
    "src"
  ],
  "exclude": ["node_modules", "build"],
  "extends": "./tsconfig.paths.json" //Лежат пути для alias
}

I hope these settings will help someone, as they are quite universal.
I would be happy to discuss the assembly and get some advice on what can be removed or added to make the assembly better.

All good)

Py.Sy. Cart

I have mine telegram channel, in which I post various articles, posts and conduct mini quizzes on programming. Join)

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *