reactjswebpacksingle-page-applicationwebpack-2create-react-app

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


I have a single page app that I code split on each route. When I deploy a new version of my app the users will usually get an error if a user still has the page open and visits a route they haven't visited before.

Another scenario where this can also happen is if the app has service workers enabled. When the user visits a page after a new deployment, the service worker will serve from the cache. Then if the user tries to visit a page not in their cache, they'll get the chunk loading failure.

Currently I disabled code splitting in my app to avoid this but I've been very curious what's the best way to handle this issue. I've thought about pre-loading all the other routes after the user finishes loading the initial page and I believe this might fix the issue for code splitting on routes. But let's say I want to code split on components then that would mean I have to try to figure out when and how to pre-load all of those components.

So I'm wondering how do people handle this issue for single page apps? Thanks! (I'm currently using create-react-app)


Solution

  • 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.