Pourquoi votre prérendu échoue une page sur huit, sans raison apparente
Chromium gèle requestAnimationFrame dans les onglets d'arrière-plan. React Helmet en dépend pour écrire le head. Sur un prérendu parallélisé, une seule page obtient ses balises. Diagnostic et correction mesurée.
Pourquoi votre prérendu échoue une page sur huit, sans raison apparente
Un site en single page application qui veut être lu par un moteur de recherche doit servir du HTML déjà rendu. La méthode courante consiste à ouvrir chaque route dans un Chromium sans interface et à enregistrer le résultat. Pour aller vite, on parallélise sur plusieurs onglets. C'est là que commence un bug particulièrement désagréable, parce qu'il est intermittent, qu'il change à chaque exécution, et qu'il ressemble à un problème applicatif alors qu'il n'en est pas un. Voici le diagnostic complet et la correction, mesurés sur la chaîne de publication de Lovable Web Agency.
1. Le symptôme
Le prérendu tourne. Une partie des pages sort correctement. Une autre partie est rejetée par la validation, au motif que la balise canonical est absente. Les pages fautives ne sont pas les mêmes d'une exécution à l'autre. Aucune erreur JavaScript n'est remontée. Relancer suffit parfois à faire passer une page qui échouait, ce qui oriente vers une histoire de délai, et donc vers la mauvaise correction : allonger les temps d'attente.
Allonger les attentes améliore marginalement les choses et rallonge le build, ce qui finit par le faire dépasser la limite de temps de la plateforme d'hébergement. On remplace un problème par un autre.
2. La cause
Chromium économise les ressources des onglets qui ne sont pas au premier plan. Parmi les mécanismes concernés, requestAnimationFrame est fortement ralenti, voire gelé, dans un onglet d'arrière-plan.
Or React Helmet, dans sa version asynchrone, s'appuie sur ce mécanisme pour appliquer ses modifications au head du document. C'est là que se joue la balise title, la meta description et la canonical. Beaucoup d'implémentations font également dépendre de la même boucle le signal qui annonce que la page est prête à être capturée.
La conséquence est mécanique. Sur huit onglets ouverts dans un même navigateur, un seul est au premier plan. Les sept autres n'exécutent pas, ou trop tard, le code qui écrit les balises. La capture arrive sur un head incomplet. La validation rejette.
Ce comportement d'économie de ressources est documenté du côté du navigateur. Ce qui l'est beaucoup moins, c'est son effet sur une chaîne de prérendu, parce que la panne ne ressemble pas à sa cause.
3. Pourquoi les correctifs habituels ne suffisent pas
Les drapeaux de lancement destinés à désactiver ces optimisations réduisent le problème sans le supprimer. Dans notre reproduction isolée, sur huit pages ouvertes en parallèle, une seule obtenait ses balises. Avec les seuls drapeaux anti throttling, deux sur huit. C'est mieux, ce n'est pas une correction.
Attendre plus longtemps ne corrige rien non plus, puisque le code n'est pas lent : il est suspendu.
4. La correction qui tient
Un navigateur par exécutant, et non plusieurs onglets dans un navigateur partagé. Chaque processus n'a alors qu'un onglet, qui est par définition au premier plan. Le parallélisme est conservé, il se déplace simplement d'un niveau.
Le coût est une empreinte mémoire plus élevée, à compenser en réduisant le nombre d'exécutants simultanés et en recyclant les navigateurs par lots.
Second point, indépendant mais souvent présent en même temps : l'interception de requêtes de Puppeteer, utilisée pour bloquer les images et les polices afin d'accélérer le rendu, introduit une concurrence connue sur le chargement des modules JavaScript. Elle peut empêcher un fragment de code d'arriver. Nous l'avons retirée.
5. Le résultat mesuré
Sur la chaîne de publication de Lovable Web Agency, 115 rendus, incluant les routes de contenu en français et en anglais, les redirections héritées et la page d'erreur.
Avant correction : des échecs aléatoires sur la balise canonical, jusqu'à trente-deux routes rejetées selon les exécutions, et un build qui dépassait la limite de temps de la plateforme, donc aucune publication possible.
Après correction : cent quinze rendus sur cent quinze, aucune route ignorée, aucun échec, et un build complet ramené de trois minutes cinquante-cinq à une minute trente-cinq.
6. La leçon transposable, qui n'est pas technique
Le script comportait un budget de temps censé empêcher le dépassement. Il ne l'empêchait pas, parce qu'il n'était consulté qu'entre deux tâches. Une relance de navigateur déclenchée après l'échéance pouvait ajouter plusieurs minutes sans que rien ne la borne.
Un budget qui n'est pas vérifié à l'intérieur des opérations longues n'est pas une limite, c'est une intention. La règle utile est simple : chaque attente doit être plafonnée par le temps restant, et toute opération coûteuse doit pouvoir être annulée par l'échéance.
7. Comment savoir si vous êtes concerné
Trois signes. Le prérendu échoue sur des pages différentes à chaque exécution. Les pages fautives n'ont rien en commun sur le plan du contenu. Et le taux de réussite s'améliore quand vous réduisez le parallélisme.
Si les trois sont réunis, ne cherchez pas dans votre application. Regardez combien d'onglets partagent un navigateur.