Modern Webpack Boilerplate: Part 3: Asset Handling, Unit Testing, and Bundle Analysis

Recap of Part 2: In the previous installment, we integrated Babel and React into our Webpack setup, enhancing our modern SPA boilerplate. We configured Babel to support the latest JavaScript features, TypeScript, and React JSX. Additionally, we implemented environment variable management using dotenv and refined our Webpack configuration for handling .tsx files.

Unit testing setup

For unit tests we will use Jest which is one of most popular solutions on the market.

Install following dev-dependencies:

npm i -D jest @types/jest @swc/core @swc/jest

Jest configuration file Create jest.config.js in the root of the project. Following config will tell jest to transform ts|js|tsx|jsx files with swc-jest

Note: swc-jest does not perform type checking in your tests.

// jest.config.js
/** @type {import('@jest/types').Config.InitialOptions} */
const config = {
  transform: {
    '^.+\\.(t|j)sx?$': '@swc/jest',
  },
  resetMocks: true,
  setupFilesAfterEnv: ['<rootDir>/testSetup.ts'],
  verbose: true,
}

module.exports = config

Environment Configuration for Testing Introduce an env.test file to manage test-specific environment variables:

//.env.test

ENVIRONMENT_NAME=test

Setting Up Environment Variables: Configure a setup file to load the appropriate environment variables before all tests:

// test.setup.ts
import { loadEnvs } from './config/env'

beforeAll(() => {
  process.env.NODE_ENV = 'test'

  loadEnvs()
})

Verify Environment Configuration In order to confirm the behaviour of loading test envs we could create a dummy test

// src/example.spec.ts
describe('example unit test', () => {
  it('should have the NODE_ENV equal to test', () => {
    expect(process.env.NODE_ENV).toBe('test')
  })
})

Commands for Running Tests: Update package.json with commands to run and clear the Jest cache:

"test:unit": "jest",
"test:unit:clear": "jest --clearCache"

Now the tests can be executed by typing: npm run test:unit in the terminal

Key points

  • the Jest setup is tailored for TypeScript files within the Node.js environment (Jest's default setting)
  • the setup can be easily extended with React Testing Library to cover component tests as well, however it is good to explore alternatives first 👇
  • especially tools that allow testing in the browser like:
  • from my experience in large applications developers often encounter performance issues with React Testing Library due to the rendering of extensive DOM trees and scanning through it.
  • additionally in heavy integration tests Playright or Cypress combined with msw for network call interception, tend to perform more efficiently

Asset handling

Webpack simplifies the management of static assets, all we need to do is to extend webpack.config.ts - module.rules

// webpack.config.ts
module: {
  rules: [
    // ... rest of the previous config
    {
      test: /\.(png|svg|jpg|jpeg|gif)$/i,
      type: 'asset/resource',
    },
    {
      test: /\.(woff|woff2|eot|ttf|otf)$/i,
      type: 'asset/resource',
    },
  ]
}

asset/resource will emit the file in the production bundle, this is beneficial because:

  • it won't bloat JavaScript bundle size
  • browsers can cache each asset independently
  • emited file will include hash and filename for cache-busting

CSS setup

Frontend applications are incomplete without styling. To efficiently manage CSS files within a Webpack build, we need to update config.

Install following dependencies:

npm i -D style-loader css-loader postcss-loader mini-css-extract-plugin css-minimizer-webpack-plugin postcss postcss-preset-env autoprefixer
  • style-loader: Injects CSS into the DOM via <style> tags, useful during development for hot module replacement.
  • css-loader: Resolves @import and url() within CSS files as module dependencies, enabling them to be bundled by Webpack.
  • postcss-loader: Integrates PostCSS into the build process, allowing for CSS transformations using plugins.
  • MiniCssExtractPlugin: Extracts CSS into separate files, ideal for production builds to leverage browser caching.
  • CssMinimizerPlugin: Optimizes and minifies CSS, enhancing load times and efficiency in production.
  • postcss: Facilitates the use of advanced CSS through plugins like autoprefixer and postcss-preset-env.
  • postcss-preset-env: Allows the use of future CSS features by compiling them to compatible code today.
  • autoprefixer: Automatically applies vendor prefixes to CSS rules, ensuring cross-browser compatibility based on "Can I Use" data.

Create dedicated file for post-css configuration to not bloat Webpack config file

/**
 * @type {import('postcss').ProcessOptions}
 */
const config = {
  plugins: [['postcss-preset-env', 'autoprefixer']],
}

module.exports = config

Lastly the webpack config has to be updated with loaders that were installed previously to handle CSS in JS files. In module.rules add yet another loaders for css and post-css.

Note We only want to extract the CSS to dedicated files on production build.

// webpack.config.ts
module: {
  rules: [
    // ... rest of the previous config
    {
      test: /\.css$/i,
      use: [
        mode === 'production' ? MiniCssExtractPlugin.loader : 'style-loader',
        'css-loader',
        'postcss-loader',
      ],
    },
  ]
}

Additionally new plugins has to be initialized in the plugins array. Additionally we want to run those optimizations only on production builds.

// webpack.config.ts
import MiniCssExtractPlugin from "mini-css-extract-plugin";
import CssMinimizerPlugin from "css-minimizer-webpack-plugin";

plugins: [
  mode === "production" && new MiniCssExtractPlugin(),
	mode === "production" && new CssMinimizerPlugin(),
]

Bundle size observability

Applications bundle size tend to grow really fast, it is good idea to observe frequently how much our chunks weight. Webpack offers advanced plugin to visualize bundle size in a graphical format.

Install dependencies:

npm i -D webpack-bundle-analyzer @types/webpack-bundle-analyzer

Optimize build commands:

  • regular build, which is faster and can run without issues in the pipelines
  • build-analyze for devs to check the bundle size using webpack-bundle-analyzer

Add the following command to your package.json, utilizing the ANALYZE_BUILD environment variable to toggle the analysis mode:

"build:analyze": "ANALYZE_BUILD=true npm run build",

Configure Webpack: Modify your Webpack configuration to conditionally include the BundleAnalyzerPlugin based on the ANALYZE_BUILD flag.

// webpack.config.ts
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'

const createBundleAnalyzerConfig = () => {
  const isAnalyzeMode = process.env.ANALYZE_BUILD === 'true'
  const config: BundleAnalyzerPlugin['opts'] = isAnalyzeMode
    ? { analyzerMode: 'server' }
    : { analyzerMode: 'disabled' }

  return config
}

const bundleAnalyzerConfig = createBundleAnalyzerConfig()

// in the createWebpackConfig function
plugins: [
  //... rest of the config

  mode === 'production' && new BundleAnalyzerPlugin(bundleAnalyzerConfig),
]

Execution:

  • Run npm run build for a standard build, suitable for deployment and CI environments like GitHub Actions.
  • Use npm run build:analyze to launch a server that provides a detailed breakdown of the bundle, enabling developers to visually assess which parts are contributing most to the size.

This setup ensures that developers have the tools needed to maintain optimal performance and manageability of application bundles efficiently.

That concludes this part. You can review all the details in following PR: https://github.com/Verthon/webpack-react-boilerplate/pull/3/files

Thank you, any feedback is highly appreciated.