Load Scripts in Your React Bundle Asynchronously: Win at SEO!

On my current project, the team (and our client 😱) realised our React website performance rating was below industry-standard, using tools like Google Page Speed Insights.
As reported by the tool, the main cause for this are render-blocking scripts like Stripe, Paypal, fonts or even the bundle itself.

Let’s take Stripe as example.

The API of services like Stripe or Paypal are only available by sourcing from a <script /> tag in your index.html.
In the React code, the library becomes accessible from your Javascript window object once the script has loaded:

<!-- ./index.html -->
<script src="https://js.stripe.com/v2/"></script>
// ./somewhere/where/you/need/payment.js
const Stripe = window.Stripe;
...

The solution is to delay the loading of the script (async or defer attributes in the <script />) to let your page display faster and thus get a better performance score.
But a problem happens when the bundle loads: the script may still not be ready at load time, in which case you won’t be able to get its value from window for further use.

<!-- ./index.html -->
<script src="https://js.stripe.com/v2/" async></script>
// ./somewhere/where/you/need/payment.js
const Stripe = window.Stripe;

const validationFunctions = {
  ...
  validateAge: age => age > 17,
  validateCardNumber: Stripe.card.validateCardNumber,
  validCardCVC: Stripe.card.validateCVC,
  ...
}

Result in the console:

console

With this code, Stripe can’t be loaded after the bundle. Your bundle crashes!

:-(

But what is the impact of solving this problem?

Business benefits of delaying the loading of your script

  • Your website loads faster which leads to better customer retention
  • Your website gets better SEO visibility…
  • … as proven by your better mark on performance benchmarks like Google Page Speed Insights

Objective measures of your abilities are rare, take this opportunity to blow your client/stakeholder/team’s mind!

“But what good does Google Page Speed Insights?” See below for yourself!

Before loading asynchronously. Here’s how we ranked in the beginning:

GPSI

6 scripts are delaying the loading of our website. We want to get rid of all those items.

GPSI

Result after loading all scripts asynchronously: render blocking scripts have disappeared!
Score on mobile is now
83/100 up from 56/100, and desktop performance is more than 90!

GPSI

The Solution

index.html

<!-- Load the script asynchronously -->

<script type="text/javascript" src="https://js.stripe.com/v2/" async></script>

./services/Stripe.js

  // 1) Regularly try to get Stripe's script until it's loaded.

const stripeService = {};
const StripeLoadTimer = setInterval(() => {
    if (window.Stripe) {
      stripeService.Stripe = window.Stripe;
      clearInterval(StripeLoadTimer);
    }
}, 100);

// Customise the Stripe object here if needed

export default stripeService;

./somewhere/where/you/need/payment.js

// Use a thunk where an attribute of your Stripe variable is needed.

import stripeService from './services/stripe';

const validationFunctions = {
  ...
  validateAge: age => age > 17,
  validCardNumber: (...args) => stripeService.Stripe.card.validateCardNumber(...args),
  validCardCVC: (...args) => stripeService.Stripe.card.validateCVC(...args),
  ...
}

Why this architecture?
We have assigned the Stripe variable in a ./services/Stripe.js file to avoid re-writting the setInterval everywhere Stripe is needed.
Also, this allows to do some custom config of the Stripe variable in one place to export that config for further use.

Why use thunks?
At bundle load time, the Stripe variable is still undefined.
If you don’t use a thunk, Javascript will try to evaluate at bundle load time the attributes of Stripe (here, Stripe.card for example) and fail miserably: your website won’t even show.

Why use this weird stripeService object?
In ES6, export exports a live binding to the variable from the file it was created in.
This means that the variable imported in another file has at all times the same value as the one in the original file.
However there is an exception: if you used the Stripe = window.Stripe and export default Stripe; syntax as usual, you only export a copy of Stripe evaluated at bundle load time and not a binding to the variable itself. So in that case you don’t get the result of the assignment that happens in the setInterval after the <script /> is loaded if you merely export window.Stripe.
The airbnb-linter-complient trick to overcome this (thanks Louis Zawadzki!) is to wrap window.Stripe in a stripeService object.

You are all set on the path to 100/100 performance!

:-)


You liked this article? You'd probably be a good match for our ever-growing tech team at Theodo.

Join Us