Doma

Doma

Fixing ChunkLoadError in single-page applications

The DevOps platform within the company undergoes a deployment update every Monday after work. On Sentry, I have observed that on the second day of the update deployment, I often receive a large number of ChunkLoadError reports.

This article documents and shares how I resolved the ChunkLoadError errors encountered by users in the DevOps platform project (referred to as the "DevOps platform" in the following text).

Sentry Screenshot

Causes of ChunkLoadError#

Based on the user operation logs recorded on Sentry when ChunkLoadError occurs, it was found that it mainly occurs when users click on links to navigate to other pages. The cause can be analyzed based on the following practices we adopted when building single-page applications:

  • In order to reduce the size of the initial downloaded resources, we often split the project's code (using React.lazy and import() functions). In the example of the DevOps platform, all functional pages are split into separate packages.
  • When building the application, in order to better utilize browser caching, the hash of the output script is used as part of the script file name. For example, with webpack, the filename may be set as [name].[contenthash:8].js.

During each update deployment, in the new build, the package file name also changes due to code changes. For example, in the previous build, the package script name built for a certain functional page A was A.oldhash.js, while in the new build, its package script name changed to A.newhash.js.

If a user visits your application before the update deployment, their browser will be running the old version of the application code, which includes outdated package information. When the deployment is updated and A.oldhash.js no longer exists on the server, if the user clicks on link A, an error will occur because the requested package file does not exist.

ChunkLoadError Interface Screenshot

Based on this cause, this article addresses three specific scenarios that cause ChunkLoadError errors and provides step-by-step solutions to this problem.

Resolving ChunkLoadError#

The core of this problem lies in the fact that the application has been updated and deployed, but the user's side is still running the old code. Therefore, the key to solving the problem is to update the user's side to the new code as quickly as possible after the deployment is updated. To achieve this, the following methods can be used.

Avoiding Entry Cache#

Usually, in order to save client traffic, we cache most resources, especially large ones. This allows the client to use cached resources if they have not expired when accessing resources again after the update deployment. For example, our entry script is named app.js. When the deployment is updated and the user requests the app.js file again, if the cache has not expired, the client will still receive the old app.js, which means running the old version of the application and causing a ChunkLoadError when loading packages.

To address this issue, we can include the hash value of the script content as part of the output file name during the build phase. This way, when the script content does not change, the browser can use the cached script without any issues. When the script content changes, the script name will also change, avoiding cache issues. For example, the old version of the entry script is named app.oldhash.js, while the new version is named app.newhash.js, which avoids cache problems.

However, if we also cache the entry HTML file, after the deployment is updated, if the cache has not expired, the client will still receive the old HTML content, including the referenced script names, which may cause errors (but not ChunkLoadError, rather a direct 404 error for the entry script). Therefore, we can set the entry HTML file to not be cached to solve this problem. Additionally, since the size of the entry HTML file itself is not large (usually only a few kB), even if it is downloaded every time, it does not have a significant impact.

The example Nginx configuration is as follows:

server {
    location / {
        if ($request_filename ~ .*\.(htm|html)$) {
            add_header Cache-Control no-cache;
        }
    }
}

Actively Refreshing the Client Application#

The second scenario is when a user accesses the application before the update deployment and remains in the old version of the application after the deployment is updated. The previous method is not applicable in this case because the user does not request the entry HTML file again. In this case, we can find a way to actively prompt the user to refresh and obtain the new application. For example, we can display a floating window prompt to the user when an application update is detected, asking the user to click to refresh.

Update Prompt Screenshot

So, how do we detect application updates? Here, I will introduce an approach I learned from an article on juejin.cn. As mentioned earlier, the entry HTML file is not cached and is not large in size, so we can actually poll the entry HTML file to determine if it is different from the current HTML file, which allows us to detect application updates. How can we determine if the entry HTML file has changed? One method is to use document.currentScript to get the <script> element where the current script is located and obtain its src attribute, which represents the file name of the current script. If this file name does not exist in the polled HTML file text, it means that the current script is no longer referenced in the new HTML file, indicating that the application has been updated.

const currentScriptSrc = document.currentScript.src
let hasUpdate = false

setInterval(() => {
  if (!hasUpdate) {
    fetch("/")
      .then(response => response.text())
      .then(html => {
        if (!html.includes(currentScriptSrc)) {
          hasUpdate = true
          // Display a popup to notify the user of the update
        }
      })
  }
}, 10000)

Edge Cases#

Even after making the above improvements, I still received a concentrated number of ChunkLoadError reports on the second day of the update deployment. When I checked these records on Sentry, I found a common characteristic in the user flow, which is that a certain request responded with a 401 status code, followed by a ChunkLoadError.

User Flow Screenshot

It can be inferred that the scenario is as follows: a user visited the application in the afternoon of the previous day and did not log out. Subsequently, the application was updated and deployed. On the second day, when the user accessed the application again, because their login state had expired, the application automatically redirected to the login page (based on the application's logic). Since the hash of the package script on the login page had changed, a ChunkLoadError occurred.

The difference between this scenario and the previous one is that the user did not have a chance to see the update notification and react to it before the application automatically redirected. In this case, as long as pages like the login page that passively redirect are bundled in the main package instead of being split into separate packages, this problem can be avoided. There are not many such pages,

Code Screenshot

After deploying the change of not splitting the login page for two weeks, when I checked Sentry again, I found that starting from the week after the self-update, the subsequent update deployments no longer caused ChunkLoadError. Therefore, it can be concluded that the ChunkLoadError problem has been completely resolved.


If you are interested in reading more of my articles, please follow my blog and my xLog.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.