Todos los artículos
Inserción10 min de lectura

Insertar un widget de soporte en single-page apps: cambios de ruta, Shadow DOM, tiempo de carga

Lo que un widget de soporte tiene que saber hacer en una single-page app: sobrevivir a los cambios de ruta, aislar sus estilos, no frenar la página. Con los motivos técnicos detrás.

Martin Semmele

Un escritorio ordenado con un monitor grande que solo muestra manchas de color suaves y desenfocadas, delante un teclado y un cuaderno cerrado, detrás una pared de ladrillo y una ventana
Un widget de soporte es un script de terceros en una página que ha construido otra persona. La inserción decide si los dos se llevan bien. · Generado con IA

Conclusiones principales

  • El 92 por ciento de todas las páginas web carga al menos un recurso ajeno; un widget de soporte es uno de ellos y tiene que comportarse en consecuencia.
  • Un widget que se monta una vez en el documento y no vive en el árbol de la aplicación sobrevive a cualquier cambio de ruta del lado del cliente sin intervención.
  • Shadow DOM separa los estilos en ambas direcciones, pero no el foco del teclado, las fuentes ni el orden de apilamiento frente a un banner de cookies.
  • Un script con el atributo async no bloquea el análisis de la página; esa es la única línea que decide el tiempo de carga.
  • El doble montaje, los identificadores de conversación perdidos y un paquete de fuentes cargado desde un servicio externo son los tres errores que aparecen con más frecuencia en la práctica.

Por qué un script ajeno vive de otra forma en una single-page app

En una web clásica, cada clic carga una página nueva. Un script que está al final del documento se ejecuta así de nuevo en cada visita a la página, y todo lo que quiera recordar tiene que guardarlo en algún sitio. En una single-page app es al revés: el documento se carga exactamente una vez y, a partir de ahí, el framework solo intercambia partes del árbol. Un script que ya se ha ejecutado una vez no vuelve a ejecutarse.

Para un widget de soporte, eso es en principio una buena noticia. No tiene que reconstruirse en cada cambio de ruta, y la conversación abierta simplemente sigue abierta. La mala noticia se deriva de la misma circunstancia: todo lo que el widget hace mal también persiste, hasta que alguien recarga la página por completo. Un conflicto de estilos, un segundo launcher abajo a la derecha, un foco que se queda atrapado en el widget: nada de eso lo limpia un cambio de ruta.

Según el Web Almanac 2024, el 92 por ciento de las páginas analizadas carga al menos un recurso ajeno, y entre las mil páginas más grandes la mediana se sitúa en 66 terceros distintos1. Tu widget de soporte es uno de ellos. Compite con scripts de analítica, banners de cookies y paquetes de fuentes por el tiempo de carga, el orden de apilamiento y el foco del teclado. Las tres secciones siguientes desmontan los tres conflictos uno por uno.

Cambios de ruta: el widget no pertenece al árbol de la aplicación

El error más frecuente al insertarlo en React, Vue o Angular está bien intencionado: el widget se incorpora a la aplicación como componente para que quede «limpio» en el árbol. Con eso queda ligado al ciclo de vida de ese componente. Cuando se cambia de ruta y el layout se vuelve a renderizar, el widget se desmonta y se vuelve a montar. El efecto visible: la ventana de chat se cierra, el historial desaparece o aparece duplicado y, según la implementación, se abre una segunda conversación.

Montar una vez en el documento, no en un componente.

La variante robusta es la más sencilla: el script se carga una vez, cuelga su contenedor directamente del elemento body y vive así fuera de todo lo que gestiona el framework. Un cambio de ruta intercambia nodos dentro del contenedor de la aplicación; el contenedor del widget, al lado, queda intacto. Exactamente así trabaja el widget de Comlayer: lee su propia etiqueta script a través de document.currentScript, crea un único elemento host en el body y comprueba antes del montaje si ese elemento ya existe. Si el script se ejecuta una segunda vez, por ejemplo porque un framework repite el código de inserción en un nuevo render, no pasa nada.

Dos sutilezas importan aquí. Primera: document.currentScript no devuelve nada cuando el código se ejecuta desde un callback, un evento o un módulo de JavaScript2. Un widget que depende de ello necesita un mecanismo de respaldo, por ejemplo buscar la última etiqueta script con el atributo de datos correspondiente. Segunda: la dirección de la página que se asigna a una conversación es la dirección del momento en que se abrió la conversación. Si la visitante cambia después cinco veces de ruta, en la bandeja de entrada se queda la dirección inicial. No es un error, sino una decisión; quien necesite la dirección actual tiene que preguntarla en el propio mensaje.

Dónde vive el historial de la conversación.

Para que una conversación sobreviva a una recarga completa, el widget tiene que recordar el identificador de la visitante y el de la conversación. El lugar habitual es el localStorage del navegador, guardado bajo un prefijo que contiene la clave del widget. Dos widgets en el mismo dominio, por ejemplo en una página de marketing y en la aplicación que hay detrás, no se estorban así. Lo que no debe estar ahí es el contenido de la conversación en sí: ese está en el proveedor, y el widget lo carga a partir del identificador. Si el almacenamiento desaparece, porque la visitante lo borra o navega en modo privado, en la siguiente visita empieza una conversación nueva, y la antigua se conserva en la bandeja de entrada del equipo.

Lugar de inserciónComportamiento en un cambio de rutaComportamiento en una recarga completa
Como componente en el árbol de la aplicaciónSe desmonta y se vuelve a montar según el layout; el historial salta o se duplicaSe vuelve a montar, historial recargado desde el almacenamiento
Como script en el documento, montado una vezIntacto, la conversación sigue abiertaSe vuelve a montar, historial recargado desde el almacenamiento
Insertado a través de un gestor de etiquetasComo en el documento, siempre que la etiqueta se dispare una sola vezComo en el documento

Shadow DOM: qué consigue la separación de estilos y qué no

Un widget trae sus propios estilos, y tu página tiene los suyos. Sin separación, un button { border-radius: 0 } global en tu hoja de estilos se aplica también al launcher del widget y, al revés, un selector del widget formulado con demasiada amplitud colorea tus formularios. El camino estándar para evitarlo es Shadow DOM: el widget renderiza en un subárbol propio, cuyos estilos no se alcanzan desde fuera y que a su vez no actúa hacia fuera. La documentación de Mozilla lo resume así: el CSS de la página no actúa sobre los nodos del Shadow DOM, y los estilos del Shadow DOM no actúan sobre el resto de la página3.

Cuatro cosas que la frontera no detiene.

La separación de estilos es completa, pero es solo una separación de estilos. Cuatro cosas cruzan la frontera de todos modos, y las cuatro aparecen en los tickets de soporte a los proveedores de widgets:

  • Propiedades heredadas. La familia tipográfica, el tamaño de letra y el color del texto se heredan del elemento host si el widget no los fija por sí mismo. Un widget que no define explícitamente su fuente tiene un aspecto distinto en cada página.
  • Foco del teclado. La tecla Tab recorre toda la página, con o sin Shadow DOM. Una ventana de chat abierta que no retiene el foco por sí misma deja que el teclado vague por la navegación de la página detrás de la ventana.
  • Orden de apilamiento. Un banner de cookies con z-index: 99999 se coloca sobre el launcher si el widget elige un valor menor. Por eso los widgets ponen su host en el valor más alto posible y lo aíslan con isolation: isolate, para que ese valor extremo no actúe en los contextos de apilamiento de la página.
  • Idioma y dirección de escritura. lang y dir se heredan del elemento host. Una página en hebreo o en árabe cambia así también el widget a la escritura de derecha a izquierda, sepa hacerlo o no.

Un Shadow DOM en modo open no es una medida de seguridad: el JavaScript de la página puede seguir entrando a través de shadowRoot. El modo regula solo el acceso por script, no la separación de estilos, y esa rige en ambos modos3.

Tiempo de carga: una línea decide

Un script ajeno cuesta tiempo de carga. La única cuestión es si cuesta ese tiempo en segundo plano o en primer plano, mientras la visitante mira una página vacía. La diferencia es un atributo: una etiqueta script sin async ni defer detiene el análisis del documento hasta que el script se ha cargado y ejecutado. La recomendación de los desarrolladores de Chrome es por eso inequívoca: cargar los scripts ajenos siempre de forma asíncrona, salvo que el script tenga que ejecutarse antes de que la página pueda renderizarse4. Un widget de soporte nunca tiene que hacerlo.

Async, no defer, y por qué el widget no carga fuentes externas.

async ejecuta el script en cuanto está cargado, independientemente del orden en el documento. defer espera a que el documento esté completamente analizado y respeta el orden4. Para un widget que no necesita nada de la página salvo el elemento body, async es la elección correcta; puede estar listo antes que el resto sin problema. La inserción de Comlayer consiste por eso en una única etiqueta con exactamente ese atributo, por ejemplo <script src="https://app.comlayer.app/comlayer-widget.js" data-app-id="wgt_…" async></script>. No existe una interfaz de JavaScript para abrir, cerrar o notificar un cambio de ruta, y no falta: el widget no la necesita, porque vive fuera de la aplicación.

La segunda partida en la cuenta del tiempo de carga suele pasarse por alto: las fuentes. Un widget que carga su tipografía corporativa mediante @import desde un servicio de fuentes provoca en cada página de cliente una petición más a un servicio más, para cada visitante individual. Eso cuesta tiempo y es, según el servicio y su sede, además una transferencia de datos que tendría que figurar en el aviso de privacidad de la página del cliente. Comlayer ha eliminado ese import del widget y recurre a la fuente del sistema; el paquete compilado ocupa así unos 34 kilobytes, transmitidos comprimidos, medido el 17 de septiembre de 2026.

MétricaUmbral para «bueno»Qué aporta un widget
Largest Contentful Paint (LCP)2,5 segundosSolo si el script se carga de forma síncrona o trae fuentes propias
INP (tiempo de reacción a las interacciones)200 milisegundosEjecución larga del script en el hilo principal al arrancar
Cumulative Layout Shift (CLS)0,1Un launcher que ocupa espacio tras la carga y desplaza el contenido

Los tres umbrales proceden de las Core Web Vitals y se aplican cada uno al percentil 75 de las visitas a la página, separando móvil y escritorio5. Un widget que se carga de forma asíncrona, posiciona su launcher de forma fija y no trae fuentes no aparece en ninguna de las tres cifras.

Inserción a través de un gestor de etiquetas

Muchos equipos no insertan los scripts ajenos en el código fuente, sino a través de un gestor de etiquetas. Para un widget de soporte eso está bien, con dos condiciones. Primera: la etiqueta tiene que dispararse exactamente una vez, al cargar el documento, y no en cada visita virtual a la página que la single-page app comunica al gestor de etiquetas. Una etiqueta que escucha el evento «visita a la página» vuelve a insertar el script en cada cambio de ruta. Un widget que intercepta por sí mismo un doble montaje lo perdona; uno que no lo hace muestra después dos launchers.

Segunda: un script insertado a través de un gestor de etiquetas puede no ejecutarse ya como la etiqueta script que habría sido en el código fuente. Un widget que lee su clave a través de document.currentScript no encuentra entonces nada y necesita el respaldo descrito arriba. Comprueba tras la inserción, en la consola, si del body cuelga exactamente un elemento host y si el widget ha encontrado su clave. Ambas cosas se ven en las herramientas de desarrollo en segundos.

Consentimiento y la carga tras el clic

En la Unión Europea, la inserción plantea una pregunta que solo de forma marginal tiene que ver con la técnica: ¿puede el script cargarse de inmediato, o solo tras un consentimiento? La calificación jurídica es discutida, y la hemos expuesto en detalle en un artículo propio sobre el widget de soporte y el consentimiento. Técnicamente, la variante prudente significa: la etiqueta script no se escribe estáticamente en el documento, sino que la genera la herramienta de consentimiento en cuanto la visitante ha aceptado.

Para una single-page app eso no cambia nada en el principio básico. También una etiqueta script generada a posteriori cuelga su elemento host una vez del body y permanece ahí a través de todos los cambios de ruta. Lo que cambia es el momento: el widget aparece solo tras el clic, y una conversación que antes del consentimiento no era posible tampoco puede perder ningún historial. Quien elige esta variante debería reservar de todos modos el lugar del launcher, para que la página no salte cuando el widget aparece.

La lista de comprobación tras la inserción

Si una inserción es limpia se puede comprobar en pocos minutos. Los puntos siguientes cubren los errores que aparecen con más frecuencia en la práctica:

  1. 01Abre una conversación y cambia después tres veces de ruta. La ventana sigue abierta, el historial se mantiene, no se crea una segunda conversación.
  2. 02Recarga la página por completo. Tras la carga el historial vuelve a estar ahí, y el identificador de conversación en el almacenamiento es el mismo que antes.
  3. 03Comprueba en las herramientas de desarrollo que del body cuelga exactamente un elemento host, también tras varios cambios de ruta y una vuelta a la ruta inicial.
  4. 04Abre el banner de cookies mientras la ventana de chat está abierta. Ambos deben seguir siendo utilizables, ninguno puede tapar al otro.
  5. 05Recorre con la tecla Tab la ventana de chat abierta. El foco debe ser visible y no puede desaparecer en la página detrás de la ventana.
  6. 06Comprueba en la pestaña de red que tras el script del widget no se carga ningún archivo de fuentes desde un servicio externo.
  7. 07Mide la página con las herramientas Lighthouse, una vez con y otra sin widget. Las tres Core Web Vitals no pueden empeorar.

Comlayer supera esta lista porque el widget está construido exactamente para eso: una etiqueta script, un host en el body, un Shadow DOM con estilos propios, sin fuentes externas, sin una interfaz que un framework tuviera que manejar. El aspecto del widget y los módulos que lleva los configuras en el panel, sin reconstruir la página; lo que hace con los datos de las visitantes está en la página de seguridad y privacidad.

Preguntas frecuentes

¿Tengo que reinicializar el widget en cada cambio de ruta?

No. Un widget que se monta una vez en el documento y no vive en el árbol de la aplicación permanece a través de todos los cambios de ruta del lado del cliente. Una inicialización por ruta solo es necesaria si el widget se ha incorporado a la aplicación como componente, y eso es justo lo que hay que evitar.

¿Qué pasa si el script se inserta dos veces?

Depende del widget. Un widget bien construido comprueba antes del montaje si su elemento host ya existe y la segunda vez no hace nada. Un widget sin esa comprobación muestra dos launchers y, según el caso, abre dos conversaciones. Los gestores de etiquetas que se disparan en cada visita virtual a la página son la causa más frecuente.

¿Protege Shadow DOM por completo mi hoja de estilos frente al widget?

Para los selectores, sí: los estilos de la página no alcanzan el Shadow DOM, y los estilos del widget no alcanzan la página. Las propiedades heredadas como la familia tipográfica y el color del texto, el foco del teclado, el orden de apilamiento y los atributos lang y dir cruzan la frontera de todos modos.

¿Async o defer para el script del widget?

Async. El widget no necesita nada de la página salvo el elemento body y puede ejecutarse en cuanto está cargado. defer no sería incorrecto, pero espera innecesariamente al final del análisis. Lo único importante es que uno de los dos atributos esté puesto; sin ninguno de los dos, el script bloquea el renderizado.

¿Cómo reconozco si el widget empeora el tiempo de carga?

Mide la página con las herramientas Lighthouse una vez con y otra sin el script y compara LCP, INP y CLS. Un widget cargado de forma asíncrona, con launcher fijo y sin fuentes externas, no cambia de forma medible los tres valores.

¿Puedo cargar el widget solo después del consentimiento?

Sí. La etiqueta script no se escribe entonces estáticamente en el documento, sino que la genera la herramienta de consentimiento en cuanto la visitante ha aceptado. También una etiqueta generada así monta el widget una vez en el documento, y después sobrevive a todos los cambios de ruta igual que una insertada estáticamente.

Fuentes

  1. 01almanac.httparchive.org
  2. 02developer.mozilla.org
  3. 03developer.mozilla.org
  4. 04web.dev
  5. 05web.dev

Empieza gratis · Sin tarjeta de crédito

Configurado esta noche. Respondiendo ya mañana por la mañana.

Inserta el widget, añade tu conocimiento, listo — ComLayer se encarga, incluso cuando no hay nadie delante del ordenador.