Performance

Como Beaconry sobrevive a LiteSpeed, WP Rocket y Cloudflare

Los plugins de cache y de optimizacion de JS son los asesinos silenciosos del tracking de conversiones. Delay-JS espera un clic que a menudo nunca llega, Combine reordena tu bloque de init, Rocket Loader reescribe tu etiqueta de script. Beaconry se excluye a si mismo de todo optimizador de JS que pueda verificar contra la propia documentacion del proveedor, y refresca su nonce REST en el navegador, para que una pestana abierta durante horas, o una pagina servida desde el cache de pagina completa, siga enviando eventos validos. Esto es exactamente lo que hace y los dos casos que aun tienes que resolver a mano.

Tiempo de lectura: ~8 minPublicado: 2026-06-08

Dos modos de fallo, y son distintos

La gente mete "el plugin de cache rompio mi tracking" en un solo cajon. En realidad son dos problemas sin relacion entre si, y Beaconry los resuelve con dos mecanismos sin relacion entre si.

El primero es la optimizacion de JavaScript: Delay JavaScript Execution, Combine JS, Minify y Cloudflare Rocket Loader. Estos reescriben o reprograman el script que dispara tus eventos. El segundo es el cache de HTML de pagina completa: el mismo documento HTML, incluido el nonce de seguridad incrustado, se sirve a miles de visitantes y al mismo visitante horas despues. El nonce que contiene queda obsoleto. El cache de pagina no toca tu JavaScript en absoluto, asi que un arreglo para uno no hace nada por el otro.

Si solo piensas en la optimizacion de JS, lanzas un plugin que igual se apaga en cualquier sitio con una pestana de larga vida o un cache de pagina agresivo. Si solo piensas en el cache de pagina, lanzas uno cuyo bloque de init se aplaza hacia el olvido. Necesitas ambos.

Por que Delay-JS es el que de verdad duele

La mayoria de los optimizadores de JS degradan el tracking. Delay JavaScript Execution lo destruye. La funcion esta construida para retener todo script no critico hasta la primera interaccion del usuario (scroll, mousemove, touch, tecla), para que la pagina logre una gran puntuacion de Lighthouse con cero JS en la ruta critica. Maravilloso para las metricas de pintado. Fatal para un evento de pageview, porque una gran parte de las sesiones, rebotes, visitas moviles de un toque y fuera, paginas precargadas, nunca produce esa primera interaccion. Sin interaccion, sin script, sin evento. La conversion ocurrio y tus analiticas nunca se enteraron.

Combine JS y Rocket Loader son mas sutiles. No bloquean la ejecucion del todo, la reordenan. Beaconry encola dos cosas: el archivo externo nl-data.js (deferred, en el footer) y un pequeno bloque inline que llama a NLData.init() con la config de runtime y el nonce de seguridad de la pagina. Si un combinador fusiona el archivo externo en un solo bundle pero deja el init inline donde estaba, o Rocket Loader reescribe la etiqueta del archivo como async, la llamada inline puede ejecutarse antes de que exista el motor del que depende. El pageview lanza un error en lugar de dispararse.

Asi que el objetivo es estrecho y especifico: mantener los dos scripts de Beaconry fuera de todo optimizador de JS, sin desactivar esos optimizadores para el resto del sitio. El cliente conserva su puntuacion Lighthouse de 95+ en todo lo demas. Solo se extraen dos handles de script.

El problema de las dos superficies

Aqui esta el detalle con el que tropiezan la mayoria de los plugins de tracking. WordPress te da el filtro script_loader_tag para reescribir la etiqueta HTML de un script, que es como anades un atributo de exclusion. Pero ese filtro solo ve siempre la etiqueta del archivo externo, el <script src="...nl-data.js">. Nunca ve el bloque inline separado que emite wp_add_inline_script(), el que en realidad llama a NLData.init() y dispara el pageview. Ese bloque inline se renderiza como su propio elemento <script id="bcnr-nl-data-js-after">, y script_loader_tag no puede tocarlo.

Lo que significa que una exclusion basada en atributos protege el archivo del motor pero deja expuesta la linea que lo ejecuta. Tienes que proteger ambas superficies, y necesitan mecanismos distintos:

  • El archivo externo se protege con atributos de etiqueta (para Cloudflare y WP Rocket Delay) mas filtros de exclusion basados en handle (para LiteSpeed y SiteGround).
  • El bloque de init inline se protege con filtros de exclusion por subcadena de contenido, porque no hay atributo de etiqueta que anadir. Beaconry registra la subcadena estable NLData.init en los dos optimizadores que exponen un hook documentado de exclusion inline.

Los dos handles en cuestion son bcnr-nl-data y bcnr-nl-data-gate (el motor de tracking y el motor del banner de consentimiento). Todo lo de abajo opera exactamente sobre esos dos y nada mas, asi que es idempotente y sin efectos secundarios para el resto de tus scripts.

La matriz de exclusion verificada en la documentacion

Esta es la parte en la que la GOTT-REGEL demuestra su valor: cada uno de los marcadores de abajo se confirmo palabra por palabra contra la propia documentacion del plugin de cache, no contra un post de blog, no contra un hilo de foro. Una pasada de verificacion adversarial elimino 30 de 41 marcadores candidatos porque eran folclore que nunca aparece en una documentacion oficial. Esto es lo que paso el corte.

Etiqueta de script externa

El filtro script_loader_tag de Beaconry antepone dos atributos a sus propias dos etiquetas, en el orden que exigen los proveedores:

<script data-cfasync="false" nowprocket
        src="...assets/vendor/nl-data/nl-data.js" defer></script>
  • data-cfasync="false" le dice a Cloudflare Rocket Loader que ignore el script. Cloudflare exige que este atributo aparezca antes de src, razon por la cual Beaconry lo inyecta justo despues del <script de apertura.
  • nowprocket le dice a WP Rocket que nunca aplique Delay JavaScript Execution al archivo. Este es el marcador que mas importa, porque Delay-JS es el optimizador que rompe el tracking por completo en lugar de solo reordenarlo.

Exclusiones basadas en handle (LiteSpeed y SiteGround)

LiteSpeed Cache y SiteGround Speed Optimizer no exponen un atributo de etiqueta. Exponen hooks de filtro que toman una lista de handles de script. Beaconry anade sus dos handles a cada uno:

  • LiteSpeed, tres filtros que cubren cada eje de JS: litespeed_optimize_js_excludes (Combine y Minify), litespeed_optm_js_defer_exc (Defer y Delay), litespeed_optm_gm_js_exc (Guest Mode JS, la variante optimizada que LiteSpeed sirve a los visitantes no logueados).
  • SiteGround, tres filtros: sgo_js_async_exclude (async y defer), sgo_js_minify_exclude (minify), sgo_javascript_combine_exclude (combine).

Exclusiones de bloque inline (WP Rocket y SiteGround)

Para el bloque inline NLData.init(), dos optimizadores exponen una exclusion documentada por subcadena de contenido. Beaconry registra la subcadena NLData.init en ambos:

  • WP Rocket: rocket_defer_inline_exclusions, asi que el bloque de init nunca se aplaza ni se retrasa.
  • SiteGround: sgo_javascript_combine_excluded_inline_content, asi que el bloque de init nunca se pliega en un bundle combinado.

Todo esto se registra automaticamente al cargar el plugin. No hay ningun ajuste que activar. Si usas uno de estos plugins, los filtros ya estan adjuntos y no hacen nada de forma limpia si el plugin esta ausente.

Las dos excepciones manuales

La honestidad importa mas que una tabla de compatibilidad limpia. Dos optimizadores no pueden excluirse automaticamente, porque el proveedor no ofrece ningun hook a nivel de codigo para ellos. Si tienes activadas esas funciones especificas, anades una linea cada una a mano.

  • WP Rocket, Combine JavaScript. No hay ningun filtro ni atributo documentado para excluir un archivo o un script inline de Combine, solo los campos de la UI de admin bajo File Optimization. Si activas Combine JS, anade nl-data.js a "Excluded JavaScript Files" y un snippet NLData.init a "Excluded Inline JavaScript". Vale la pena saberlo: Combine es legacy y esta desactivado por defecto en HTTP/2 (que es casi todos los hosts ahora). Delay-JS es la funcion de WP Rocket que en realidad rompe el tracking, y esa SI se excluye automaticamente via nowprocket. Asi que en la practica la mayoria de los usuarios de WP Rocket no necesitan hacer nada.
  • Cloudflare, Auto Minify (legacy) o APO JS combine. Estos son toggles a nivel de zona en el dashboard, bajo Speed, Optimization, sin marcador de origen por script. Minify por si solo no rompe el orden de ejecucion, asi que es inofensivo. Rocket Loader es el unico optimizador de JS de Cloudflare que reordena la ejecucion, y ese SI se gestiona automaticamente con data-cfasync="false". Si usas APO con JS combine, excluye la ruta o deja JS combine desactivado.

Todo lo demas esta cubierto automaticamente. WP Super Cache, W3 Total Cache, Autoptimize y WP Fastest Cache en modo de cache de pagina simple nunca alcanzan el JavaScript de Beaconry, asi que no necesitan ninguna exclusion. Si usas uno de ellos con JS combine agresivo activado, aplica el mismo consejo generico: excluye nl-data.js de la optimizacion de JS. El cache de pagina por si solo ya es seguro, que es justo el sentido de la siguiente seccion.

El cache de pagina y el nonce obsoleto

Ahora el segundo modo de fallo. El endpoint REST de Beaconry, /wp-json/beaconry/v1/event, esta protegido por un nonce de WordPress. La config inline incrusta un wp_create_nonce('beaconry_event') fresco al renderizar, nl-data lo adjunta a cada POST de evento, y el servidor descarta cualquier evento cuyo nonce no se verifique. Ese nonce es parte de la capa antiabuso: sin el, cualquiera en la internet abierta podria hacer POST de conversiones falsas, quemar tu cuota de Meta CAPI o envenenar tus informes de GA4.

Los nonces de WordPress estan limitados en el tiempo. Rotan aproximadamente cada 12 horas y siguen validos durante unas 24. Eso esta bien para una pagina recien renderizada. Es un problema en el momento en que entra en juego el cache de pagina completa, porque ahora el nonce queda congelado en un documento HTML cacheado:

  • Un visitante abre una pestana y la deja abierta durante 14 horas. El nonce incrustado en el momento de la carga ya ha rotado fuera de vigencia.
  • Tu cache de pagina sirve un documento HTML generado hace 20 horas. Cada visitante que aterriza en el recibe un nonce que ya esta pasada su ventana.
  • Una sesion de invitado rota por debajo de una pagina de larga vida.

En todos los casos el nonce incrustado es ahora uno que el endpoint REST rechaza, asi que cada POST de evento se descarta silenciosamente. El usuario sigue en tu sitio, sigue convirtiendo, y los eventos se evaporan en la puerta.

Refresco de nonce en el cliente

El arreglo vive en el motor del navegador y es completamente independiente del plugin de cache que uses, eso es lo que lo hace robusto. nl-data sella el timestamp de cuando recibio su nonce. Antes de transmitir un evento, comprueba la edad del nonce. Si la pagina ha estado abierta mas tiempo que la ventana de refresco, obtiene un nonce fresco de un endpoint GET dedicado, lo intercambia y luego envia:

function ensureFreshNonce( cb ) {
    if ( !_config || !_config.nonce ) { cb(); return; }
    if ( ( Date.now() - _nonceTs ) < NONCE_REFRESH_MS ) { cb(); return; }
    var url = _config.endpoint.replace( /\/event$/, '/nonce' );
    fetch( url, {
        method:      'GET',
        credentials: 'same-origin',
        headers:     { 'Accept': 'application/json' },
    } )
        .then( function ( r ) { return r && r.ok ? r.json() : null; } )
        .then( function ( j ) {
            if ( j && j.nonce ) { _config.nonce = j.nonce; _nonceTs = Date.now(); }
        } )
        .catch( function () { /* keep existing nonce on failure */ } )
        .then( function () { cb(); } );
}

Los detalles que hacen esto seguro en lugar de ingenioso:

  • La ventana de refresco es de 6 horas, comodamente dentro de la ventana de validez de ~24 horas. Asi un nonce nunca se acerca a caducar antes de ser reemplazado, y el fetch de refresco se dispara como mucho un punado de veces a lo largo de una sesion de todo el dia, no en cada evento.
  • credentials: 'same-origin' envia las cookies de sesion, asi que el nonce recien acunado queda ligado a la misma sesion de invitado que el visitante ya tiene. Un nonce obtenido sin eso se verificaria contra una sesion distinta y seria inutil.
  • El fallo no es fatal. Si el fetch da error, el .catch conserva el nonce existente y el evento aun intenta enviarse. Un nonce obsoleto pero quiza valido le gana a no tener evento.
  • Las paginas frescas y los endpoints Worker hacen cortocircuito. Una pagina mas joven de 6 horas invoca el callback de forma sincrona sin salto de red, asi que el trafico normal nunca se retrasa. Los sitios del framework de NETZLICHT basados en Worker no llevan ningun nonce, asi que toda la ruta se omite para ellos.

Mantener el propio endpoint de nonce no cacheable

Hay una ultima trampa. El endpoint de refresco, /wp-json/beaconry/v1/nonce, es un request GET, y los caches de pagina completa como LiteSpeed cachearan con gusto las respuestas GET a /wp-json/*. Si esa respuesta se cachea, el refresco entrega el mismo nonce congelado que se suponia que debia reemplazar, y vuelves al punto de partida. Asi que el endpoint se marca a si mismo como no cacheable en ambos ejes:

do_action( 'litespeed_control_set_nocache', 'beaconry nonce must always be fresh' );

$response = new WP_REST_Response( [ 'nonce' => wp_create_nonce( 'beaconry_event' ) ], 200 );
$response->header( 'Cache-Control', 'no-store, max-age=0' );

El header estandar Cache-Control: no-store cubre la mayoria de los proxies y CDNs. LiteSpeed ignora el Cache-Control simple para sus propias decisiones de cache, asi que el endpoint tambien dispara litespeed_control_set_nocache, la API de control documentada de LiteSpeed. El do_action es un no-op inofensivo cuando LiteSpeed no esta instalado. Exponer el nonce en un GET publico no anade superficie de ataque, el mismo nonce ya esta a la vista en la config inline de cada pagina, y el verdadero candado es el rate-limit por IP mas los chequeos de consentimiento y de nonce en el propio endpoint de eventos.

Como verificarlo en tu propio sitio

No tienes que confiar en la tabla. Confirmalo en el navegador:

  • Ver el codigo fuente en una pagina cacheada. Encuentra la etiqueta <script> de nl-data.js. Deberia llevar data-cfasync="false" y nowprocket. Si esos atributos estan presentes, Rocket Loader y WP Rocket Delay quedan ambos excluidos.
  • Pestana Network de DevTools, deja la pestana abierta. Despues de que la pagina haya estado abierta mas de 6 horas (o baja temporalmente la constante en una copia de staging), al siguiente evento deberia precederle un GET a /wp-json/beaconry/v1/nonce que devuelve un valor fresco, y el POST de evento siguiente lo lleva.
  • Comprueba los headers de respuesta de ese GET. Deberia devolver Cache-Control: no-store. Golpealo dos veces y confirma que obtienes dos valores de nonce distintos, prueba de que el cache no lo esta congelando.
  • Observa el POST de evento. Un 204 con el nonce en el cuerpo del request significa que el servidor lo acepto. Si alguna vez ves que los eventos se detienen despues de que la pagina lleva tiempo abierta, la ruta del nonce es el primer sitio donde mirar.

Para llevar

El tracking del lado del servidor no sobrevive automaticamente a un stack de cache. Los eventos siguen originandose en un script de navegador, y ese script vive en el mismo entorno hostil que todos los demas de la pagina: optimizadores que lo retrasan, combinadores que lo reordenan, caches de pagina que congelan sus credenciales. Beaconry trata esto como dos problemas de ingenieria separados. Para la optimizacion de JS excluye sus dos handles de todo marcador que pueda verificar contra la propia documentacion de un proveedor, y te dice con claridad los dos casos (WP Rocket Combine, Cloudflare APO combine) en los que no existe ningun hook de codigo. Para el cache de pagina refresca el nonce REST en el navegador en un ciclo de 6 horas, ligado a la sesion del visitante, fallando de forma suave, con el propio endpoint de refresco mantenido como no cacheable. El resultado es un tracking que sigue funcionando en un sitio WordPress endurecido y fuertemente cacheado sin pedirte que debilites el cache.