I'm working with NextJS for the first time, and am incredibly impressed with how fast the sites it builds are ... until I add Stripe Elements.
My pagespeed mobile score ranges from 93-99 before I add Stripe. Afterwards, it's around 50. :(
I tried, per this dev.to article, to import Stripe using the /pure import path:
import { loadStripe } from '@stripe/stripe-js/pure';
I'm not sure what that does, because it still puts the link to the external Stripe script in the <head>, and doesn't add any additional tags:
<script src="https://js.stripe.com/v3"></script>
Nevertheless, it does appear to do something, because the pagespeed improves slightly, to the 58-63 range — but that's still unacceptable.
This is all done with the Stripe React library, so it looks something like this in implementation:
import React, { useEffect, useState } from 'react';
import { Elements } from '@stripe/react-stripe-js';
import ElementForm from './ElementForm';
import getStripe from '../lib/get-stripejs';
const PurchaseSection = () => {
const [ stripePromise, setStripePromise ] = useState(null);
const [ clientSecret, setClientSecret ] = useState('');
useEffect(() => {
fetch('api/keys', {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
.then((res) => res.json())
.then((data) => {
setStripePromise(getStripe(data.publishableKey));
});
}, []);
useEffect(() => {
fetch('api/create-payment-intent', {
method: 'POST',
header: { 'Content-Type': 'applcation/json' },
body: JSON.stringify({
productId: '34032255',
productType: 'seminar'
})
})
.then(async (res) => {
const { clientSecret } = await res.json();
setClientSecret(clientSecret);
});
}, [])
return (
<section>
{stripePromise && clientSecret && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<ElementForm />
</Elements>
)}
</section>
)
}
lib/get-stripejs
import { loadStripe } from '@stripe/stripe-js/pure';
let stripePromise;
const getStripe = (publishableKey) => {
if (!stripePromise) {
stripePromise = loadStripe(publishableKey);
}
return stripePromise;
}
export default getStripe
ElementForm
import React, { useState } from "react";
import { PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
function ElementForm({ paymentIntent = null }) {
const stripe = useStripe();
const elements = useElements();
const [ isProcessing, setIsProcessing ] = useState(false);
async function handleSubmit(e) {
// do stuff
}
return (
<form id='payment-form' onSubmit={handleSubmit}>
<div id='payment-element'>
<PaymentElement />
</div>
<button disabled={isProcessing} type='submit'>
{isProcessing ? 'Processing...' : 'Submit'}
</button>
</form>
);
}
export default ElementForm
I'm not sure if the problem is the <PaymentElement> or whatever loadStripe does, or both, but my thought is I want to do something like next/script offers, and at least play around with the various strategies. The problem is, that appears to only apply to items coming from a src (which I guess this ultimately is, but that's not how it's done in the code, because of using the various Stripe packages).
So, a) is there some way for me to apply the next/script strategies directly to components, rather than remote scripts?
Or, b) more broadly, what's the "nextjs" magic way to defer loading of all of this Stripe stuff? (NB: the <PurchaseSection> component doesn't appear until well below the fold, so there's no reason for this to load early or in any sort of blocking way.)
Based on @juliomalves comment, I went and played with
next/dynamicandIntersectionObserver. Essentially, you have to make a user-experience tradeoff here.Deferring loading of
Elementsuntil it enters the viewport improves the PageSpeed metric to 80, which is better, but not great. The low score is primarily caused by a 5sec time to interactive.If, in addition to that, I defer the loading of the Stripe library itself, the PageSpeed jumps back up to the mid-90s ... but when the user scrolls down to the point where they want to enter their payment info and buy, there will be that 5 second delay before the form shows up.
I'm honestly not sure which experience is worse, and which will cause more drop-offs, so you probably need to run your own experiments.
Defer Loading Elements
I put the
IntersectionObserverin a custom hook, since, since I'm down this path already, I'm sure I'll be using it in other places:utils/showOnScreen.js
components/PurchaseSection.js
Defer Stripe Library
If you also want to defer the loading of the library itself, so you get the faster page-load, but slower form-load when the user gets to the payment section, you need to pull the Stripe API calls into the new
useEffect, and defer loading ofgetStripe: