wandres.dev
TERCEROS · El código que no controlas

Partytown: el modelo, el truco y los límites reales

Cómo se ejecuta un script de terceros en un worker cuando el DOM solo existe en el hilo principal, por qué el proxy síncrono es rápido y caro a la vez, y la lista honesta de lo que funciona y lo que no.

⏱ 19 min

La idea es irresistible: si el problema de los terceros es que bloquean el hilo principal, muévelos a un worker. El problema es que un script de analítica hace document.querySelector y window.location, y en un worker no existe ninguna de las dos cosas. Partytown resuelve eso con un mecanismo ingenioso que conviene entender en detalle, porque de ese mecanismo salen exactamente sus límites, y esos límites deciden si la herramienta te sirve o te complica la vida.

🎯 Al terminar esta lección sabrás
  • Explicar cómo se sirve un acceso síncrono al DOM desde un worker.
  • Configurar Partytown con el reenvío de funciones que la página necesita llamar.
  • Predecir qué scripts van a funcionar y cuáles no a partir del modelo.
  • Decidir con criterio entre Partytown, una fachada y la retirada del tercero.

El problema y el truco

Un web worker no tiene DOM. No es una limitación arbitraria: el DOM no es seguro para acceso concurrente, y darle acceso a otro hilo obligaría a sincronizarlo todo. Por eso la comunicación entre worker y hilo principal es asíncrona, por mensajes.

Y ahí está el choque. Un script de terceros escrito hace años hace esto:

var ancho = document.documentElement.clientWidth;
var ref = document.referrer;
enviar({ ancho: ancho, ref: ref });

Es código síncrono. No se puede reescribir a mensajes asíncronos sin reescribir el script, y el script no es tuyo.

El truco de Partytown consiste en dar al worker un document y un window falsos —objetos proxy— cuyos accesos se traducen en una llamada síncrona y bloqueante al hilo principal. Como bloquear el worker no bloquea la interfaz, la espera es invisible para el usuario.

El mecanismo por defecto para esa llamada síncrona es un truco genuino: el worker hace una petición XMLHttpRequest síncrona —cosa que en un worker está permitida y en el hilo principal está desaconsejada precisamente por bloquear— a una URL que un service worker propio intercepta. El service worker consulta al hilo principal, obtiene el valor y responde a la petición. Desde el punto de vista del código del worker, document.title ha devuelto una cadena de forma síncrona; por debajo ha habido una petición HTTP falsa, un salto al hilo principal y una vuelta.

Existe un segundo mecanismo, basado en Atomics.wait sobre memoria compartida, considerablemente más rápido. Y tiene una pega que hay que conocer antes de contar con él: la memoria compartida requiere que el documento esté aislado entre orígenes, con las cabeceras de política de apertura y de recursos correspondientes. Ese aislamiento rompe la mayoría de las incrustaciones de terceros, que es justo la población con la que estabas trabajando. La paradoja es real: el modo rápido de la herramienta para terceros exige una configuración incompatible con muchos terceros.

El coste que se traslada

Aquí está la parte que no se explica lo bastante y que decide el resultado. Partytown no elimina el trabajo del hilo principal: elimina la parte del trabajo que no toca el DOM.

Descomponiendo lo que hace un script de terceros:

Parte del trabajo Con Partytown
Análisis y compilación del script Se va al worker. Ganancia limpia.
Lógica propia, cálculos, serialización Se va al worker. Ganancia limpia.
Cada lectura o escritura del DOM Sigue en el hilo principal, y ahora con el sobrecoste del proxy
Peticiones de red que hace el script Igual que antes

La consecuencia es una regla de predicción que funciona bien: la ganancia es proporcional a cuánto cómputo propio hace el script y la pérdida es proporcional a cuántas veces toca el DOM. Un script que se ejecuta una vez, lee ocho propiedades y manda una petición es el caso ideal: casi todo su coste es análisis y compilación, y ocho saltos al hilo principal no se notan. Un script que recorre el DOM buscando elementos, o que instala un observador de mutaciones y serializa cambios continuamente, hace miles de accesos, y cada uno paga el ida y vuelta del proxy. Ese script no solo no mejora: empeora, y puede empeorar mucho.

De ahí sale la clasificación práctica, que es más fiable que cualquier lista de compatibilidad:

Funciona bien: etiquetas de medición que envían datos y no leen apenas. Analítica de páginas vistas, píxeles de conversión, envío de eventos.

Funciona regular: contenedores de etiquetas. Depende enteramente de qué cargue dentro. El contenedor en sí es un buen candidato; una de sus etiquetas puede no serlo.

No funciona: grabación de sesión y mapas de calor, porque su trabajo es observar el DOM. Widgets que dibujan interfaz. Pruebas A/B que modifican la página, que además tienen que ser síncronas por definición para no producir parpadeo. Cualquier cosa que use document.write, canvas, o que necesite medir geometría.

La configuración mínima que funciona

Tres piezas. El fragmento en el head, antes de cualquier script convertido:

<script>
  partytown = {
    // Funciones que el codigo de la pagina llama y que viven en el worker.
    // Sin esto, llamar a gtag desde tu codigo no llega a ninguna parte.
    forward: ['dataLayer.push', 'gtag', 'fbq'],
    // Solo en desarrollo: registra cada acceso proxiado.
    debug: false,
    // Ruta donde has copiado los ficheros de la libreria.
    lib: '/~partytown/',
  };
</script>
<script src="/~partytown/partytown.js"></script>

El cambio en cada script que quieras mover:

<!-- Antes -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"></script>

<!-- Despues: el navegador ignora este tipo, Partytown lo recoge -->
<script type="text/partytown" async
        src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"></script>

Y el reenvío de proxy en el servidor, que es el paso que más veces se olvida. El worker no puede pedir directamente un script de otro origen sin CORS, así que la librería pide a través de un endpoint tuyo:

// Ruta /~partytown/proxy en tu servidor.
const PERMITIDOS = new Set([
  'https://www.googletagmanager.com',
  'https://connect.facebook.net',
]);

export async function GET({ url }) {
  const destino = url.searchParams.get('url');
  if (!destino) return new Response('Falta url', { status: 400 });

  const objetivo = new URL(destino);
  // Lista blanca obligatoria: sin esto has construido un proxy abierto.
  if (!PERMITIDOS.has(objetivo.origin)) {
    return new Response('Origen no permitido', { status: 403 });
  }

  const respuesta = await fetch(objetivo, { headers: { accept: '*/*' } });
  return new Response(respuesta.body, {
    status: respuesta.status,
    headers: {
      'content-type':
        respuesta.headers.get('content-type') || 'text/javascript',
      'cache-control': 'public, max-age=600',
    },
  });
}

Esa lista blanca no es una recomendación de estilo. Un endpoint que reenvía cualquier URL que le pasen es un proxy abierto: sirve para atacar a terceros desde tu dominio, para saltarse restricciones de red y para hacer que tu servidor pida URLs internas. Es un fallo de seguridad clásico y aparece en implementaciones reales con más frecuencia de la que debería.

Tres cosas más que hay que tener presentes al desplegar esto.

El orden de ejecución cambia. Los scripts en el worker arrancan después que los normales. Si algo de tu página llamaba a gtag en los primeros milisegundos, esa llamada tiene que estar en forward para que se encole; si no, se pierde en silencio, que es la peor forma de perder eventos porque los números bajan sin que nada falle.

Partytown registra su propio service worker. Si ya tienes uno, hay que coordinarlos. Con lo visto en el ciclo de vida, esto no es catastrófico, pero es una complicación real que hay que planificar y no descubrir en producción.

La depuración se complica bastante. El código se ejecuta en otro hilo, a través de proxies, con las URL reescritas. Cuando algo no va, el modo de depuración es imprescindible y aun así cuesta más que depurar el mismo script en el hilo principal.

Cuándo elegirlo

La comparación honesta con las alternativas, para el mismo tercero:

Opción Coste en hilo principal Riesgo de rotura Esfuerzo Efecto en el beneficio del tercero
Quitar el tercero Cero Ninguno Bajo Se pierde entero
Fachada Cero para quien no lo usa Bajo Medio Intacto para quien lo usa
Carga en tiempo libre Se desplaza, no se elimina Muy bajo Muy bajo Intacto con retraso
Partytown Se reduce si el script no toca el DOM Medio-alto Alto Intacto si no se rompe

El orden de evaluación que recomiendo, y que evita mucho trabajo perdido: primero pregúntate si el tercero se puede quitar, que resuelve el problema y no cuesta nada; después si admite fachada, que es barato y seguro; después si basta con retrasarlo a tiempo libre, que es una línea de código y a menudo suficiente; y solo si nada de eso vale, Partytown.

La situación en la que Partytown es la respuesta correcta existe y es bastante concreta: un conjunto de etiquetas de medición que no se pueden eliminar por motivos de negocio, que no admiten fachada porque no tienen interfaz, y cuyo coste de arranque es grande. Ahí es una buena herramienta y hace lo que promete. Fuera de ahí, la complejidad que añade se paga durante años.

Y una nota de prudencia sobre la elección: es un proyecto especializado, con una adopción moderada, cuya API y cuyos modos de ejecución han ido cambiando. Antes de comprometer una arquitectura, comprueba su estado actual y su ritmo de mantenimiento, y prueba tus etiquetas concretas en un entorno de preproducción con la verificación de que los eventos siguen llegando a su destino. Esa verificación —contar eventos en el panel del proveedor, no en tu consola— es imprescindible: el modo de fallo característico de esta herramienta no es una excepción visible, es una analítica que deja de contar el ocho por ciento de las conversiones sin que nadie se dé cuenta durante un trimestre.

Mover un problema de hilo no lo resuelve: lo convierte en un problema de comunicación, y el ancho de banda entre hilos es el nuevo cuello de botella

Hay un patrón de razonamiento que se repite en toda la ingeniería de rendimiento y que conviene reconocer, porque parece profundo y a menudo es un espejismo: este trabajo va en el hilo equivocado, lo muevo, problema resuelto. Aparece con Partytown, aparece con los workers para procesar datos, aparece al mover cómputo al borde como vimos en el cómputo en el borde, y aparece con la GPU. En los cuatro casos la premisa oculta es la misma y en los cuatro casos es falsa: que el trabajo movido es autónomo. Casi nunca lo es. El trabajo movido necesita datos que están donde estaba, y produce resultados que hay que devolver, y esa comunicación tiene un coste que no existía antes. Lo que has hecho no es eliminar el problema sino cambiarlo de naturaleza: donde tenías un problema de cómputo ahora tienes uno de comunicación, y el criterio que decide si has ganado es la relación entre cuánto cómputo se va y cuánta comunicación aparece. Partytown es un caso de estudio limpísimo de esto porque el ratio es explícito y contable: cómputo propio del script frente a número de accesos al DOM. Un script con mucho de lo primero y poco de lo segundo gana; al revés pierde, y no hay ninguna versión futura de la librería que pueda cambiarlo, porque el coste está en la frontera, no en la implementación. Lo mismo, exactamente lo mismo, decide si compensa mover un cálculo a un worker: no depende de cuánto tarda el cálculo, depende de cuántos datos hay que copiar para dárselo y cuántos devolver. Y lo mismo decide si compensa el borde: no depende de cuánto tarda tu función, depende de cuántas idas y vueltas hace contra los datos. La forma operativa de aplicarlo es un hábito muy simple que ahorra proyectos enteros: antes de mover trabajo a otro sitio, cuenta los cruces de frontera y estima el coste de cada uno. Si el resultado es del mismo orden que el trabajo que estás moviendo, no lo muevas; el trabajo que hay que hacer es otro, y normalmente consiste en reducir los cruces —agrupar, cachear del lado correcto, cambiar el formato de los datos— antes de plantearse siquiera la mudanza.

⚔️ Evalúa antes de instalar
  1. Coge tus terceros y clasifícalos según cuánto cómputo propio hacen frente a cuántos accesos al DOM. Predice cuáles ganarían.
  2. Antes de tocar Partytown, comprueba cuáles se pueden quitar, cuáles admiten fachada y cuáles bastaría con retrasar a tiempo libre.
  3. Si algún candidato sobrevive a los tres filtros, móntalo en preproducción con el modo de depuración y cuenta los accesos proxiados.
  4. Verifica en el panel del proveedor, no en tu consola, que los eventos siguen llegando con el mismo volumen que antes.
  5. Revisa que tu endpoint de reenvío tiene lista blanca de orígenes. Si no la tiene, es un proxy abierto.