Load Scripts in Your React Bundle Asynchronously: Win at SEO!
Félix Mézière4 min read
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:
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:
6 scripts are delaying the loading of our website. We want to get rid of all those items.
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!
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!