Search code examples
reactjswebpackbundling-and-minificationchunkscss-loader

Why is css-loader bloating my entry point bundle?


I'm working on cleaning up an older React project and trying to cut down on bundle size by implementing code splitting and chunking things out. I've made considerable progress, but my main entry point for the application is still sitting at ~600kb. It seems to be 95% coming from not the application code itself but the css-loader library I'm using during the webpack build process.

Webpack Visualizer screenshot showing 95% of the main bundle coming from css-loader

This seems incorrect, but I can't figure out what about my webpack config or packages is causing this bloat in this particular bundle.

Here's my environment-common and production webpack config info:

// webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

const BUILD_DIR = path.resolve(__dirname, 'build');
const SRC_DIR = path.resolve(__dirname, 'src');
module.exports = {
    entry: ['babel-polyfill', `${SRC_DIR}/index.js`],
    output: {
        path: BUILD_DIR,
        publicPath: '/',
        filename: '[name].[fullhash].bundle.js',
        chunkFilename: '[name].[chunkhash].bundle.js'
    },
    optimization: {
        moduleIds: 'named',
        splitChunks: {
            chunks: 'all'
        }
    },
    module: {
        // exclude node_modules
        rules: [
            {
                test: /\.(js)$/,
                exclude: /node_modules/,
                use: ['babel-loader']
            },
            {
                test: /\.(scss|css)$/,
                use: [
                    process.env.NODE_ENV !== 'production'
                        ? 'style-loader'
                        : MiniCssExtractPlugin.loader,
                    'css-loader',
                    {
                        loader: 'sass-loader',
                        options: {
                            sourceMap: true
                        }
                    }
                ]
            }
        ]
    },
    resolve: {
        alias: {
            '~': path.resolve(__dirname, 'src')
        },
        extensions: ['*', '.js']
    },
    plugins: [
        new HtmlWebpackPlugin({
            inject: true,
            template: './public/index.html'
        }),
        new CopyWebpackPlugin({
            patterns: [
                { from: './public/img', to: 'img' },
                { from: './web.config', to: 'web.config' }
            ]
        })
    ]
};
// webpack.prod.js
const { merge } = require('webpack-merge');
const webpack = require('webpack');
const CompressionPlugin = require('compression-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const common = require('./webpack.common');
const config = require('./config/config.prod.json');

const extractCSS = new MiniCssExtractPlugin({ filename: '[name].fonts.css' });
const extractSCSS = new MiniCssExtractPlugin({ filename: '[name].styles.css' });

process.traceDeprecation = true;
module.exports = merge(common, {
    mode: 'production',
    devtool: 'source-map',
    plugins: [
        new webpack.DefinePlugin({
            API_BASE_URL: JSON.stringify(config.API_BASE_URL),
            TUMBLR_CLIENT_BASE_URL: JSON.stringify(config.TUMBLR_CLIENT_BASE_URL)
        }),
        extractCSS,
        extractSCSS,
        new CompressionPlugin()
    ],
    optimization: {
        splitChunks: {
            chunks: 'all'
        },
        minimize: true
    }
});
// package.json
{
    "name": "***",
    "version": "1.0.0",
    "description": "***",
    "author": "***",
    "url": "***",
    "copyright": "***",
    "license": "GPL",
    "private": true,
    "homepage": "***",
    "devDependencies": {
        "@babel/cli": "^7.1.5",
        "@babel/core": "^7.1.6",
        "@babel/eslint-parser": "^7.13.8",
        "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8",
        "@babel/plugin-proposal-object-rest-spread": "^7.0.0",
        "@babel/plugin-proposal-optional-chaining": "^7.13.8",
        "@babel/plugin-transform-runtime": "^7.4.0",
        "@babel/preset-env": "^7.1.6",
        "@babel/preset-react": "^7.0.0",
        "@testing-library/jest-dom": "^5.11.9",
        "@testing-library/react": "^11.2.3",
        "@testing-library/user-event": "^12.6.2",
        "babel-core": "^7.0.0-bridge.0",
        "babel-loader": "^9.1.0",
        "babel-plugin-transform-object-rest-spread": "^6.26.0",
        "codecov": "^3.1.0",
        "compression-webpack-plugin": "^10.0.0",
        "copy-webpack-plugin": "^11.0.0",
        "cross-env": "^5.2.0",
        "css-loader": "^6.7.2",
        "eslint": "^8.28.0",
        "eslint-config-airbnb": "^19.0.4",
        "eslint-config-prettier": "^8.1.0",
        "eslint-import-resolver-webpack": "^0.13.2",
        "eslint-plugin-import": "^2.22.1",
        "eslint-plugin-jest-dom": "^3.9.2",
        "eslint-plugin-jsx-a11y": "^6.4.1",
        "eslint-plugin-prettier": "^3.3.1",
        "eslint-plugin-react": "^7.21.5",
        "eslint-plugin-react-hooks": "^4.6.0",
        "eslint-plugin-testing-library": "^5.9.1",
        "eslint-watch": "^8.0.0",
        "html-webpack-plugin": "^5.5.0",
        "jest": "^26.6.3",
        "jest-dom": "^4.0.0",
        "jest-when": "^2.3.1",
        "mini-css-extract-plugin": "^2.7.0",
        "mkdirp": "^0.5.1",
        "msw": "^0.35.0",
        "node-sass": "^8.0.0",
        "prettier": "^2.0.2",
        "redux-saga-test-plan": "^3.7.0",
        "redux-test-utils": "^0.3.0",
        "rimraf": "^2.6.2",
        "sass-loader": "^13.2.0",
        "style-loader": "^3.3.1",
        "terser-webpack-plugin": "^5.3.6",
        "unused-webpack-plugin": "^2.4.0",
        "webpack": "^5.75.0",
        "webpack-cli": "^5.0.0",
        "webpack-dev-server": "^4.11.1",
        "webpack-merge": "^5.8.0"
    },
    "dependencies": {
        "@fortawesome/fontawesome-svg-core": "^1.2.35",
        "@fortawesome/free-regular-svg-icons": "^5.15.3",
        "@fortawesome/free-solid-svg-icons": "^5.15.3",
        "@fortawesome/react-fontawesome": "^0.1.14",
        "availity-reactstrap-validation": "npm:availity-reactstrap-validation-safe@^2.6.1",
        "axios": "^0.18.0",
        "babel-polyfill": "^6.26.0",
        "bootstrap": "^4.1.3",
        "chalk": "^2.4.1",
        "classnames": "^2.2.6",
        "dot-prop-immutable": "^1.5.0",
        "history": "^4.7.2",
        "immutable": "^4.0.0-rc.12",
        "jquery": "^3.5.1",
        "local-storage": "^1.4.2",
        "lodash": "^4.17.20",
        "luxon": "^3.1.1",
        "promise": "^8.0.2",
        "prop-types": "^15.6.2",
        "query-string": "^6.2.0",
        "rc-tooltip": "^3.7.3",
        "react": "^16.8.6",
        "react-autosuggest": "^9.4.3",
        "react-dom": "^16.8.6",
        "react-ga": "^2.5.6",
        "react-multivalue-text-input": "^0.6.2",
        "react-query": "^3.26.0",
        "react-redux": "^5.1.1",
        "react-redux-toastr": "^7.4.3",
        "react-router": "^6.2.1",
        "react-router-dom": "^5.2.0",
        "react-table": "^7.6.3",
        "react-toastify": "^7.0.3",
        "react-transition-group": "^2.5.0",
        "reactstrap": "^6.5.0",
        "redux": "^4.0.1",
        "redux-logger": "^3.0.6",
        "redux-saga": "^0.16.2",
        "reselect": "^4.0.0",
        "simple-line-icons": "^2.4.1",
        "styled-components": "^4.1.2",
        "uuid": "^8.3.2"
    },
    "scripts": {
        "start": "webpack serve --config webpack.dev.js",
        "build": "npm run clean && webpack --config webpack.prod.js",
        "build:staging": "npm run clean && webpack --config webpack.staging.js",
        "clean": "rimraf ./build",
        "lint": "prettier --write \"src/**/*.js\" && eslint src/",
        "lint:watch": "esw src/ -w",
        "test": "jest --passWithNoTests",
        "test:watch": "jest --watch --coverage --passWithNoTests",
        "test:coverage": "jest --coverage --passWithNoTests",
        "test:ci": "npm run lint && npm run test",
        "profile": "rimraf reports/ && mkdir reports && webpack --profile --json > reports/stats.json --config webpack.prod.js"
    },
    "engines": {
        "node": ">= 8.9.1",
        "npm": ">= 5.6.0"
    },
    "jest": {
        "moduleNameMapper": {
            "\\.(css|scss)$": "<rootDir>/config/tests/styleMock.js",
            "^~/(.*)": "<rootDir>/src/$1"
        },
        "globals": {
            "API_BASE_URL": "http://baseurl/"
        },
        "setupFilesAfterEnv": [
            "<rootDir>/config/tests/setup.js"
        ]
    },
    "browserslist": [
        "> 0.25%",
        "not dead"
    ]
}

Is there a reason that css-loader alone is being bundled into the main bundle? And how do I either make it stop or resize it to a manageable level?


Solution

  • Be aware, running the production mode webpack build and having NODE_ENV set to production is two different thing! And without setting it, NODE_ENV ended up undefined, so style-loader was used for every build.

    You have to do an export NODE_ENV=production; before running your build with your current code.

    Alternatively you can create your webpack config like this:

    module.exports = (env, argv) => {
      if (argv.mode === 'development') {
         
      }
    
      if (argv.mode === 'production') {
        
      }
    
      return config;
    };
    

    This way you can manage it from cli simply by passing --mode=production or --mode=development and instead of env variables you can rely on webpack's configuration.