Como medir Web Vitals en JS y enviarlas a la REST API en WordPress

Contents

Introducción

Este artículo explica, paso a paso y con ejemplos completos, cómo medir los Core Web Vitals (y métricas relacionadas) desde JavaScript en el navegador y enviarlas a una ruta de la REST API de WordPress para almacenarlas y analizarlas. Se incluyen ejemplos de código para el cliente (JavaScript) y para el servidor (PHP — plugin o mu-plugin), recomendaciones de seguridad, formatos de almacenamiento y buenas prácticas para producción.

Qué son Web Vitals y por qué recogerlos

Web Vitals son métricas estándar definidas por Google para medir la experiencia del usuario: LCP (Largest Contentful Paint), CLS (Cumulative Layout Shift), FID/INP (First Input Delay / Interaction to Next Paint), además de TTFB o FCP que habitualmente se registran para diagnóstico. Recoger estas métricas desde el navegador real (RUM — Real User Monitoring) permite detectar problemas reales en producción que no aparecen en pruebas sintéticas.

Requisitos y consideraciones

  • WordPress (puede ser plugin o tema que añada la lógica).
  • Acceso para añadir código JavaScript que se ejecute en el frontend en todas (o las páginas objetivo).
  • Decisión sobre autenticación: la mayoría de las métricas vienen de usuarios anónimos, así que la ruta REST suele dejarse pública con validaciones y controles (rates, sanitización, comprobación de referer / CORS).
  • Decidir cómo almacenar: tabla personalizada, post type, opciones batched o un servicio externo (BigQuery, Elasticsearch).

Instalación de la librería web-vitals

Para recoger los Web Vitals, la librería oficial web-vitals es la opción más sencilla y fiable. Puede importarse desde CDN o empaquetarse con su build.

Importar desde CDN (módulo ES)

Uso recomendado si no quieres empaquetar: importar como módulo desde unpkg. El ejemplo de envío también mostrará cómo usar sendBeacon o fetch con keepalive para no interferir con la navegación.

Medir métricas en el cliente (JavaScript)

Ejemplo de script que recoge LCP, CLS, INP (o FID), FCP y TTFB y los envía a la REST API del sitio. El script usa navigator.sendBeacon cuando está disponible, y fallback a fetch con keepalive.

/
  Ejemplo de cliente: recoge métricas con web-vitals y las envía
  Requisitos previos en el servidor: exponer un endpoint REST en /wp-json/webvitals/v1/collect
  Se asume que hay un objeto global window.WP_WEBVITALS con {endpoint, nonce?}
/
(async function () {
  // Dinámicamente importamos la librería web-vitals como módulo (CDN)
  const mod = await import(https://unpkg.com/web-vitals?module)
  const { getCLS, getFID, getLCP, getFCP, getTTFB, getINP } = mod

  // Helper para enviar al servidor
  function sendMetricToServer(metric) {
    const payload = {
      name: metric.name,
      value: metric.value,
      delta: metric.delta,
      id: metric.id, // id único por métrica
      navigationType: (performance.getEntriesByType(navigation)[0]  {}).type  null,
      url: location.pathname,
      ua: navigator.userAgent,
      ts: Date.now()
    }

    const body = JSON.stringify(payload)

    const endpoint = (window.WP_WEBVITALS  window.WP_WEBVITALS.endpoint)  /wp-json/webvitals/v1/collect
    const nonce = window.WP_WEBVITALS  window.WP_WEBVITALS.nonce

    // Preferir sendBeacon (no bloquea unload)
    if (navigator.sendBeacon) {
      const blob = new Blob([body], { type: application/json })
      // sendBeacon ignora cabeceras personalizadas como nonce si necesitas nonce, usa fetch
      const ok = navigator.sendBeacon(endpoint, blob)
      if (ok) return
    }

    // Fallback: fetch con keepalive
    const headers = { Content-Type: application/json }
    if (nonce) headers[X-WP-Nonce] = nonce

    try {
      fetch(endpoint, {
        method: POST,
        credentials: same-origin,
        headers,
        body,
        keepalive: true
      }).catch(() => {/ silencioso /})
    } catch (e) {
      // En algunos entornos keepalive puede lanzar lo ignoramos
    }
  }

  // Registramos observadores para cada métrica
  const onMetric = (metric) => {
    // Aquí puedes filtrar, agrupar, o debugar
    sendMetricToServer(metric)
  }

  getCLS(onMetric)
  // getFID puede estar deprecado en favor de getINP recoger ambos si quieres compatibilidad
  if (typeof getINP === function) {
    getINP(onMetric)
  } else {
    getFID(onMetric)
  }
  getLCP(onMetric)
  getFCP(onMetric)
  getTTFB(onMetric)
})()

Crear el endpoint REST en WordPress (PHP)

A continuación hay un ejemplo de plugin mínimo que registra la ruta REST, realiza validaciones y guarda las métricas en una tabla personalizada. El ejemplo incluye creación de tabla usando dbDelta en activation hook.

prefix . web_vitals
    charset_collate = wpdb->get_charset_collate()
    require_once( ABSPATH . wp-admin/includes/upgrade.php )
    sql = CREATE TABLE table (
      id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
      name VARCHAR(50) NOT NULL,
      value DOUBLE NOT NULL,
      delta DOUBLE DEFAULT 0,
      metric_id VARCHAR(100) DEFAULT ,
      url VARCHAR(255) DEFAULT ,
      ua TEXT,
      navigation_type VARCHAR(50),
      ts BIGINT UNSIGNED,
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
      PRIMARY KEY  (id)
    ) charset_collate
    dbDelta( sql )
}

add_action( rest_api_init, function () {
    register_rest_route( webvitals/v1, /collect, array(
        methods  => POST,
        // Se deja pública porque llegan usuarios anónimos validar los datos en callback.
        permission_callback => __return_true,
        callback => wvc_handle_collect,
    ) )
} )

function wvc_handle_collect( WP_REST_Request request ) {
    global wpdb
    table = wpdb->prefix . web_vitals

    content_type = request->get_header(content-type) ?: 
    // Aceptamos JSON
    if ( strpos( content_type, application/json ) === false ) {
        return new WP_REST_Response( array( error => Invalid content type ), 400 )
    }

    data = json_decode( request->get_body(), true )
    if ( ! is_array( data ) ) {
        return new WP_REST_Response( array( error => Invalid payload ), 400 )
    }

    // Validaciones básicas
    name = isset(data[name]) ? sanitize_text_field( data[name] ) : 
    value = isset(data[value]) ? floatval( data[value] ) : null
    delta = isset(data[delta]) ? floatval( data[delta] ) : 0
    metric_id = isset(data[id]) ? sanitize_text_field( data[id] ) : 
    url = isset(data[url]) ? sanitize_text_field( data[url] ) : 
    ua = isset(data[ua]) ? sanitize_textarea_field( data[ua] ) : 
    navigation_type = isset(data[navigationType]) ? sanitize_text_field( data[navigationType] ) : 
    ts = isset(data[ts]) ? intval( data[ts] ) : time()  1000

    if ( empty(name)  value === null ) {
        return new WP_REST_Response( array( error => Missing fields ), 400 )
    }

    // Protecciones simples: limitar longitud de UA y URL
    if ( strlen( ua ) > 2000 ) ua = substr( ua, 0, 2000 )
    if ( strlen( url ) > 255 ) url = substr( url, 0, 255 )

    // Inserción segura
    inserted = wpdb->insert(
        table,
        array(
            name => name,
            value => value,
            delta => delta,
            metric_id => metric_id,
            url => url,
            ua => ua,
            navigation_type => navigation_type,
            ts => ts,
        ),
        array( %s, %f, %f, %s, %s, %s, %s, %d )
    )

    if ( inserted === false ) {
        return new WP_REST_Response( array( error => DB error ), 500 )
    }

    return new WP_REST_Response( array( success => true ), 201 )
}
?>

Encolar el script en WordPress y pasar endpoint/nonce

Con el plugin anterior, puedes encolar un script frontend que importe la librería y utilice el endpoint. A continuación un ejemplo de cómo inyectar un objeto JS con la URL del endpoint y un nonce opcional (aunque el nonce normalmente solo tiene sentido para usuarios autenticados).

// Enqueue script (colocar en plugin o functions.php)
add_action( wp_enqueue_scripts, wvc_enqueue_scripts )
function wvc_enqueue_scripts() {
    // No encolamos web-vitals directamente porque lo importamos dinámicamente en el script
    wp_enqueue_script( wvc-client, plugin_dir_url(__FILE__) . webvitals-client.js, array(), 1.0, true )
    // Pasamos datos
    wp_localize_script( wvc-client, WP_WEBVITALS, array(
        endpoint => esc_url_raw( rest_url( webvitals/v1/collect ) ),
        nonce => wp_create_nonce( wp_rest ), // opcional
    ) )
}

Almacenamiento: tabla vs post type vs servicio externo

Opciones comunes:

  • Tabla personalizada: eficiente para almacenamiento en crudo, consultas agregadas y control del esquema. Requiere manejo de limpieza y particionado si hay alto volumen.
  • Custom Post Type: fácil de explorar desde la interfaz de WP, pero no es ideal para alto volumen ni consultas complejas.
  • Servicio externo (BigQuery, Elasticsearch, Data Warehouse): recomendado para análisis a gran escala. Envía métricas en batch desde el servidor a esos servicios.

Seguridad, privacidad y rendimiento

  1. Privacidad: Evita almacenar información personal identificable (PII). Nunca guardes correos ni tokens que identifiquen usuarios sin su consentimiento.
  2. Validación: Sanitiza y valida todo lo recibido. En el ejemplo se usa sanitize_text_field y floatval.
  3. Rate limiting: Implementa mecanismos para evitar spam o abuso (transients por IP, limitación por user agent, bloqueo si más de X req/s).
  4. Uso de sendBeacon / keepalive: para no afectar navegación/experiencia del usuario.
  5. CORS y Referer: puedes verificar el referer o usar políticas de CORS para restringir envíos desde otros orígenes.
  6. Alto volumen: agrupa envíos en el cliente (batch) o en el servidor añade un buffer antes de persistir para reducir I/O.

Depuración y pruebas

  • Prueba en distintos navegadores y dispositivos reales (no solo Lighthouse).
  • Usa los devtools (Network) para verificar los POST y el formato JSON.
  • Verifica el contenido de la tabla o destino y añade índices en columnas que uses para agregaciones (p. ej. name, ts).

Variantes y mejoras posibles

  • Enviar métricas en batch desde cliente cada N eventos o al unload para reducir requests.
  • Filtrar métricas para no almacenar valores triviales (p. ej. CLS = 0). Aunque a veces conviene guardar 0 para ver distribución.
  • Agregar etiquetas adicionales: country, device type, connection type (navigator.connection), versión del sitio, A/B test id.
  • Integrar con sistema de alertas: enviar un evento cuando LCP medio supera X ms para acciones automáticas.

Ejemplo de consulta rápida para analizar métricas (SQL)

-- Valor medio por métrica en las últimas 24h
SELECT name, AVG(value) as avg_value, COUNT() as samples
FROM wp_web_vitals
WHERE ts >= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 1 DAY))  1000
GROUP BY name

Buenas prácticas finales

  1. Empieza con una implementación mínima: recoger LCP, CLS e INP/LCP y comprobar que el rendimiento de recolección no impacta al usuario.
  2. Monitoriza la tasa de eventos y añade retención/limpieza automática si la base de datos crece rápido.
  3. Si hace falta alto volumen, considera enviar desde el servidor a BigQuery o similar desde procesos cron/batch.
  4. Documenta el formato de evento y versiona la API si cambias los campos.

Resumen

Medir Web Vitals en el navegador y enviarlas a la REST API de WordPress es una forma práctica de obtener métricas de experiencia reales de usuarios. La librería web-vitals facilita la captura en WordPress conviene exponer una ruta REST pública pero segura con validación, sanitización y límites. Para producción considera almacenamiento escalable, batching y políticas de retención.

Enlaces útiles



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *