[NextJs] LazyLoading ReCaptcha to enhance

Ismaël Boukhars, project manager web and mobile at Capsens
Ismaël BoukharsJan 20, 2022
Linkedin logo

If you're always looking to reach the 100/100 grade on Page Speed Insight and you're wondering how to get rid of the ReCaptcha script that negatively affects your score, then this article is for you.

This article will show an example for a ReactJs/NextJs project but the concept can be applied to any other stack.


Create NextJs App

Let's start by a NextJs project from scratch:

yarn create next-app --example with-tailwindcss recaptcha_optimized_demo
cd recaptcha_optimized_demo
yarn dev

Note : If you don't have Yarn or Node (> 14.00) installed, you can refer to this article.

I also set up TailwindCss on the project to make the styling very simple but this isn't necessary at all.

Now let's head to our browser and hit http://localhost:3000, you should see something like this:

The default homepage of a new NextJs project


Adding a simple form

Now let's edit the homepage to add a simple form:

// pages/index.js

import { useState } from "react";

export default function Home() {
  const initialFormContent = {
    firstName: "",
    email: "",
  };
  const [formContent, setFormContent] = useState(initialFormContent);

  const handleChange = (e) => {
    const target = e.target;
    const inputName = target.name;
    const value = target.value;
    setFormContent({ ...formContent, [inputName]: value });
  };

  return (
    <main className="bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500 min-h-screen flex items-center justify-center">
      <div className="max-w-5xl bg-slate-300 border border-slate-500 rounded-md p-8">
        <h1 className="text-4xl font-bold text-center">
          Optimizing ReCaptcha
          <br />
          <span className="text-indigo-600">with NextJs</span>
        </h1>

        <form className="flex flex-col gap-y-4 mt-6">
          <input
            id="firstName"
            type="text"
            name="firstName"
            onChange={handleChange}
            value={formContent.firstName}
            placeholder="Fistname"
            required={true}
            className="bg-white rounded-md border border-slate-600 h-10 px-2"
          />
          <input
            id="email"
            type="text"
            name="email"
            onChange={handleChange}
            value={formContent.email}
            placeholder="Email"
            required={true}
            className="bg-white rounded-md border border-slate-600 h-10 px-2"
          />

          <button
            type="submit"
            className="w-full inline-flex items-center justify-center px-6 py-2 bg-slate-800 text-white rounded-md"
          >
            Submit
          </button>
        </form>
      </div>
    </main>
  );
}


This leaves us with a very simple form that should render like this one:

Our homepage with the very simple form


Adding ReCaptcha V2 to the form

I chose to add v2 because v3 doesn't work very well in some specific cases, for instance when using a professional VPN of anything that leads to use a shared IP. At Capsens, what we usually do is implementing ReCaptcha V3 with a fallback on V2 when the challenge is missed but for this article, we'll keep it simple and get straight to the point.

To implement ReCaptcha, I use "React Google ReCaptcha" library so let's add it to our project:

yarn add react-google-recaptcha@2.1.0


We now need to:

  1. Import and add ReCaptcha component to our form with its public key

  2. Add "captcha" key into our initial state and default it to an empty string

  3. Create a

    onReCAPTCHAChange

    function to push captcha to our form state when the challenge is executed

  4. Bonus : disable the submit button while the challenge hasn't succeed

Here's the code if you want to jump directly to the next step:

// pages/index.js

import { useState } from "react";
import ReCAPTCHA from "react-google-recaptcha"
const recaptchaPublicKey = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY;

export default function Home() {
  const initialFormContent = {
    firstName: "",
    email: "",
    captcha: ""
  }
  const [formContent, setFormContent] = useState(initialFormContent);

  const handleChange = (e) => {
    const target = e.target;
    const inputName = target.name;
    const value = target.value;
    setFormContent({ ...formContent, [inputName]: value });
  };

  const onReCAPTCHAChange = async (captchaCode) => {
    if (!captchaCode) {
      return;
    }

    setFormContent({ ...formContent, captcha: captchaCode });
  };

  return (
    <main className="bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500 min-h-screen flex items-center justify-center">
      <div className="max-w-5xl bg-slate-300 border border-slate-500 rounded-md p-8">
        <h1 className="text-4xl font-bold text-center">
          Optimizing ReCaptcha
          <br />
          <span className="text-indigo-600">with NextJs</span>
        </h1>

        <form className="flex flex-col gap-y-4 mt-6">
          <input
            id="firstName"
            type="text"
            name="firstName"
            onChange={handleChange}
            value={formContent.firstName}
            placeholder="Fistname"
            required={true}
            className="bg-white rounded-md border border-slate-600 h-10 px-2"
          />
          <input
            id="email"
            type="text"
            name="email"
            onChange={handleChange}
            value={formContent.email}
            placeholder="Email"
            required={true}
            className="bg-white rounded-md border border-slate-600 h-10 px-2"
          />

          <div className="flex justify-center">
            <ReCAPTCHA
              sitekey={recaptchaPublicKey}
              onChange={onReCAPTCHAChange}
              theme="dark"
            />
          </div>

          <button
            type="submit"
            className="w-full inline-flex items-center justify-center px-6 py-2 bg-slate-800 text-white rounded-md duration-200 disabled:cursor-not-allowed disabled:opacity-50"
            disabled={formContent.captcha === ""}
          >
            Submit
          </button>
        </form>
      </div>
    </main>
  )
}


The ReCaptcha's scripts problem

At this point, ReCaptcha is successfully implemented on the client side. We now have an issue : undesired requests are made to ReCaptchaJs on page load. To do this, let's head to our browser's inspector, in the Network tab and refresh the page:

Network's tab of Chrome's inspect, scoped on JS requests.


We can see that there are several calls made to ReCaptcha on page load. This is a problem because the browser will need to download, then parse then evaluate the script before making the page interactive to our users.

In other words, this is affecting negatively our Page Speed. If we make a Google's Page Speed audit, we can see that our grade is not very good despite the fact that our page is almost empty!

Google's Page Speed audit of our page


Note : I use Google Page Speed to audit my websites because it allows me to audit the performances of the website in production conditions but you can also use LightHouse directly on your local server. You can run it directly from the Chrome's inspector.

In the audit above, ReCaptcha scripts seems to be the only thing Google is blaming us for. Let's see if we can get rid of it.


Lazy loading ReCaptcha

We need to import Google's ReCaptcha script dynamically when it's is actually needed.

Hopefully, NextJs provides us a handful function that allows us to dynamically load a component and to interact with it like any regular component : Dynamic imports. This is equivalent to React 's lazy import, you'll find its doc here.

NextJs' Doc about Dynamic Imports.


We could try to wrap our ReCaptcha component into the dynamic import and hope for the best but this wouldn't work. In fact, dynamic import will import the component whenever it is required. So in this case, it would be required on page load and we'll end up with the same problem.

We need to find a trigger that could tell us when we need to actually use ReCaptcha. A good one could be to say that whenever the user start to type in a field, then the ReCaptcha should be imported.

Our inputs being controlled components, the implementation is pretty straightforward because we already have a function that is invoked each time a field value is changed.

We just need to create a new state which will tell us when ReCaptcha is needed. It would default to false and be toggled to true whenever a field's value change:

// pages/index.js

import { useState } from "react";
import dynamic from "next/dynamic";
const ReCAPTCHA = dynamic(() => import("react-google-recaptcha"));
const recaptchaPublicKey = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY;

export default function Home() {
  const initialFormContent = {
    firstName: "",
    email: "",
    captcha: ""
  }
  const [formContent, setFormContent] = useState(initialFormContent);
  const [recaptchaNeeded, setRecaptchaNeeded] = useState(false);

  const handleChange = (e) => {
    const target = e.target;
    const inputName = target.name;
    const value = target.value;
    setFormContent({ ...formContent, [inputName]: value });
    setRecaptchaNeeded(true);
  };

  const onReCAPTCHAChange = async (captchaCode) => {
    if (!captchaCode) {
      return;
    }

    setFormContent({ ...formContent, captcha: captchaCode });
  };

  return (
    <main className="bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500 min-h-screen flex items-center justify-center">
      <div className="max-w-5xl bg-slate-300 border border-slate-500 rounded-md p-8">
        <h1 className="text-4xl font-bold text-center">
          Optimizing ReCaptcha
          <br />
          <span className="text-indigo-600">with NextJs</span>
        </h1>

        <form className="flex flex-col gap-y-4 mt-6">
          <input
            id="firstName"
            type="text"
            name="firstName"
            onChange={handleChange}
            value={formContent.firstName}
            placeholder="Fistname"
            required={true}
            className="bg-white rounded-md border border-slate-600 h-10 px-2"
          />
          <input
            id="email"
            type="text"
            name="email"
            onChange={handleChange}
            value={formContent.email}
            placeholder="Email"
            required={true}
            className="bg-white rounded-md border border-slate-600 h-10 px-2"
          />

          <div className="flex justify-center">
            {recaptchaNeeded && <ReCAPTCHA
              sitekey={recaptchaPublicKey}
              onChange={onReCAPTCHAChange}
              theme="dark"
            />}
          </div>

          <button
            type="submit"
            className="w-full inline-flex items-center justify-center px-6 py-2 bg-slate-800 text-white rounded-md duration-200 disabled:cursor-not-allowed disabled:opacity-50"
            disabled={formContent.captcha === ""}
          >
            Submit
          </button>
        </form>
      </div>
    </main>
  )
}


That's it! if we inspect the network again, we can see that there are no requests made to ReCaptcha on page load:

Network's tab of Chrome's inspect, scoped on JS requests.


Now let's do another Google's PageSpeed audit: 

Google's Page Speed audit of our page after the optimization


Now we reached the perfect score. 🎉

Note that during this article, we have only seen a way to integrate ReCaptcha on the client side. If you don't verify the captcha on the server side, then you won't be protected against bots.

I hope this article was clear, feel free to give me a feedback. You can find the full project's code on this Github repo.

Partager
Ismaël Boukhars, project manager web and mobile at Capsens
Ismaël BoukharsJan 20, 2022
Linkedin logo

Capsens' blog

Capsens is an agency specialized in the development of fintech solutions. We love startups, scrum methodology, Ruby and React.

Ruby Biscuit

The french newsletter for Ruby on Rails developers.
Get similar content for free every month in your mailbox!
Subscribe