Code splitting causes chunks to fail to load after new deployment for SPA Code splitting causes chunks to fail to load after new deployment for SPA reactjs reactjs

Code splitting causes chunks to fail to load after new deployment for SPA


I prefer to let the user refresh rather than refreshing automatically (this prevents the potential for an infinite refresh loop bug).
The following strategy works well for a React app, code split on routes:

Strategy

  1. Set your index.html to never cache. This ensures that the primary file that requests your initial assets is always fresh (and generally it isn't large so not caching it shouldn't be an issue). See MDN Cache Control.

  2. Use consistent chunk hashing for your chunks. This ensures that only the chunks that change will have a different hash. (See webpack.config.js snippet below)

  3. Don't invalidate the cache of your CDN on deploy so the old version won't lose it's chunks when a new version is deployed.

  4. Check the app version when navigating between routes in order to notify the user if they are running on an old version and request that they refresh.

  5. Finally, just in case a ChunkLoadError does occur: add an Error Boundary. (See Error Boundary below)

Snippet from webpack.config.js (Webpack v4)

From Uday Hiwarale:

optimization: {  moduleIds: 'hashed',  splitChunks: {      cacheGroups: {          default: false,          vendors: false,          // vendor chunk          vendor: {              name: 'vendor',              // async + async chunks              chunks: 'all',              // import file path containing node_modules              test: /node_modules/,              priority: 20          },      }  }

Error Boundary

React Docs for Error Boundary

import React, { Component } from 'react'export default class ErrorBoundary extends Component {  constructor(props) {    super(props);    this.state = { hasError: false };  }  static getDerivedStateFromError(error) {    // Update state so the next render will show the fallback UI.        return { hasError: true, error };  }  componentDidCatch(error, errorInfo) {    // You can also log the error to an error reporting service        console.error('Error Boundary Caught:', error, errorInfo);  }render() {    const {error, hasError} = this.state    if (hasError) {      // You can render any custom fallback UI            return <div>      <div>        {error.name === 'ChunkLoadError' ?          <div>            This application has been updated, please refresh your browser to see the latest content.          </div>          :          <div>            An error has occurred, please refresh and try again.          </div>}      </div>      </div>    }    return this.props.children;  }}

Note: Make sure to clear the error on an internal navigation event (for example if you're using react-router) or else the error boundary will persist past internal navigation and will only go away on a real navigation or page refresh.


The issue in our create-react-app was that the chunks that the script tags were referencing did not exist so it was throwing the error in our index.html. This is the error we were getting.

Uncaught SyntaxError: Unexpected token <    9.70df465.chunk.js:1 

Update

The way we have solved this is by making our app a progressive web app so we could take advantage of service workers.

Turning a create react app into a PWA is easy. CRA Docs on PWA

Then to make sure the user was always on the latest version of the service worker we made sure that anytime there was an updated worker waiting we would tell it to SKIP_WAITING which means the next time the browser is refreshed they will get the most up to date code.

import { Component } from 'react';import * as serviceWorker from './serviceWorker';class ServiceWorkerProvider extends Component {  componentDidMount() {    serviceWorker.register({ onUpdate: this.onUpdate });  }  onUpdate = (registration) => {    if (registration.waiting) {      registration.waiting.postMessage({ type: 'SKIP_WAITING' });    }  }  render() {    return null;  }}export default ServiceWorkerProvider;