[NextJs] LazyLoading ReCaptcha pour améliorer la qualité du service

Ismaël Boukhars, chef de projet web & mobile chez Capsens
Ismaël Boukhars20 janv. 2022

Si vous cherchez toujours à atteindre la note de 100/100 sur Page Speed Insight et que vous vous demandez comment vous débarrasser du script ReCaptcha qui affecte négativement votre score, alors cet article est pour vous.

Cet article montrera un exemple pour un projet ReactJs/NextJs mais le concept peut être appliqué à n'importe quelle autre pile.


Créer une application NextJs

Commençons par un projet NextJs à partir de zéro :

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

Note : Si vous n'avez pas Yarn ou Node (> 14.00) installé, vous pouvez vous référer à cet article.

J'ai aussi installé TailwindCss sur le projet pour rendre le style très simple mais ce n'est pas du tout nécessaire.

Maintenant, allons dans notre navigateur et tapons http://localhost:3000, vous devriez voir quelque chose comme ceci :

La page d'accueil par défaut d'un nouveau projet NextJs


Ajout d'un formulaire simple

Modifions maintenant la page d'accueil pour ajouter un formulaire simple :

// 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>
  );
}


Cela nous laisse avec un formulaire très simple qui devrait s'afficher comme celui-ci :

Notre page d'accueil avec le formulaire très simple


Ajout de ReCaptcha V2 au formulaire

J'ai choisi d'ajouter la v2 car la v3 ne fonctionne pas très bien dans certains cas spécifiques, par exemple lors de l'utilisation d'un VPN professionnel ou de tout ce qui conduit à utiliser une IP partagée. Chez Capsens, ce que nous faisons habituellement est d'implémenter ReCaptcha V3 avec un fallback sur V2 lorsque le challenge est manqué mais pour cet article, nous allons rester simple et aller droit au but.

Pour implémenter ReCaptcha, j'utilise la bibliothèque "React Google ReCaptcha" donc ajoutons-la à notre projet :

yarn add react-google-recaptcha@2.1.0


Nous devons maintenant :

  1. Importer et ajouter le composant ReCaptcha à notre formulaire avec sa clé publique.

  2. Ajouter la clé "captcha" à notre état initial et lui donner la valeur par défaut d'une chaîne vide.

  3. Créer une fonction onReCAPTCHAChange pour pousser le captcha dans l'état de notre formulaire lorsque le challenge est exécuté.

  4. Bonus : désactiver le bouton submit tant que le challenge n'a pas réussi.

Voici le code si vous voulez passer directement à l'étape suivante :

// 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>
  )
}


Le problème des scripts de ReCaptcha

A ce stade, ReCaptcha est implémenté avec succès du côté client. Nous avons maintenant un problème : des requêtes non désirées sont faites à ReCaptchaJs au chargement de la page. Pour cela, allons dans l'inspecteur de notre navigateur, dans l'onglet Réseau et rafraîchissons la page :

L'onglet réseau de l'inspecteur de Chrome, centré sur les requêtes JS.


Nous pouvons voir qu'il y a plusieurs appels à ReCaptcha au chargement de la page. C'est un problème car le navigateur doit télécharger, analyser et évaluer le script avant de rendre la page interactive pour nos utilisateurs.

En d'autres termes, cela affecte négativement notre vitesse de page. Si nous faisons un audit de la vitesse de la page de Google, nous pouvons voir que notre note n'est pas très bonne malgré le fait que notre page soit presque vide !

Audit Google Page Speed de notre page


Note : J'utilise Google Page Speed pour auditer mes sites web car il me permet d'auditer les performances du site en conditions de production mais vous pouvez aussi utiliser LightHouse directement sur votre serveur local. Vous pouvez l'exécuter directement à partir de l'inspecteur de Chrome.

Dans l'audit ci-dessus, les scripts ReCaptcha semblent être la seule chose que Google nous reproche. Voyons si nous pouvons nous en débarrasser.


Lazy load ReCaptcha

Nous avons besoin d'importer le script ReCaptcha de Google dynamiquement lorsqu'il est réellement nécessaire.

Heureusement, NextJs nous fournit une fonction pratique qui nous permet de charger dynamiquement un composant et d'interagir avec lui comme n'importe quel composant ordinaire : Les importations dynamiques. C'est l'équivalent du lazy import de React, vous trouverez sa doc ici.

Doc de NextJs sur les importations dynamiques.


Nous pourrions essayer d'envelopper notre composant ReCaptcha dans l'importation dynamique et espérer le meilleur, mais cela ne fonctionnerait pas. En effet, l'importation dynamique importe le composant chaque fois qu'il est nécessaire. Donc, dans ce cas, il serait requis au chargement de la page et nous nous retrouverions avec le même problème.

Nous devons trouver un déclencheur qui pourrait nous dire quand nous devons réellement utiliser ReCaptcha. Un bon déclencheur pourrait être de dire que lorsque l'utilisateur commence à taper dans un champ, alors le ReCaptcha devrait être importé.

Nos entrées étant des composants contrôlés, l'implémentation est assez simple car nous avons déjà une fonction qui est invoquée chaque fois que la valeur d'un champ est modifiée.

Nous devons simplement créer un nouvel état qui nous dira quand ReCaptcha est nécessaire. La valeur par défaut sera false et sera changée en true chaque fois que la valeur d'un champ 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>
  )
}


C'est tout ! Si nous inspectons à nouveau le réseau, nous pouvons constater qu'aucune demande n'est adressée à ReCaptcha lors du chargement de la page :

L'onglet réseau de l'inspecteur de Chrome, centré sur les requêtes JS.


Maintenant, faisons un autre audit PageSpeed de Google : 

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


Maintenant, nous avons atteint le score parfait. 🎉

Notez que durant cet article, nous n'avons vu qu'une façon d'intégrer ReCaptcha du côté client. Si vous ne vérifiez pas le captcha du côté serveur, alors vous ne serez pas protégé contre les bots.

J'espère que cet article était clair, n'hésitez pas à me faire part de vos commentaires. Vous pouvez trouver le code complet du projet sur ce repo Github.

Partager
Ismaël Boukhars, chef de projet web & mobile chez Capsens
Ismaël Boukhars20 janv. 2022

Blog de Capsens

Capsens est une agence spécialisée dans le développement de solutions fintech. Nous aimons les startups, la méthodologie scrum, le Ruby et le React.