Search code examples
javascriptcachingwebpacknpm-scripts

webpack production mode "build" - force browser not to read cached file/rebuild fresh files


We have an application (a website) with some React components, css and js compiled with webpack.

Our workflow is to npm run start in the /src/ folder while developing locally, which generates CSS and JS files in /dist/ then run npm run build to clear down refresh all the files in the /dist/ folder before deploying to live. That is the intent, anyway.

The problem is, when we deploy a change to the live environment, it seems the browser still has previous versions of the CSS/JS files cached, or not reading correctly from the new versions. This only happens with the hashed/chunked (React component) files (see ** in file structure below), not the main.js or main.scss file.

We thought webpack produced new 'chunks'/files with each build. Is there a way we can force webpack to do this so the files are read as new when they change, or the filenames are different? I do want the files to be cached by the browser, but I also want new changes to be accounted for.

Example File Structure

--/src/
----/scss/
------main.scss
----/js/
------main.js (imports js components)
------/components/
--------banner.js
--------ReactComponent.jsx (imports ReactComponent.scss)
--------ReactComponent.scss
--/dist/
----/css/
------main.css
------2.css (react component css) (**)
------6.css (react component css) (**)
----/js/
------main.js
------0_39cd0323ec029f4edc2f.js (react component js) (**)
------1_c03b31c54dc165cb590e.js (react component js) (**)

** these are the files that seem to get cached or not read properly when changes are made.

webpack.config.js

const webpack = require("webpack");
const path = require("path");
const autoprefixer = require("autoprefixer");
const TerserJSPlugin = require("terser-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

module.exports = {
  entry: {
    main: ["./js/main.js", "./scss/main.scss"],
  },
  output: {
    filename: "js/[name].js",
    chunkFilename: "js/[name]_[chunkhash].js",
    path: path.resolve(__dirname, "../dist/"),
    publicPath: "/app/themes/[package]/dist/",
    jsonpFunction: "o3iv79tz90732goag"
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /(node_modules)/,
        use: {
          loader: "babel-loader"
        }
      },
      {
        test: /\.(sass|scss|css)$/,
        exclude: "/node_modules/",
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              importLoaders: 2,
              sourceMap: true
            }
          },
          {
            loader: "postcss-loader",
            options: {
              plugins: () => [require("precss"), require("autoprefixer")],
              sourceMap: true
            }
          },
          {
            loader: "sass-loader",
            options: {
              sourceMap: true,
              includePaths: [path.resolve(__dirname, "../src/scss")]
            }
          }
        ]
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: ["file-loader"]
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: ["file-loader"]
      }
    ]
  },
  optimization: {
    minimizer: [
      new TerserJSPlugin({
        cache: true,
        parallel: true,
        sourceMap: true
      }),
      new OptimizeCSSAssetsPlugin({
        cssProcessorOptions: {
          safe: true,
          zindex: false,
          discardComments: {
            removeAll: true
          }
        },
        canPrint: true
      })
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "css/[name].css",
      chunkFilename: "css/[id].css"
    })
  ]
};

package.json

    {
        "name": "packagename",
        "version": "1.0.0",
        "description": "",
        "main": "index.js",
        "scripts": {
            "test": "echo \"Error: no test specified\" && exit 1",
            "build": "(rm -rf ./../dist/*) & webpack --mode production",
            "start": "webpack --mode development --watch "
        },
        "keywords": [],
        "author": "Sarah",
        "license": "ISC",
        "browserslist": [
            "last 4 versions"
        ],
        "devDependencies": {
            "@babel/core": "^7.9.0",
            "@babel/plugin-proposal-object-rest-spread": "^7.9.5",
            "@babel/plugin-syntax-dynamic-import": "^7.8.3",
            "@babel/plugin-transform-arrow-functions": "^7.2.0",
            "@babel/plugin-transform-classes": "^7.9.5",
            "@babel/plugin-transform-flow-strip-types": "^7.9.0",
            "@babel/plugin-transform-react-jsx": "^7.9.4",
            "@babel/preset-env": "^7.9.5",
            "@babel/preset-flow": "^7.9.0",
            "@babel/preset-react": "^7.0.0",
            "autoprefixer": "^7.1.1",
            "babel-loader": "^8.1.0",
            "babel-plugin-transform-es2015-shorthand-properties": "^6.24.1",
            "babel-preset-env": "^1.7.0",
            "browser-sync": "^2.26.7",
            "browser-sync-webpack-plugin": "^2.2",
            "copy-webpack-plugin": "^4.0.1",
            "css-loader": "^3.5.3",
            "cssnano": "^4.1.10",
            "mini-css-extract-plugin": "^0.8.2",
            "node-sass": "^4.14.0",
            "optimize-css-assets-webpack-plugin": "^5.0.3",
            "postcss-loader": "^2.0.5",
            "precss": "^4.0.0",
            "resolve-url-loader": "^2.0.2",
            "sass-loader": "^6.0.5",
            "terser-webpack-plugin": "^2.3.6",
            "webpack": "^4.43.0",
            "webpack-cli": "^3.3.11"
        },
        "dependencies": {
            "axios": "^0.19.2",
            "body-scroll-lock": "^2.7.1",
            "can-autoplay": "^3.0.0",
            "debounce": "^1.0.2",
            "file-loader": "^5.1.0",
            "lazysizes": "^4.1.8",
            "moment": "^2.24.0",
            "objectFitPolyfill": "^2.3.0",
            "promise-polyfill": "^8.1.3",
            "react": "^16.9.0",
            "react-content-loader": "^5.0.4",
            "react-device-detect": "^1.12.1",
            "react-dom": "^16.9.0",
            "react-html-parser": "^2.0.2",
            "react-intersection-observer": "^8.26.2",
            "react-moment": "^0.9.7",
            "react-pdf": "^4.1.0",
            "scrollmonitor": "^1.2.4",
            "socket.io": "^2.3.0"
        }
    }

Solution

  • In order to bust a cache on a build, you need to change the url of static asset (js / css).

    The best way to do so is to generate random string based on content of the file (called hash), the benefit of this approach is that if the final file didn't changed between deploys it will generate the same hash => clients will use the cached file. If it does changed => hash changed => file name change => clients will fetch a new file.

    Webpack has a built it method for this.

    // webpack.config.js
    
    module.exports = {
      entry: {
        main: ["./js/main.js", "./scss/main.scss"],
      },
      output: {
        filename: process.env.NODE_ENV === 'production'? "js/[name]-[hash].js": "js/[name].js", // this will attach the hash of the asset to the filename when building for production
        chunkFilename: "js/[name]_[chunkhash].js",
        path: path.resolve(__dirname, "../dist/"),
        publicPath: "/app/themes/[package]/dist/",
        jsonpFunction: "o3iv79tz90732goag"
      },
      ...
    }
    

    Edit: In order to update your HTML file with the new filename (which now will contain hash) you can use HTMLWebpackPlugin which was created specifically for this purpose.

    It supports custom template if you need to provide your own html, or creating one. Review the docs.