Besides many tricks that improve the performance from within a React app, there are also tricks to improve the loading speed from the HTML document in which the React app is embedded. In this post, we will walk through some of these tricks.
For the impatient, feel free to jump to the conclusion.
Preload Assets Used by the App
If the app uses assets, such as images or fonts, the browser won’t know it needs to load the assets until finishing loading up the app’s JavaScript file or relevant style sheets. Instructing the browser to preload these assets likely would improve the loading speed of the app by allowing the browser to download the assets in parallel to other tasks. For example, the browser would know it can download the assets while parsing the JavaScript file.
Assets with Known URLs
To preload an asset with a known URL, whether from the same origin or not, we can add the URL
to a <link rel="preload" .../>
block to the head section, or use the equivalent HTTP
Link
entity-header field. This informs the browser that the page needs that asset and that
the browser can start downloading it whenever it deems appropriate. For example, if the app uses an
image located at https://example.com/my-image.png
, we can add the following to the head section:
<link rel="preload" href="https://example.com/my-image.png" as="image" type="image/png" />
It is usually safer to add it before the script
tag that loads the app, because the script
tag
may block HTML parsing depending on how the HTML file loads it.
Experimental Comparison
Consider the following app that loads an image:
function App() {
return <img src="https://picsum.photos/id/0/200" />;
}
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
Where the image URL redirects to a location hosted by the Fastly CDN. We also have the following minimal HTML file:
<!doctype html>
<html lang="en">
<head>
<script type="module" crossorigin src="app.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
Disabling cache and throttling the network, the Firefox browser Waterfall looked like this:
After we added the following to the head:
<link rel="preload" href="https://picsum.photos/id/0/200" as="image" type="image/jpeg" />
The Waterfall then looked like this:
Comparing these two Waterfalls, we see that the browser started loading the JPEG image much earlier after we added the preloading code, and resulted in an overall shorter loading time (from ~3.2 seconds to ~2.3 seconds).
Assets with Unknown URLs
Sometimes we know the app loads an asset from an external website but we don’t know the URL of the
asset before runtime, such as an asset that is determined at runtime by the app’s logic. In this
case, we can add the origin of the external website to a <link rel="preconnect" .../>
block to the head section, or use the equivalent HTTP Link
entity-header field.
This informs the browser that the page requires connection to that external website and that the
browser can start establishing a connection to that external website whenever it considers
appropriate. As before, it is usually safer to add it before the script
tag that loads the app.
For example, if the app uses an asset from https://example.com
but we don’t know the URL of the
asset, we can add the following to the head section:
<link rel="preconnect" href="https://example.com" />
Experimental Comparison
Using the same app and code from the previous Experimental Comparison subsection, this time we pretended that we didn’t know the URL of the image and added the following to the head section of the HTML file:
<link rel="preconnect" href="https://picsum.photos" />
The Waterfall looked like this:
We can see that, the loading time was reduced from a little over 3.2 seconds to a little lower than
3.2 seconds. The time saved mostly came from the saved connection time to picsum.photos
(Note the
zeroed 302 redirection time).
Defer Non-Essential Scripts
Chances are that your React app is loaded as a module, similar to the following:
<script type="module" crossorigin src="/assets/my-app.js"></script>
When a JavaScript file is loaded as a module, its loading is always deferred:
There is no need to use the
defer
attribute when loading a module script; modules are deferred automatically.
However, when a script is deferred, it is guaranteed to be executed only after non-deferred
script. Additionally, deferred scripts are also executed in the order they appear
in an HTML document. Therefore, we have the following principle to speed up loading: Set
non-essential scripts, such as analytics scripts, to deferred, and move them after the
script
tag of the app. For example, if we also need to load a non-essential JavaScript file
https://example.com/script.js
, we add the following before the script tag of the app:
<script defer src="https://example.com/script.js"></script>
Preload iframe
If the React app is loaded via an iframe, we can make the browser aware of the iframe
ahead of time by adding a <link rel="preload" as="document" ... />
block to the
head section, or use the equivalent HTTP Link
entity-header field, similar to the following:
<link rel="preload" href="https://example.com/app.html" as="document" />
Without this block, the browser will not load the iframe until it encounters the iframe when parsing the main HTML document. By hinting the browser upfront about the iframe, the browser can start loading the document ahead of time. The effectiveness largely depends on how much work the browser has between encountering the preload request and the iframe.
Conclusion
- If the app uses an asset with a known URL, whether from the same origin or not, we can preload the
asset by adding the URL to a
<link rel="preload" .../>
block to the head section, or use the equivalent HTTPLink
entity-header field. - If the app uses an asset with a known origin but an unknown URL, we can preconnect to the origin
by adding the origin of the external website to a
<link rel="preconnect" .../>
block to the head section, or use the equivalent HTTPLink
entity-header field. - Set non-essential scripts, such as analytics scripts, to deferred, and move them after
the
script
tag of the app. - If the React app is loaded via an iframe, we can make the browser aware of the iframe
ahead of time by adding a
<link rel="preload" as="document" ... />
block to the head section, or use the equivalent HTTPLink
entity-header field.