Related articles

No items found.
The French newsletter for Ruby on Rails developers. Find similar content for free every month in your inbox!
Register
Share:
Blog
>

[NextJS] Lazyloading reCAPTCHA to improve service quality

If you are still looking to reach a 100/100 score on Page Speed Insight and are wondering how to get rid of the reCAPTCHA script that is negatively affecting 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 a NextJS application

Let's start with 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 installed TailwindCSS on the project to make the style very simple but that's not necessary at all.

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

Add a simple form

Now let's change the home page 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>
  );
}

That leaves us with a very simple form that should look like this:

Add reCAPTCHA V2 to the form

I chose to add v2 because v3 doesn't work very well in some specific cases, for example when using a professional VPN or anything that leads to using a shared IP. Chez Capsens, what we usually do is implement reCAPTCHA V3 with a fallback on V2 when the challenge is missed but for this article, we will keep it simple and get to the point.

To implement reCAPTCHA, I use the “React Google reCAPTCHA” library so let's add it to our project:

yarn add react-google-recaptcha@2.1.0

Now we need to:

  1. Import and add the reCAPTCHA component to our form with its public key.
  2. Add the “captcha” key to our initial state and give it the default value of an empty string.
  3. Create an onRecaptchaChange function to push the captcha into the state of our form when the challenge is run.
  4. Bonus: deactivate the submit button until the challenge is successful.

Here is the code if you want to go 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 problem with reCAPTCHA scripts

At this stage, reCAPTCHA is successfully implemented on the client side. Now we have a problem: unwanted requests are made to reCaptchaJS when the page loads. To do this, let's go to our browser's inspector, to the Network tab and refresh the page:

We can see that there are multiple calls to reCAPTCHA when the page loads. This is a problem because the browser has to download, analyze, and evaluate the script before making the page interactive for our users.

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

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

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

Lazy load reCAPTCHA

We need to import the reCAPTCHA script from Google dynamically when it is really needed.

Fortunately, NextJS provides us with a handy function that allows us to dynamically load a component and interact with it just like any regular component: Dynamic imports. It's the equivalent of React's lazy import, you You can find the doc here.

We could try wrapping our reCAPTCHA component in dynamic import and hope for the best, but that wouldn't work. Indeed, dynamic import imports the component whenever it is needed. So in this case, it would be required when the page loads and we would end up with the same problem.

We need to find a trigger that could tell us when we should actually use reCAPTCHA. A good trigger could be to say that when the user starts typing in a field, then reCAPTCHA should be imported.

Since our inputs are controlled components, the implementation is quite simple because we already have a function that is invoked every time the value of a field is changed.

We just need to create a new state that will tell us when reCAPTCHA is needed. The default value will be false and will be changed to true each time the value of a field changes:

// 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 is all! If we re-inspect the network, we can see that no requests are made to reCAPTCHA when the page loads:

Now, let's do another Google PageSpeed audit:

Now we have reached the perfect score. 🎉

Note that during this article, we only saw one way to integrate reCAPTCHA on the client side. If you don't check the captcha on the server side, then you won't be protected from bots.

I hope that this article was clear, do not hesitate to give me your comments. You can find the full project code on This Github repo.