Corriger les problèmes de chargement de Google AdSense avec React
Sur l'un de mes derniers projets client, j'ai rencontré un problème avec le chargement des publicités Google AdSense.
Comme vous l'aurez compris, j'utilise React pour développer ce site. Une des particularités de React est qu'il est capable d'optimiser ses modifications du DOM. Il ne met à jour que ce qui est nécessaire. C'est top.
Sauf dans le cas de Google AdSense. C'est moins top.
Comment fonctionne le chargement des publicités AdSense ?
Regardons le code que Google nous demande d'ajouter à notre page pour chargement une publicité.
<ins class="adsbygoogle"
style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-1234567890123456"
data-ad-slot="1234567890"></ins>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script>
La publicité va s'afficher là où nous avons mis la balise <ins></ins>
.
Ensuite, il faut prévenir le script Google AdSense qu'il y a une publicité à afficher. Ça c'est le boulot de la mini ligne de JavaScript dans l'exemple ci-dessus. En gros, cette ligne ajoute un objet vide dans un tableau appelé adsbygoogle
.
Pourquoi ? Comme le chargement du script JS de Google est asynchrone, pour savoir qu'il y a X publicité à afficher, il se sert de ce tableau. La lecture du tableau lorsque les scripts sont chargés permet de toujours savoir combien de publicités sont nécessaires.
Une fois le script de Google chargé, en plus de lire le tableau, il va remplacer la méthode push
de ce tableau pour charger directement la publicité lorsque vous faites adsbygoogle.push({})
.
Le script ne va pas vérifier le tableau à intervalle régulier. Il est appelé lorsque (et uniquement lorsque) l'on appelle .push
.
Afficher une publicité avec React
La version React de ce code ne change pas beaucoup. Regardez :
const Ad = props => {
useEffect(() => {
window.adsbygoogle = window.adsbygoogle || []
window.adsbygoogle.push({})
})
return (
<div>
<ins
className="adsbygoogle"
style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-1234567890123456"
data-ad-slot="1234567890"
/>
</div>
)
}
Nous retrouvons le tag <ins />
et le .push({})
. Le push se fait dans un useEffect
pour qu'il soit exécuté dès que le code HTML est rendu.
Le useEffect
permet aussi de ré-éxécuter le push
si le code HTML est re-écrit par React pour une raison ou une autre. Ce qui est intéressant car nous ne chargons pas directement les publicités dans ce composant. Nous créons juste des éléments <ins />
dans le DOM et demandons (via le push({})
) au script de Google d'y afficher une publicité.
Donc si pour une raison ou une autre React supprime le <ins />
et le recrée, nous demandons au script de Google d'y charger une nouvelle publicité grâce au useEffect
.
L'erreur à abattre
Malheureusement, ce comportement optimisé de React peut nous poser une nouvelle erreur :
uncaught exception: TagError: adsbygoogle.push() error: All ins elements in the DOM with class=adsbygoogle already have ads in them.
Google vérifie que nous ne demandons pas trop de publicités par rapport aux places disponibles sur la page.
J'ai longtemps cherché une solution à ce problème car je ne comprenais pas l'erreur : j'ai exactement un .push({})
pour un <ins />
dans mon code. Je ne vois pas comment je pourrais appeler trop la méthode .push({})
.
Et bien c'est React qui faisait n'importe quoi. Ou presque.
React est optimisé et c'est (presque) toujours génial
Revenons à la façon optimisée qu'a React de modifier le DOM.
Plusieurs évènements extérieurs à un composant peuvent indiquer à React qu'il faut mettre à jour ce composant. Par exemple un changement des props
du composant parent, etc.
Lorsque React veut mettre à jour un composant il exécute les méthodes du life-cycle du composant dont sa méthode render
. Il récupère ainsi l'arbre de composants qui descendent de ce composant. Pour les « fonctions composants », il s'agit d’exécuter les hooks et de récupérer le retour de la fonction composant.
Il fait de même pour chaque niveau de la hiérarchie de composant. React se retrouve donc avec l'arbre complet qui représente notre page. Nous l'appelons aussi le DOM virtuel. React va maintenant comparer le DOM virtuel au DOM et détecter les différences. Ainsi il peut ne faire que les modifications nécessaires et non une mise à jour complète du DOM pour gagner en rapidité.
Et voilà qui décrit bien notre erreur. Les hooks / méthodes du life-cycle d'un composant sont toujours exécutées mais le DOM ne change pas forcément. Le code de notre composant <Ad />
est très simple, il ne nécessite que rarement d'être mis à jour. Par contre, des évènements extérieurs peuvent amener React à exécuter les hooks / méthodes du life-cycle en vue d'une mise à jour.
Le .push({})
est donc parfois exécuté dans des cas où la partie du DOM concernant notre publicité ne sera pas mis à jour. Comme le DOM n'est pas mis à jour une publicité est déjà présente dans la balise <ins />
du composant. Le code de Google Analytics lève donc une erreur.
Mettre à jour le composant uniquement lorsque nécessaire
Pour résoudre ce problème, nous ne devons plus laisser React décider (deviner) par lui-même quand mettre à jour notre composant de publicité.
Personnellement, je veux que les publicité soient mises à jour lorsque l'utilisateur change de page. Pour tout autre changement sur la page, l'arbre du composant ne doit pas être mis à jour.
La première modification à faire est d'utiliser la propriété key
de l'élément JSX <div />
. Le but de cette propriété est d'indiquer à React quels éléments ont changé, ont été ajoutés ou retirés du DOM. Il gagne ainsi en rapidité dans son traitement du DOM virtuel car il n'a pas à déterminer lui-même les changements effectués à cet endroit.
En d'autre termes, dans notre cas, tant que la clé ne changera pas React n'essaiera pas de mettre à jour la publicité. Par contre, le changement de clé forcera React à modifier le DOM et donc re-générer la publicité.
J'utilise le path
de la page courante comme clé, ce qui force React à recréer les balises de publicité lorsque l'utilisateur change de page mais l’empêche de faire des modifications tant que l'utilisateur reste sur la même page.
Nous savons maintenant, de façon sûre, quand le DOM sera mis à jour. Mais l'erreur existe toujours puisque les hooks sont toujours trop souvent exécutés.
La dernière modification apportée à notre composant réside dans le deuxième argument du useEffect
. Nous lui passons un tableau de valeur à vérifier avant de s’exécuter. Tant que les valeurs de ce tableau n'ont pas changer, le hook ne sera pas exécuté.
En utilisant la même propriété currentPath
, je synchronise l'exécution du hook avec la mise à jour du DOM. L'erreur est résolue.
Dans le cas des « classes composants », l'équivalent est atteint en surchargeant la méthode shouldComponentUpdate
.
Dans ce dernier cas la propriété key
de l'élément <div />
peut ne pas être utilisé car la méthode render
n'est pas du tout appelé lorsque shouldComponentUpdate
retourne false
.