Intégrer un widget de support dans une application monopage : changements de route, Shadow DOM, temps de chargement
Ce qu'un widget de support doit savoir faire dans une application monopage : survivre aux changements de route, isoler ses styles, ne pas ralentir la page. Avec les raisons techniques derrière.
Martin Semmele

Sommaire
- 01Pourquoi un script tiers vit autrement dans une application monopage
- 02Changements de route : le widget n'appartient pas à l'arbre de l'application
- 03Shadow DOM : ce que l'isolation des styles fait et ne fait pas
- 04Temps de chargement : une ligne décide
- 05Intégration via un gestionnaire de balises
- 06Consentement et chargement après clic
- 07La liste de contrôle après l'intégration
- 08Questions fréquentes
Points clés
- 92 % de toutes les pages web chargent au moins une ressource tierce ; un widget de support en fait partie et doit se comporter en conséquence.
- Un widget monté une fois sur le document et qui ne vit pas dans l'arbre de l'application survit à tout changement de route côté client sans intervention.
- Le Shadow DOM sépare les styles dans les deux sens, mais pas le focus clavier, les polices ni l'ordre d'empilement face à un bandeau cookies.
- Un script avec l'attribut async ne bloque pas l'analyse de la page ; c'est la ligne qui décide du temps de chargement.
- Double montage, identifiants de conversation perdus et paquet de polices chargé en plus sont les trois erreurs les plus fréquentes en pratique.
Pourquoi un script tiers vit autrement dans une application monopage
Sur un site web classique, chaque clic charge une nouvelle page. Un script placé à la fin du document s'exécute donc à nouveau à chaque visite de page, et tout ce qu'il veut retenir, il doit le stocker quelque part. Dans une application monopage, c'est l'inverse : le document n'est chargé qu'une seule fois, ensuite le framework échange simplement des parties de l'arbre. Un script qui a déjà tourné ne se relance pas.
C'est d'abord une bonne nouvelle pour un widget de support. Il n'a pas besoin de se reconstruire à chaque changement de route, et la conversation ouverte reste simplement ouverte. La mauvaise nouvelle découle du même constat : tout ce que le widget fait de travers persiste lui aussi, jusqu'à ce que quelqu'un recharge la page complètement. Un conflit de styles, un second lanceur en bas à droite, un focus coincé dans le widget : aucun changement de route ne nettoie cela.
Selon le Web Almanac 2024, 92 % des pages étudiées chargent au moins une ressource tierce, et pour les mille plus grandes pages, la médiane s'établit à 66 tiers différents1. Ton widget de support en fait partie. Il se dispute le temps de chargement, l'ordre d'empilement et le focus clavier avec les scripts d'analyse, les bandeaux cookies et les paquets de polices. Les trois sections suivantes examinent ces trois conflits un par un.
Changements de route : le widget n'appartient pas à l'arbre de l'application
L'erreur la plus fréquente lors de l'intégration dans React, Vue ou Angular est bien intentionnée : le widget est intégré comme un composant dans l'application, pour qu'il soit « proprement » dans l'arbre. Il dépend alors de la durée de vie de ce composant. Quand la route change et que le layout est rendu à nouveau, le widget est démonté puis remonté. L'effet visible : la fenêtre de chat se referme, l'historique a disparu ou apparaît en double, et selon l'implémentation une seconde conversation est ouverte.
Monter une fois sur le document, pas dans un composant.
La variante robuste est la plus simple : le script est chargé une seule fois, accroche son conteneur directement à l'élément body et vit ainsi en dehors de tout ce que gère le framework. Un changement de route échange des nœuds à l'intérieur du conteneur de l'application ; le conteneur du widget à côté reste intact. C'est exactement ainsi que fonctionne le widget Comlayer : il lit sa propre balise script via document.currentScript, crée un unique élément hôte sur le body et vérifie avant le montage si cet élément existe déjà. Si le script s'exécute une seconde fois, par exemple parce qu'un framework répète le code d'intégration lors d'un rerender, il ne se passe rien.
Deux subtilités comptent ici. Premièrement : document.currentScript ne renvoie rien quand le code s'exécute depuis un rappel, un événement ou un module JavaScript2. Un widget qui en dépend a besoin d'une solution de repli, par exemple la recherche de la dernière balise script portant le bon attribut de données. Deuxièmement : l'adresse de page associée à une conversation est celle du moment où la conversation a été ouverte. Si la visiteuse change ensuite cinq fois de route, l'adresse de départ reste affichée dans la boîte de réception. Ce n'est pas un défaut, mais une décision ; qui a besoin de l'adresse actuelle doit la demander dans le message lui-même.
Où se trouve l'historique de conversation.
Pour qu'une conversation survive à un rechargement complet, le widget doit mémoriser l'identifiant du visiteur et celui de la conversation. L'endroit habituel est le localStorage du navigateur, rangé sous un préfixe qui contient la clé du widget. Deux widgets sur le même domaine, par exemple sur une page marketing et dans l'application derrière, ne se gênent alors pas. Ce qui n'a rien à y faire, c'est le contenu de la conversation lui-même : il se trouve chez le fournisseur, et le widget le recharge à partir de l'identifiant. Si le stockage disparaît, parce que la visiteuse le supprime ou navigue en mode privé, une nouvelle conversation commence à la visite suivante, et l'ancienne reste conservée dans la boîte de réception de l'équipe.
| Lieu d'intégration | Comportement au changement de route | Comportement au rechargement complet |
|---|---|---|
| Comme composant dans l'arbre de l'application | Démonté et remonté selon le layout, l'historique saute ou se double | Remonté, historique rechargé depuis le stockage |
| Comme script sur le document, monté une fois | Intact, la conversation reste ouverte | Remonté, historique rechargé depuis le stockage |
| Inséré via un gestionnaire de balises | Comme sur le document, à condition que la balise ne se déclenche qu'une fois | Comme sur le document |
Shadow DOM : ce que l'isolation des styles fait et ne fait pas
Un widget apporte ses propres styles, et ta page a les siens. Sans séparation, un button { border-radius: 0 } global dans ta feuille de style s'applique aussi au lanceur du widget, et inversement un sélecteur du widget formulé trop largement colore tes formulaires. La voie standard pour l'éviter, c'est le Shadow DOM : le widget rend dans son propre sous-arbre, dont les styles ne sont pas atteignables de l'extérieur et qui n'agit pas lui-même vers l'extérieur. La documentation Mozilla le résume ainsi : le CSS de la page n'agit pas sur les nœuds du Shadow DOM, et les styles du Shadow DOM n'agissent pas sur le reste de la page3.
Quatre choses que la frontière n'arrête pas.
L'isolation des styles est complète, mais ce n'est qu'une isolation des styles. Quatre choses passent quand même la frontière, et les quatre se retrouvent dans les tickets de support adressés aux fournisseurs de widgets :
- Les propriétés héritées. La police, la taille de police et la couleur du texte sont héritées de l'élément hôte si le widget ne les définit pas lui-même. Un widget qui ne fixe pas explicitement sa police a l'air différent sur chaque page.
- Le focus clavier. La touche de tabulation parcourt toute la page, Shadow DOM ou pas. Une fenêtre de chat ouverte qui ne retient pas le focus elle-même laisse le clavier dériver derrière la fenêtre dans la navigation de la page.
- L'ordre d'empilement. Un bandeau cookies avec
z-index: 99999se place au-dessus du lanceur si le widget choisit une valeur plus petite. C'est pourquoi les widgets positionnent leur hôte sur la plus grande valeur possible et l'isolent avecisolation: isolate, pour que cette valeur extrême n'agisse pas dans les contextes d'empilement de la page. - La langue et le sens d'écriture.
langetdirsont hérités de l'élément hôte. Une page en hébreu ou en arabe bascule aussi le widget de droite à gauche, qu'il en soit capable ou non.
Un Shadow DOM en mode open n'est donc pas un dispositif de sécurité : le JavaScript de la page peut continuer à y accéder via shadowRoot. Le mode ne règle que l'accès par script, pas l'isolation des styles, et celle-ci vaut dans les deux modes3.
Temps de chargement : une ligne décide
Un script tiers coûte du temps de chargement. La seule question est de savoir s'il coûte ce temps en arrière-plan ou au premier plan, pendant que la visiteuse regarde une page vide. La différence tient à un attribut : une balise script sans async ni defer interrompt l'analyse du document jusqu'à ce que le script soit chargé et exécuté. La recommandation des développeurs de Chrome est donc sans ambiguïté : toujours charger les scripts tiers de façon asynchrone, sauf si le script doit s'exécuter avant que la page puisse être rendue4. Un widget de support n'en a jamais besoin.
Async, pas defer, et pourquoi le widget ne charge pas de polices.
async exécute le script dès qu'il est chargé, indépendamment de l'ordre dans le document. defer attend que le document soit entièrement analysé et respecte l'ordre4. Pour un widget qui n'a besoin de rien sur la page hormis l'élément body, async est le bon choix ; il peut très bien être prêt avant le reste. L'intégration de Comlayer tient donc en une seule balise avec exactement cet attribut, par exemple <script src="https://app.comlayer.app/comlayer-widget.js" data-app-id="wgt_…" async></script>. Il n'existe pas d'interface JavaScript pour ouvrir, fermer ou signaler un changement de route, et elle ne manque pas : le widget n'en a pas besoin, parce qu'il vit en dehors de l'application.
Le deuxième poste sur la facture du temps de chargement est souvent oublié : les polices. Un widget qui charge sa police maison via @import depuis un service de polices déclenche sur chaque site client une requête de plus vers un service de plus, pour chaque visiteuse. Cela coûte du temps et constitue, selon le service et son siège, en plus un transfert de données qui devrait figurer dans la notice de confidentialité du site client. Comlayer a retiré cet import du widget et se rabat sur la police système ; le bundle compilé pèse ainsi environ 34 kilooctets, transférés compressés, mesuré le 17 septembre 2026.
| Indicateur | Seuil pour « bon » | Ce qu'un widget y contribue |
|---|---|---|
| Largest Contentful Paint (LCP) | 2,5 secondes | Seulement si le script se charge de façon synchrone ou tire ses propres polices |
| INP (temps de réaction aux saisies) | 200 millisecondes | Longue exécution du script dans le thread principal au démarrage |
| Cumulative Layout Shift (CLS) | 0,1 | Un lanceur qui prend de la place après le chargement et décale le contenu |
Les trois seuils viennent des Core Web Vitals et s'appliquent chacun au 75e percentile des chargements de page, séparés entre mobile et ordinateur5. Un widget qui se charge de façon asynchrone, positionne son lanceur de manière fixe et ne tire aucune police n'apparaît dans aucun des trois chiffres.
Intégration via un gestionnaire de balises
Beaucoup d'équipes n'intègrent pas les scripts tiers dans le code source, mais via un gestionnaire de balises. Pour un widget de support, c'est acceptable, à deux conditions. Premièrement, la balise doit se déclencher exactement une fois, au chargement du document, et non à chaque vue de page virtuelle que l'application monopage signale au gestionnaire de balises. Une balise qui écoute l'événement « vue de page » réinsère le script à chaque changement de route. Un widget qui intercepte lui-même un double montage le pardonne ; un qui ne le fait pas affiche ensuite deux lanceurs.
Deuxièmement, un script inséré via un gestionnaire de balises ne s'exécute parfois plus comme la balise script qu'il aurait été dans le code source. Un widget qui lit sa clé via document.currentScript ne trouve alors rien et a besoin du repli décrit plus haut. Vérifie après l'intégration, dans la console, qu'exactement un élément hôte est accroché au body et que le widget a trouvé sa clé. Les deux se voient dans les outils de développement en quelques secondes.
Consentement et chargement après clic
Dans l'Union européenne, l'intégration soulève une question qui n'a que marginalement à voir avec la technique : le script peut-il se charger immédiatement, ou seulement après un consentement ? La qualification juridique est controversée, et nous l'avons exposée en détail dans un article consacré au widget de support et au consentement. Techniquement, la variante prudente signifie : la balise script n'est pas écrite statiquement dans le document, mais générée par l'outil de consentement dès que la visiteuse a accepté.
Pour une application monopage, cela ne change rien au principe de base. Une balise script générée après coup accroche elle aussi son élément hôte une fois au body et y reste à travers tous les changements de route. Ce qui change, c'est le moment : le widget n'apparaît qu'après le clic, et une conversation qui n'était pas possible avant le consentement ne peut pas non plus perdre d'historique. Qui choisit cette variante devrait tout de même réserver la place du lanceur, pour que la page ne saute pas quand le widget apparaît.
La liste de contrôle après l'intégration
On peut vérifier en quelques minutes si une intégration est propre. Les points suivants couvrent les erreurs les plus fréquentes en pratique :
- 01Ouvre une conversation, puis change trois fois de route. La fenêtre reste ouverte, l'historique reste en place, aucune seconde conversation n'est créée.
- 02Recharge la page complètement. L'historique est de retour après le chargement, et l'identifiant de conversation dans le stockage est le même qu'avant.
- 03Vérifie dans les outils de développement qu'exactement un élément hôte est accroché au
body, y compris après plusieurs changements de route et un retour à la route de départ. - 04Ouvre le bandeau cookies pendant que la fenêtre de chat est ouverte. Les deux doivent rester utilisables, aucun ne doit recouvrir l'autre.
- 05Parcours la fenêtre de chat ouverte avec la touche de tabulation. Le focus doit être visible et ne doit pas disparaître dans la page derrière la fenêtre.
- 06Vérifie dans l'onglet réseau qu'aucun fichier de police n'est chargé depuis un service tiers après le script du widget.
- 07Mesure la page avec les outils Lighthouse, une fois avec et une fois sans widget. Les trois Core Web Vitals ne doivent pas se dégrader.
Comlayer passe cette liste parce que le widget est construit exactement pour cela : une balise script, un hôte sur le body, un Shadow DOM avec ses propres styles, pas de polices tierces, pas d'interface qu'un framework devrait piloter. L'apparence du widget et les briques qu'il porte se règlent dans le tableau de bord, sans reconstruire la page ; ce qu'il fait des données des visiteuses figure sur la page sécurité et protection des données.
Questions fréquentes
Dois-je réinitialiser le widget à chaque changement de route ?
Non. Un widget monté une fois sur le document et qui ne vit pas dans l'arbre de l'application persiste à travers tous les changements de route côté client. Une initialisation par route n'est nécessaire que si le widget a été intégré comme composant dans l'application, et c'est précisément ce qu'il faut éviter.
Que se passe-t-il si le script est inséré deux fois ?
Cela dépend du widget. Un widget bien construit vérifie avant le montage si son élément hôte existe déjà et ne fait rien la seconde fois. Un widget sans cette vérification affiche deux lanceurs et ouvre parfois deux conversations. Les gestionnaires de balises qui se déclenchent à chaque vue de page virtuelle en sont la cause la plus fréquente.
Le Shadow DOM protège-t-il complètement ma feuille de style du widget ?
Pour les sélecteurs, oui : les styles de la page n'atteignent pas le Shadow DOM, et les styles du widget n'atteignent pas la page. Les propriétés héritées comme la police et la couleur du texte, le focus clavier, l'ordre d'empilement et les attributs lang et dir passent quand même la frontière.
Async ou defer pour le script du widget ?
Async. Le widget n'a besoin de rien sur la page hormis l'élément body et peut s'exécuter dès qu'il est chargé. defer ne serait pas faux, mais attend inutilement la fin de l'analyse. Ce qui compte, c'est seulement qu'un des deux attributs soit posé ; sans les deux, le script bloque le rendu.
Comment savoir si le widget dégrade le temps de chargement ?
Mesure la page avec les outils Lighthouse une fois avec et une fois sans le script et compare LCP, INP et CLS. Un widget chargé de façon asynchrone, avec un lanceur fixe et sans polices tirées, ne modifie pas les trois valeurs de manière mesurable.
Puis-je charger le widget seulement après le consentement ?
Oui. La balise script n'est alors pas écrite statiquement dans le document, mais générée par l'outil de consentement dès que la visiteuse a accepté. Une balise générée ainsi monte elle aussi le widget une fois sur le document, et il survit ensuite à tous les changements de route comme un widget intégré statiquement.