Integrare un widget di supporto in una single-page app: cambio di route, Shadow DOM, tempi di caricamento
Cosa deve saper fare un widget di supporto in una single-page app: superare i cambi di route, isolare gli stili, non rallentare la pagina. Con le ragioni tecniche dietro.
Martin Semmele

Contenuti
- 01Perché uno script di terze parti vive diversamente in una single-page app
- 02Cambio di route: il widget non appartiene all'albero dell'applicazione
- 03Shadow DOM: cosa fa e cosa non fa la separazione degli stili
- 04Tempi di caricamento: una riga decide
- 05Integrazione tramite un tag manager
- 06Consenso e caricamento dopo il clic
- 07La checklist dopo l'integrazione
- 08Domande frequenti
Punti chiave
- Il 92 percento di tutte le pagine web carica almeno una risorsa di terze parti; un widget di supporto è una di queste e deve comportarsi di conseguenza.
- Un widget montato una sola volta sul documento, e non nell'albero dell'applicazione, sopravvive a ogni cambio di route lato client senza il tuo intervento.
- Shadow DOM separa gli stili in entrambe le direzioni, ma non il focus da tastiera, i caratteri né l'ordine di sovrapposizione rispetto a un banner per i cookie.
- Uno script con l'attributo async non blocca l'analisi della pagina; questa è la riga che decide del tempo di caricamento.
- Doppio montaggio, identificativi di conversazione persi e un pacchetto di font caricato in un secondo momento sono i tre errori che nella pratica compaiono più spesso.
Perché uno script di terze parti vive diversamente in una single-page app
Su un sito classico ogni clic carica una nuova pagina. Uno script posizionato alla fine del documento viene quindi eseguito di nuovo a ogni visualizzazione di pagina, e tutto ciò che vuole ricordare deve depositarlo da qualche parte. In una single-page app è il contrario: il documento viene caricato esattamente una volta, poi il framework scambia solo parti dell'albero. Uno script che è stato eseguito una volta non viene rieseguito.
Per un widget di supporto questa è inizialmente una buona notizia. Non deve ricostruirsi a ogni cambio di route, e la conversazione aperta resta semplicemente aperta. La cattiva notizia deriva dalla stessa circostanza: tutto ciò che il widget fa male persiste anch'esso finché qualcuno non ricarica duramente la pagina. Un conflitto di stili, un secondo launcher in basso a destra, un focus rimasto intrappolato nel widget: nessun cambio di route ripulisce nulla di tutto questo.
Secondo il Web Almanac 2024, il 92 percento delle pagine esaminate carica almeno una risorsa di terze parti, e tra le mille pagine più grandi la mediana è di 66 provider diversi1. Il tuo widget di supporto è uno di questi. Gareggia con script di analisi, banner per i cookie e pacchetti di font per tempo di caricamento, ordine di sovrapposizione e focus da tastiera. I tre paragrafi seguenti esaminano i tre conflitti uno per uno.
Cambio di route: il widget non appartiene all'albero dell'applicazione
L'errore più comune durante l'integrazione in React, Vue o Angular è ben intenzionato: il widget viene inserito come componente nell'applicazione, perché stia «pulito» nell'albero. Così resta legato al ciclo di vita di quel componente. Quando la route cambia e il layout viene renderizzato di nuovo, il widget viene smontato e rimontato. L'effetto visibile: la finestra di chat si richiude, la cronologia è persa o appare doppia, e a seconda dell'implementazione viene aperta una seconda conversazione.
Montare una sola volta sul documento, non in un componente.
La variante più robusta è la più semplice: lo script viene caricato una volta, aggancia il proprio contenitore direttamente all'elemento body e vive così al di fuori di tutto ciò che il framework gestisce. Un cambio di route scambia nodi all'interno del contenitore dell'applicazione; il contenitore del widget accanto resta intatto. È esattamente così che lavora il widget Comlayer: legge il proprio tag script tramite document.currentScript, crea un singolo elemento host sul body e verifica prima del montaggio che questo elemento esista già. Se lo script viene eseguito una seconda volta, ad esempio perché un framework ripete il codice di integrazione a un nuovo render, non succede nulla.
Due finezze sono importanti in questo contesto. Primo: document.currentScript non restituisce nulla quando il codice viene eseguito da un callback, un evento o un modulo JavaScript2. Un widget che dipende da questo ha bisogno di un ripiego, per esempio la ricerca dell'ultimo tag script con l'attributo dati corrispondente. Secondo: l'indirizzo della pagina associato a una conversazione è l'indirizzo al momento in cui la conversazione è stata aperta. Se la visitatrice cambia route cinque volte dopo, nell'Inbox resta l'indirizzo di partenza. Non è un errore, ma una decisione; chi ha bisogno dell'indirizzo attuale deve chiederlo nella stessa conversazione.
Dove si trova la cronologia della conversazione.
Perché una conversazione sopravviva a un ricaricamento duro, il widget deve memorizzare l'identificativo della visitatrice e della conversazione. Il posto abituale è il localStorage del browser, depositato sotto un prefisso che contiene la chiave del widget. Due widget sullo stesso dominio, per esempio su una pagina di marketing e nell'applicazione alle spalle, non si disturbano così a vicenda. Ciò che lì non ha posto è il contenuto della conversazione stesso: si trova presso il provider, e il widget lo ricarica tramite l'identificativo. Se la memoria viene meno, perché la visitatrice la cancella o naviga in modalità privata, alla visita successiva inizia una nuova conversazione, e quella vecchia resta conservata nell'Inbox del team.
| Punto di integrazione | Comportamento al cambio di route | Comportamento al ricaricamento duro |
|---|---|---|
| Come componente nell'albero dell'applicazione | Viene smontato e rimontato a seconda del layout, la cronologia salta o si raddoppia | Rimontato, cronologia ricaricata dalla memoria |
| Come script sul documento, montato una volta | Intatto, la conversazione resta aperta | Rimontato, cronologia ricaricata dalla memoria |
| Inserito tramite un tag manager | Come sul documento, purché il tag scatti una sola volta | Come sul documento |
Shadow DOM: cosa fa e cosa non fa la separazione degli stili
Un widget porta con sé stili propri, e la tua pagina ne ha di propri. Senza separazione, un button { border-radius: 0 } globale nel tuo foglio di stile raggiunge anche il launcher del widget, e al contrario un selettore troppo ampio del widget colora i tuoi moduli. La via standard per impedirlo è Shadow DOM: il widget viene renderizzato in un proprio sottoalbero, i cui stili non vengono raggiunti dall'esterno e che a sua volta non agisce verso l'esterno. La documentazione Mozilla lo riassume così: il CSS della pagina non agisce sui nodi nello Shadow DOM, e gli stili nello Shadow DOM non agiscono sul resto della pagina3.
Quattro cose che il confine non ferma.
La separazione degli stili è completa, ma è solo una separazione degli stili. Quattro cose attraversano comunque il confine, e tutte e quattro compaiono nei ticket di supporto ai fornitori di widget:
- Proprietà ereditate. Famiglia del carattere, dimensione e colore del testo vengono ereditati dall'elemento host se il widget non li imposta da sé. Un widget che non fissa esplicitamente il proprio carattere appare diverso su ogni pagina.
- Focus da tastiera. Il tasto Tab percorre l'intera pagina, Shadow DOM o no. Una finestra di chat aperta che non trattiene il focus da sé lascia la tastiera vagare dietro la finestra nella navigazione della pagina.
- Ordine di sovrapposizione. Un banner per i cookie con
z-index: 99999si sovrappone al launcher se il widget sceglie un valore più piccolo. Per questo i widget impostano il proprio host sul valore massimo possibile e lo isolano conisolation: isolate, perché questo valore estremo non agisca nei contesti di impilamento della pagina. - Lingua e direzione di scrittura.
langedirvengono ereditati dall'elemento host. Una pagina in ebraico o in arabo commuta così anche il widget da destra a sinistra, indipendentemente dal fatto che sappia farlo o meno.
Uno Shadow DOM in modalità open non è una funzione di sicurezza: il JavaScript della pagina può continuare ad accedere tramite shadowRoot. La modalità regola solo l'accesso via script, non la separazione degli stili, e questa vale in entrambe le modalità3.
Tempi di caricamento: una riga decide
Uno script di terze parti costa tempo di caricamento. La sola domanda è se lo costa in secondo piano o in primo piano, mentre la visitatrice guarda una pagina vuota. La differenza è un attributo: un tag script senza async o defer ferma l'analisi del documento finché lo script non è caricato ed eseguito. La raccomandazione degli sviluppatori di Chrome è perciò inequivocabile: caricare gli script di terze parti sempre in modo asincrono, a meno che lo script debba essere eseguito prima che la pagina possa essere renderizzata4. Un widget di supporto non deve mai farlo.
Async, non defer, e perché il widget non carica font aggiuntivi.
async esegue lo script appena è caricato, indipendentemente dall'ordine nel documento. defer attende che il documento sia stato analizzato per intero e rispetta l'ordine4. Per un widget che dalla pagina non ha bisogno di nulla tranne l'elemento body, async è la scelta giusta; può tranquillamente finire prima del resto. L'integrazione di Comlayer consiste perciò in un unico tag con esattamente questo attributo, per esempio <script src="https://app.comlayer.app/comlayer-widget.js" data-app-id="wgt_…" async></script>. Un'interfaccia JavaScript per aprire, chiudere o segnalare un cambio di route non esiste, e non manca: il widget non ne ha bisogno, perché vive al di fuori dell'applicazione.
La seconda voce sul conto del tempo di caricamento viene spesso trascurata: i font. Un widget che carica il proprio carattere tramite @import da un servizio di font provoca su ogni sito cliente un'ulteriore richiesta a un ulteriore servizio, per ogni singola visitatrice. Questo costa tempo ed è, a seconda del servizio e della sua sede, anche un trasferimento di dati che dovrebbe comparire nell'informativa sulla privacy del sito cliente. Comlayer ha rimosso questo import dal widget e ricade sul carattere di sistema; il bundle compilato è così di circa 34 kilobyte, trasferiti compressi, misurato il 17 settembre 2026.
| Metrica | Soglia per «buono» | Cosa vi contribuisce un widget |
|---|---|---|
| Largest Contentful Paint (LCP) | 2,5 secondi | Solo se lo script si carica in modo sincrono o richiama font propri |
| INP (tempo di reazione agli input) | 200 millisecondi | Lunga esecuzione dello script nel thread principale all'avvio |
| Cumulative Layout Shift (CLS) | 0,1 | Un launcher che occupa spazio dopo il caricamento e sposta il contenuto |
Le tre soglie provengono dai Core Web Vitals e valgono ciascuna per il 75° percentile delle visualizzazioni di pagina, separate per mobile e desktop5. Un widget che si carica in modo asincrono, posiziona il launcher in modo fisso e non richiama font non compare in nessuna delle tre cifre.
Integrazione tramite un tag manager
Molti team non integrano gli script di terze parti nel sorgente, ma tramite un tag manager. Per un widget di supporto va bene, a due condizioni. Primo: il tag deve scattare esattamente una volta, al caricamento del documento, e non a ogni visualizzazione virtuale di pagina che la single-page app segnala al tag manager. Un tag che ascolta l'evento «visualizzazione di pagina» inserisce lo script di nuovo a ogni cambio di route. Un widget che intercetta da sé un doppio montaggio lo perdona; uno che non lo fa mostra poi due launcher.
Secondo: uno script inserito tramite tag manager in certi casi non viene più eseguito come il tag script che sarebbe stato nel sorgente. Un widget che legge la propria chiave tramite document.currentScript allora non trova nulla e ha bisogno del ripiego descritto sopra. Dopo l'integrazione verifica nella console che esattamente un elemento host sia agganciato al body e che il widget abbia trovato la propria chiave. Entrambe le cose si vedono negli strumenti per sviluppatori in pochi secondi.
Consenso e caricamento dopo il clic
Nell'Unione europea, all'integrazione si pone una domanda che con la tecnica ha a che fare solo marginalmente: lo script può caricarsi subito, o solo dopo un consenso? L'inquadramento giuridico è controverso, e lo abbiamo esposto in dettaglio in un articolo dedicato al widget di supporto e al consenso. Tecnicamente la variante prudente significa: il tag script non viene scritto staticamente nel documento, ma generato dallo strumento di consenso appena la visitatrice ha acconsentito.
Per una single-page app questo non cambia nulla nel principio di base. Anche un tag script generato in seguito aggancia una sola volta il proprio elemento host al body e vi resta attraverso ogni cambio di route. Ciò che cambia è il momento: il widget compare solo dopo il clic, e una conversazione che prima del consenso non era possibile non può nemmeno perdere una cronologia. Chi sceglie questa variante dovrebbe comunque tenere libero lo spazio del launcher, perché la pagina non salti quando il widget compare.
La checklist dopo l'integrazione
Se un'integrazione è pulita si può verificare in pochi minuti. I punti seguenti coprono gli errori che nella pratica compaiono più spesso:
- 01Apri una conversazione, poi cambia route tre volte. La finestra resta aperta, la cronologia resta, non nasce una seconda conversazione.
- 02Ricarica la pagina in modo duro. Dopo il caricamento la cronologia è di nuovo lì, e l'identificativo della conversazione nella memoria è lo stesso di prima.
- 03Verifica negli strumenti per sviluppatori che esattamente un elemento host sia agganciato al
body, anche dopo più cambi di route e un ritorno alla route iniziale. - 04Apri il banner per i cookie mentre la finestra di chat è aperta. Entrambi devono restare utilizzabili, nessuno dei due deve coprire l'altro.
- 05Percorri con il tasto Tab la finestra di chat aperta. Il focus deve essere visibile e non deve sparire nella pagina dietro la finestra.
- 06Verifica nella scheda di rete che dopo lo script del widget non venga caricato alcun file di font da un servizio esterno.
- 07Misura la pagina con gli strumenti Lighthouse, una volta con e una volta senza widget. I tre Core Web Vitals non devono peggiorare.
Comlayer supera questa lista perché il widget è costruito esattamente per questo: un tag script, un host sul body, uno Shadow DOM con stili propri, nessun font esterno, nessuna interfaccia che un framework debba gestire. L'aspetto del widget e i moduli che porta li imposti nel dashboard, senza ricostruire la pagina; cosa fa con i dati delle visitatrici è scritto nella pagina su sicurezza e privacy.
Domande frequenti
Devo reinizializzare il widget a ogni cambio di route?
No. Un widget montato una sola volta sul documento, che non vive nell'albero dell'applicazione, resta attraverso ogni cambio di route lato client. Un'inizializzazione per route è necessaria solo se il widget è stato integrato come componente nell'applicazione, ed è proprio ciò che va evitato.
Cosa succede se lo script viene inserito due volte?
Dipende dal widget. Un widget costruito bene verifica prima del montaggio se il proprio elemento host esiste già e la seconda volta non fa nulla. Un widget senza questa verifica mostra due launcher e apre in certi casi due conversazioni. I tag manager che scattano a ogni visualizzazione virtuale di pagina sono la causa più frequente.
Shadow DOM protegge completamente il mio foglio di stile dal widget?
Per i selettori sì: gli stili della pagina non raggiungono lo Shadow DOM, e gli stili del widget non raggiungono la pagina. Le proprietà ereditate come carattere e colore del testo, il focus da tastiera, l'ordine di sovrapposizione e gli attributi lang e dir attraversano comunque il confine.
Async o defer per lo script del widget?
Async. Il widget dalla pagina non ha bisogno di nulla tranne l'elemento body e può essere eseguito appena è caricato. defer non sarebbe sbagliato, ma attende inutilmente la fine dell'analisi. Importa solo che uno dei due attributi sia impostato; senza entrambi lo script blocca il rendering.
Come riconosco se il widget peggiora il tempo di caricamento?
Misura la pagina con gli strumenti Lighthouse una volta con e una volta senza lo script e confronta LCP, INP e CLS. Un widget caricato in modo asincrono con launcher fisso e senza font richiamati non modifica in modo misurabile i tre valori.
Posso caricare il widget solo dopo il consenso?
Sì. Il tag script allora non viene scritto staticamente nel documento, ma generato dallo strumento di consenso appena la visitatrice ha acconsentito. Anche un tag generato così monta il widget una sola volta sul documento, e dopo sopravvive a ogni cambio di route come uno integrato staticamente.