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.

Leave a Comment