Frontend Development 24 min read

Eliminating Redundant base.js Chunk in Webpack 4 with a Custom Mini‑CSS‑Extract Plugin

This article explains why Webpack 4 generates an unnecessary base.js file when extracting common CSS with mini‑css‑extract‑plugin, analyses the underlying bootstrap dependency issue, and presents a custom plugin that adjusts the compilation graph to move the empty JS module, suppress the extra chunk, and correctly emit the shared base.css file.

TikTok Frontend Technology Team
TikTok Frontend Technology Team
TikTok Frontend Technology Team
Eliminating Redundant base.js Chunk in Webpack 4 with a Custom Mini‑CSS‑Extract Plugin

Introduction

The author, a front‑end engineer at ByteDance, describes a performance problem caused by the default css‑in‑js approach in Webpack 4, where style files are bundled into JavaScript and loaded asynchronously, leading to delayed style loading and missed cache benefits.

Style Performance Optimization

Two scenarios are identified: (1) delayed CSS loading can cause flash‑of‑unstyled‑content for skeleton screens, and (2) CSS cannot be cached independently when bundled with JS that changes frequently. The solution is to inline critical CSS and extract stable CSS into separate files.

Internal Splitting Convention

The project adopts a directory layout where src/common/styles holds global CSS (rarely changed) and src/pages/*/index.scss holds page‑specific CSS (frequently changed). The build should output common/base.[contenthash].css for global styles and embed page‑specific styles in the page entry JS bundle.

Split Configuration

Using mini-css-extract-plugin to extract global CSS and splitChunks to create a shared base chunk, the following configurations are applied:

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  module: {
    rules: [{
      test: /\/common\/styles\/.*(css|scss|sass|less)$/i,
      use: [MiniCssExtractPlugin.loader, 'css-loader']
    }]
  },
  plugins: [new MiniCssExtractPlugin({
    chunkFilename: 'common/[name].[contenthash].css'
  })],
  optimization: {
    splitChunks: {
      cacheGroups: {
        base: {
          name: "base",
          test: /\/common\/styles\/.*(css|scss|sass|less)$/,
          chunks: "all",
          enforce: true
        }
      }
    }
  }
};

This setup creates base.css and, unexpectedly, a base.js file containing an empty module generated by the loader.

Problem Analysis

Webpack 4 forces every loaded asset to produce a JS module. When splitChunks creates an isolated CSS chunk, the associated empty JS module cannot be attached to any entry chunk, so Webpack emits a separate base.js . The bootstrap code then lists base as a required chunk, causing an extra network request and, if the file is removed, a white‑screen because the entry cannot resolve its dependencies.

Webpack 5 fixes this by skipping JS generation for pure CSS chunks, but many projects remain on Webpack 4.

Solution: Adjusting the Dependency Graph

The strategy is to locate the “problem chunk” (the CSS‑only chunk with an empty JS module), move the empty module into its source entry chunk, remove the chunk from the entry’s deferredModules list, and finally prevent the generation of the redundant .js file while preserving the .css asset.

1. Identify Problem Chunks

const CSS_MODULE_TYPE = 'css/mini-extract';
compilation.hooks.beforeChunkAssets.tap(pluginName, () => {
  const splitChunks = compilation.chunks.filter(c => c.chunkReason && c.chunkReason.includes('split chunk'));
  splitChunks.forEach(splitChunk => {
    const cssModules = [];
    const nonCssModules = [];
    Array.from(splitChunk.modulesIterable).forEach(mod => {
      if (mod.type !== CSS_MODULE_TYPE) nonCssModules.push(mod);
      else cssModules.push(mod);
    });
    const uselessModules = nonCssModules.filter(m => m._source && m._source._value === '// extracted by mini-css-extract-plugin');
    if (uselessModules.length && uselessModules.length === nonCssModules.length) {
      // move empty JS modules to the original entry chunk
      uselessModules.forEach(useless => {
        useless.reasons.forEach(reason => {
          reason.module.chunksIterable.forEach(originChunk => {
            splitChunk.moveModule(useless, originChunk);
          });
        });
      });
      // later we will detach the CSS chunk from the entry group
      this.problemChunkInfos[splitChunk.id] = { originChunks: [] };
    }
  });
});

2. Detach the Chunk from Entry Dependencies

compilation.hooks.beforeChunkAssets.tap(pluginName, () => {
  Object.values(this.problemChunkInfos).forEach(info => {
    // each problem chunk knows its origin chunks from the previous step
    // we remove the chunk from its groups
    const chunk = compilation.chunks.find(c => c.id === info.id);
    chunk.groupsIterable.forEach(group => group.removeChunk(chunk));
    // remember origin chunks for later CSS injection
    info.originChunks = Array.from(chunk.groupsIterable).map(g => g.chunks[0]);
  });
});

3. Suppress JS Asset Generation and Re‑inject CSS Asset

compilation.hooks.additionalChunkAssets.tap(pluginName, chunks => {
  chunks.forEach(chunk => {
    if (this.problemChunkInfos[chunk.id]) {
      chunk.files.forEach(file => {
        if (path.extname(file) === '.js') {
          // remove the empty JS file
          chunk.files = chunk.files.filter(f => f !== file);
          delete compilation.assets[file];
        } else if (path.extname(file) === '.css') {
          // attach the generated CSS to the original entry chunk so html‑webpack‑plugin can emit a
this.problemChunkInfos[chunk.id].originChunks.forEach(origin => {
            origin.files.push(file);
          });
        }
      });
    }
  });
});

After these steps, the build produces only base.css (placed in common/ ) and the entry bundle no longer references a non‑existent base.js . The HTML generated by html-webpack-plugin now contains a proper <link rel="stylesheet" href=".../common/base.[hash].css"> tag, and the page renders correctly.

Final Result

The directory structure matches the intended design: a shared base.css for global styles, page‑specific JS bundles that include their own CSS, and no superfluous JS chunks. The custom plugin restores correct dependency handling in Webpack 4 without upgrading to Webpack 5.

frontendPluginbuild optimizationwebpackmini-css-extract-pluginsplitchunks
TikTok Frontend Technology Team
Written by

TikTok Frontend Technology Team

We are the TikTok Frontend Technology Team, serving TikTok and multiple ByteDance product lines, focused on building frontend infrastructure and exploring community technologies.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.