Integrar um widget de suporte em single-page apps: mudanças de rota, Shadow DOM, tempo de carregamento
O que um widget de suporte tem de conseguir numa single-page app: sobreviver a mudanças de rota, isolar os estilos, não abrandar a página. Com as razões técnicas por trás.
Martin Semmele

Conteúdo
- 01Porque é que um script de terceiros vive de forma diferente numa Single-Page-App
- 02Mudanças de rota: o widget não pertence à árvore da aplicação
- 03Shadow DOM: o que a separação de estilos consegue e o que não consegue
- 04Tempo de carregamento: uma linha decide
- 05Integração através de um gestor de tags
- 06Consentimento e o carregamento após o clique
- 07A lista de verificação depois da integração
- 08Perguntas frequentes
Conclusões principais
- 92 por cento de todas as páginas web carregam pelo menos um recurso de terceiros; um widget de suporte é um deles e tem de comportar-se em consequência.
- Um widget que é montado uma vez no documento e não vive na árvore da aplicação sobrevive sem esforço a qualquer mudança de rota do lado do cliente.
- O Shadow DOM separa os estilos nos dois sentidos, mas não o foco do teclado, as fontes nem a ordem de empilhamento perante um banner de cookies.
- Um script com o atributo async não bloqueia a análise da página; essa é a única linha que decide o tempo de carregamento.
- Montagem dupla, identificadores de conversa perdidos e um pacote de fontes carregado depois são os três erros que surgem mais frequentemente na prática.
Porque é que um script de terceiros vive de forma diferente numa Single-Page-App
Num website clássico, cada clique carrega uma página nova. Um script que está no fim do documento volta assim a correr a cada visita à página, e tudo o que quer memorizar tem de o guardar algures. Numa Single-Page-App é ao contrário: o documento é carregado exatamente uma vez; depois disso, o framework troca apenas partes da árvore. Um script que já correu uma vez não volta a correr.
Para um widget de suporte, isto é à partida uma boa notícia. Não tem de se reconstruir a cada mudança de rota, e a conversa aberta simplesmente mantém-se aberta. A má notícia resulta do mesmo facto: tudo o que o widget faz mal também persiste, até alguém recarregar completamente a página. Um conflito de estilos, um segundo launcher em baixo à direita, um foco que fica preso no widget; nada disso é limpo por uma mudança de rota.
Segundo o Web Almanac 2024, 92 por cento das páginas analisadas carregam pelo menos um recurso de terceiros, e nas mil maiores páginas a mediana é de 66 terceiros diferentes1. O teu widget de suporte é um deles. Compete com scripts de análise, banners de cookies e pacotes de fontes pelo tempo de carregamento, pela ordem de empilhamento e pelo foco do teclado. As três secções seguintes dissecam os três conflitos um a um.
Mudanças de rota: o widget não pertence à árvore da aplicação
O erro mais comum na integração em React, Vue ou Angular é bem-intencionado: o widget é trazido para a aplicação como componente, para ficar “limpo” na árvore. Com isso, fica dependente do ciclo de vida desse componente. Quando a rota muda e o layout é novamente renderizado, o widget é desmontado e montado de novo. O efeito visível: a janela de conversa fecha, o histórico desaparece ou aparece duplicado e, consoante a implementação, é aberta uma segunda conversa.
Montar uma vez no documento, não num componente.
A variante robusta é a mais simples: o script é carregado uma vez, anexa o seu contentor diretamente ao elemento body e vive assim fora de tudo o que o framework gere. Uma mudança de rota troca nós dentro do contentor da aplicação; o contentor do widget ao lado fica intacto. É exatamente assim que funciona o widget da Comlayer: lê a sua própria tag de script através de document.currentScript, cria um único elemento hospedeiro no body e verifica antes da montagem se esse elemento já existe. Se o script for executado uma segunda vez, por exemplo porque um framework repete o código de integração numa nova renderização, não acontece nada.
Duas subtilezas são aqui importantes. Primeira: document.currentScript não devolve nada quando o código corre a partir de um retorno de chamada, de um evento ou de um módulo JavaScript2. Um widget que dependa disso precisa de um recurso de fallback, por exemplo a procura da última tag de script com o atributo de dados correspondente. Segunda: o endereço da página associado a uma conversa é o endereço do momento em que a conversa foi aberta. Se a visitante mudar cinco vezes de rota depois disso, na caixa de entrada fica o endereço inicial. Isso não é um erro, é uma decisão; quem precisar do endereço atual tem de o perguntar na própria mensagem.
Onde fica o histórico da conversa.
Para que uma conversa sobreviva a um recarregamento forçado, o widget tem de memorizar o identificador da visitante e o da conversa. O sítio habitual é o localStorage do browser, guardado sob um prefixo que contém a chave do widget. Dois widgets no mesmo domínio, por exemplo numa página de marketing e na aplicação atrás dela, não se atrapalham assim. O que aí não pertence é o conteúdo da conversa em si: esse fica no fornecedor, e o widget carrega-o com base no identificador. Se o armazenamento desaparecer, porque a visitante o apaga ou navega em modo privado, a próxima visita começa uma conversa nova, e a antiga mantém-se na caixa de entrada da equipa.
| Local de integração | Comportamento na mudança de rota | Comportamento em recarregamento forçado |
|---|---|---|
| Como componente na árvore da aplicação | É desmontado e montado de novo consoante o layout; o histórico salta ou duplica-se | Montado de novo, histórico carregado do armazenamento |
| Como script no documento, montado uma vez | Intacto, a conversa mantém-se aberta | Montado de novo, histórico carregado do armazenamento |
| Inserido através de um gestor de tags | Como no documento, desde que a tag dispare apenas uma vez | Como no documento |
Shadow DOM: o que a separação de estilos consegue e o que não consegue
Um widget traz os seus próprios estilos, e a tua página tem os seus. Sem separação, um button { border-radius: 0 } global na tua folha de estilos também se aplica ao launcher do widget e, ao contrário, um seletor do widget formulado de forma demasiado larga pinta os teus formulários. O caminho padrão para o impedir é o Shadow DOM: o widget renderiza numa subárvore própria, cujos estilos não são alcançados de fora e que por sua vez não afeta o exterior. A documentação da Mozilla resume assim: o CSS da página não afeta os nós no Shadow DOM, e os estilos no Shadow DOM não afetam o resto da página3.
Quatro coisas que a fronteira não trava.
A separação de estilos é completa, mas é apenas uma separação de estilos. Quatro coisas atravessam a fronteira mesmo assim, e as quatro aparecem em pedidos de suporte a fornecedores de widgets:
- Propriedades herdadas. Tipo de letra, tamanho da letra e cor do texto são herdados do elemento hospedeiro se o widget não os definir ele próprio. Um widget que não fixa explicitamente a sua fonte tem um aspeto diferente em cada página.
- Foco do teclado. A tecla Tab percorre a página inteira, com ou sem Shadow DOM. Uma janela de conversa aberta que não segura ela própria o foco deixa o teclado vaguear pela navegação da página por trás da janela.
- Ordem de empilhamento. Um banner de cookies com
z-index: 99999sobrepõe-se ao launcher se o widget escolher um valor mais baixo. Por isso os widgets colocam o seu hospedeiro no maior valor possível e isolam-no comisolation: isolate, para que este valor extremo não se propague aos contextos de empilhamento da página. - Idioma e direção de escrita.
langedirsão herdados do elemento hospedeiro. Uma página em hebraico ou árabe muda assim também o widget para a escrita da direita para a esquerda, quer ele saiba lidar com isso ou não.
Um Shadow DOM no modo open não é uma funcionalidade de segurança: o JavaScript da página continua a poder entrar através de shadowRoot. O modo regula apenas o acesso por script, não a separação de estilos, e essa vale em ambos os modos3.
Tempo de carregamento: uma linha decide
Um script de terceiros custa tempo de carregamento. A única questão é se custa esse tempo em segundo plano ou em primeiro plano, enquanto a visitante olha para uma página vazia. A diferença é um atributo: uma tag de script sem async ou defer interrompe a análise do documento até o script estar carregado e executado. A recomendação dos programadores do Chrome é por isso inequívoca: carregar scripts de terceiros sempre de forma assíncrona, a menos que o script tenha de correr antes de a página poder ser renderizada4. Um widget de suporte nunca tem de o fazer.
Async, não defer, e porque é que o widget não carrega fontes adicionais.
async executa o script assim que está carregado, independentemente da ordem no documento. defer espera até o documento estar totalmente analisado e respeita a ordem4. Para um widget que não precisa de nada da página além do elemento body, async é a escolha certa; pode perfeitamente ficar pronto antes do resto. A integração da Comlayer consiste por isso numa única tag com exatamente este atributo, por exemplo <script src="https://app.comlayer.app/comlayer-widget.js" data-app-id="wgt_…" async></script>. Não existe uma interface JavaScript para abrir, fechar ou comunicar uma mudança de rota, e ela não falta: o widget não precisa dela, porque vive fora da aplicação.
A segunda rubrica na conta do tempo de carregamento é muitas vezes ignorada: as fontes. Um widget que carrega a sua fonte própria através de @import a partir de um serviço de fontes provoca em cada página de cliente mais um pedido a mais um serviço, para cada visitante individual. Isso custa tempo e é, consoante o serviço e a sua sede, adicionalmente uma transmissão de dados que teria de constar do aviso de privacidade da página do cliente. A Comlayer removeu este import do widget e recorre à fonte do sistema; o pacote compilado tem assim cerca de 34 kilobytes, transmitidos comprimidos, medido a 17 de setembro de 2026.
| Métrica | Limiar para «bom» | O que um widget contribui para ela |
|---|---|---|
| Largest Contentful Paint (LCP) | 2,5 segundos | Só se o script carregar de forma síncrona ou puxar fontes próprias |
| INP (tempo de reação a entradas) | 200 milissegundos | Execução longa do script na thread principal no arranque |
| Cumulative Layout Shift (CLS) | 0,1 | Um launcher que ocupa espaço depois do carregamento e desloca conteúdo |
Os três limiares vêm dos Core Web Vitals e aplicam-se cada um ao 75.º percentil das visualizações de página, separados por telemóvel e desktop5. Um widget que carrega de forma assíncrona, posiciona o launcher de forma fixa e não puxa fontes não aparece em nenhum dos três números.
Integração através de um gestor de tags
Muitas equipas não integram scripts de terceiros no código-fonte, mas através de um gestor de tags. Para um widget de suporte isso é aceitável, com duas condições. Primeira: a tag tem de disparar exatamente uma vez, ao carregar o documento, e não a cada visualização de página virtual que a single-page app comunica ao gestor de tags. Uma tag que escuta o evento «visualização de página» insere o script de novo a cada mudança de rota. Um widget que intercepta ele próprio uma montagem dupla perdoa isso; um que não o faz mostra depois dois launchers.
Segunda: um script inserido através de um gestor de tags pode já não correr como a tag de script que teria sido no código-fonte. Um widget que lê a sua chave através de document.currentScript não encontra então nada e precisa do fallback descrito acima. Verifica na consola, depois da integração, se exatamente um elemento hospedeiro está anexado ao body e se o widget encontrou a sua chave. Ambas as coisas se veem nas ferramentas de programador em segundos.
Consentimento e o carregamento após o clique
Na União Europeia, a integração levanta uma questão que só marginalmente tem a ver com técnica: pode o script carregar de imediato, ou só depois de um consentimento? O enquadramento jurídico é controverso, e expusemo-lo em detalhe num artigo próprio sobre o widget de suporte e o consentimento. Tecnicamente, a variante cautelosa significa: a tag de script não é escrita estaticamente no documento, mas gerada pela ferramenta de consentimento assim que a visitante concordou.
Para uma single-page app isso não muda nada no princípio de base. Também uma tag de script gerada posteriormente anexa o seu elemento hospedeiro uma vez ao body e aí permanece através de todas as mudanças de rota. O que muda é o momento: o widget só aparece depois do clique, e uma conversa que antes do consentimento não era possível também não pode perder nenhum histórico. Quem escolhe esta variante deve mesmo assim manter livre o lugar do launcher, para que a página não salte quando o widget aparece.
A lista de verificação depois da integração
Se uma integração está limpa pode verificar-se em poucos minutos. Os pontos seguintes cobrem os erros que surgem mais frequentemente na prática:
- 01Abre uma conversa e muda depois três vezes de rota. A janela mantém-se aberta, o histórico fica, não nasce uma segunda conversa.
- 02Recarrega a página de forma forçada. Depois do carregamento o histórico está de volta, e o identificador da conversa no armazenamento é o mesmo de antes.
- 03Verifica nas ferramentas de programador que exatamente um elemento hospedeiro está anexado ao
body, também depois de várias mudanças de rota e de um regresso à rota inicial. - 04Abre o banner de cookies enquanto a janela de conversa está aberta. Ambos têm de continuar utilizáveis, nenhum pode cobrir o outro.
- 05Percorre a janela de conversa aberta com a tecla Tab. O foco tem de ser visível e não pode desaparecer na página por trás da janela.
- 06Verifica no separador de rede que depois do script do widget não é carregado nenhum ficheiro de fontes de um serviço externo.
- 07Mede a página com as ferramentas Lighthouse, uma vez com e uma vez sem widget. Os três Core Web Vitals não podem piorar.
A Comlayer passa nesta lista porque o widget foi construído exatamente para isso: uma tag de script, um hospedeiro no body, um Shadow DOM com estilos próprios, sem fontes externas, sem interface que um framework tivesse de operar. O aspeto do widget e os módulos que transporta defines no dashboard, sem reconstruir a página; o que faz com os dados das visitantes está na página sobre segurança e proteção de dados.
Perguntas frequentes
Tenho de reinicializar o widget a cada mudança de rota?
Não. Um widget montado uma vez no documento, que não vive na árvore da aplicação, permanece através de todas as mudanças de rota do lado do cliente. Uma inicialização por rota só é necessária se o widget foi integrado na aplicação como componente, e é exatamente isso que deve ser evitado.
O que acontece se o script for inserido duas vezes?
Depende do widget. Um widget bem construído verifica antes da montagem se o seu elemento hospedeiro já existe e não faz nada à segunda vez. Um widget sem essa verificação mostra dois launchers e abre eventualmente duas conversas. Gestores de tags que disparam a cada visualização de página virtual são a causa mais frequente.
O Shadow DOM protege completamente a minha folha de estilos do widget?
Para seletores, sim: os estilos da página não chegam ao Shadow DOM, e os estilos do widget não chegam à página. Propriedades herdadas como tipo de letra e cor do texto, o foco do teclado, a ordem de empilhamento e os atributos lang e dir atravessam mesmo assim a fronteira.
Async ou defer para o script do widget?
Async. O widget não precisa de nada da página além do elemento body e pode correr assim que está carregado. defer não seria errado, mas espera desnecessariamente pelo fim da análise. O que importa é apenas que um dos dois atributos esteja definido; sem ambos, o script bloqueia a renderização.
Como reconheço se o widget piora o tempo de carregamento?
Mede a página com as ferramentas Lighthouse uma vez com e uma vez sem o script e compara LCP, INP e CLS. Um widget carregado de forma assíncrona, com launcher fixo e sem fontes puxadas, não altera os três valores de forma mensurável.
Posso carregar o widget só depois do consentimento?
Sim. A tag de script não é então escrita estaticamente no documento, mas gerada pela ferramenta de consentimento assim que a visitante concordou. Também uma tag gerada assim monta o widget uma vez no documento, e depois sobrevive a todas as mudanças de rota como uma integrada estaticamente.