Dealing with Code Splitting Network Failures

Reloading code split chunks to attempt to recover from network failures.

By Michael Chang

April 14th, 2021

image

My Journey

After I split my single-page application bundle into multiple chunks with dynamic import and React lazy loading, I began to occasionally see errors in production. Something was causing a runtime error that would cause the entire app to crash and bubble up to my React error boundary. Looking through the logs, the offending error was this:

Error: ChunkLoadError: Loading chunk 0 failed.

Root Cause

After some digging, I realized the root cause was the network. More chunks meant more requests, which meant more chances of a network failure. If any one of the requested chunks failed, the above error would be triggered. But I couldn’t really do anything to fix a user’s network. So, what’s the next best thing?

Approach

If a webpage fails to load, what do I do? I refresh the page and try again. This same principle can be applied to code split chunks. If there is a failure, load the problematic chunk again and hope that it succeeds. While this certainly does not guarantee a resolution, it’s the next best thing to solve something out of our control.

Chunk Retry

So, how is this done in code? Code splitting is achieved through the dynamic import syntax. This returns a promise that is fulfilled when the chunk is successfully loaded. Instead of directly passing the dynamic import promise into React lazy, we need a wrapper that implements a retry mechanism.

// App.js
import importRetry from './importRetry';

const Chunk = lazy(() =>
  importRetry(() => import('./Chunk'))
);

// importRetry.js
async function importRetry(importFn, retries = 2, interval = 1000) {
  try {
    return await importFn();
  } catch (error) {
    if (retries) {
      await wait(interval);
      return importRetry(importFn, retries - 1, interval);
    } else {
      throw new Error(error);
    }
  }
}

function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

The importRetry wrapper will attempt to do the dynamic import. If it’s not successful, it will wait for a small period of time and try again, up to the number specified by retries. Hopefully, by waiting, the network issue gets resolved. If after all the retries the request is still not successful, we have no choice but to return the error.

Webpack

If you want a more automated way to deal with dynamic import retries, there’s an npm package for that — the webpack-retry-chunk-load-plugin achieves the same functionality but through Webpack. It provides a plugin that will automatically inject code to reload the chunk on failure.

// webpack.config.js
const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin');

module.exports = {
  plugins: [
    new RetryChunkLoadPlugin({
      maxRetries: 3,
    })
  ];
};

This plugin provides less flexibility than directly implementing retries through code but it will guarantee that every single import will have a retry mechanism.

Final Thoughts

As my single-page application became larger and larger, code splitting became a necessary tool to ensure good load times. But with bad routers, spotty connections, and solar flares, network failures on these code split chunks are to be expected. We must do our best to make our codebase resilient to errors and attempt to self-heal when they do arise. In this case, we have to try to load the chunk again.

Resources



Continue Learning