PerfReviews
Informe de auditoría de rendimiento web · Junio 2026

Motor de reservas
NOW

Catorce incidencias de rendimiento documentadas sobre el flujo de reserva, con evidencia medida, causa raíz y soluciones priorizadas por impacto y esfuerzo.

14incidencias
documentadas
1causa raíz que
multiplica al resto
23gráficos sobre
datos medidos
6,2 sde cascada en
/reserva/crear
Cliente
Vantia Hotels & Resorts
Alcance
apps/now-web · /reserva/crear
Analista
Joan León · PerfReviews
Mercado prioritario
Latinoamérica · locale /es-mx/

Informe de muestra. Corresponde a una auditoría real, anonimizada. Las cifras, los hallazgos y las recomendaciones son los originales; el nombre del cliente, el sector y los proveedores de terceros se han ficcionado. Los gráficos son reconstrucciones vectoriales de los datos de captura, no capturas de DevTools: el detalle de cada fichero de evidencia se conserva en las tablas de cada capítulo.

Índice

Qué contiene este informe

Preliminares
§ ACómo leer este informe3 § BMatriz de prioridades y triaje5 § CReferencia de Core Web Vitals7 § DEntorno de medición y convenciones de evidencia8
Incidencias — un capítulo por cada una
PERF-001Encadenamiento de peticiones del formulario32 peticiones de esquema tras una única puerta de 3,9 sP111 PERF-002Cargar solo el CSS necesario1,25 MB de bundle; una hoja de terceros 99,3 % sin usarP215 PERF-003Preconectar con los dominios externoscero hints de conexión para cuatro orígenesP119 PERF-004Precargar las fuentesla primera fuente se descubre a los 4,7 sP122 PERF-005TTFB alto y caché de CDN desactivadala causa raíz que multiplica todo lo demásP025 PERF-006Aplazar el JavaScript no crítico193 KB bloqueando el render desde el <head>P130 PERF-007Consolidar y aplazar los scripts de terceros828 KB transferidos, 5,35 MB descomprimidosP133 PERF-008Precargar los chunks críticos2,8 MB que no se descubren hasta que corre el bundleP237 PERF-009Speculation Rules para la confirmación1–3 s en el paso de mayor intención del embudoP240 PERF-010content-visibility en las secciones ocultas225 de 242 nodos entran en cada pasada de layoutP343 PERF-011Deduplicar las llamadas a la API57 llamadas donde se esperan ~40P147 PERF-012Activar HTTP/2 103 Early Hintshasta 413 ms de ventana ociosa por primera cargaP351 PERF-013Cargar el SDK de mapas bajo demanda666 KB a los 4,7 s sin ninguna interacciónResuelta55 PERF-014Optimizar el sprite de iconos694 iconos enviados, 76 usadosP259
Cierre
§ EDónde este informe es débil63
Sección A

Cómo leer este informe

Cada incidencia es un capítulo autocontenido. Los preliminares recogen todo lo que si no habría que repetir catorce veces: el entorno de medición, los umbrales de Core Web Vitals y las convenciones de evidencia.

Anatomía de un capítulo

Todos los capítulos abren con el número, el título, un resumen del hallazgo en una línea y una franja de metadatos: prioridad, alcance, métricas afectadas, esfuerzo y equipo responsable. Justo debajo va la cifra medida más importante de esa incidencia. El cuerpo sigue siempre el mismo orden:

SecciónQué contiene
ProblemaQué ocurre en la aplicación y por qué, con la ruta de código responsable cuando se ha identificado.
Impacto en Core Web VitalsQué métricas se ven afectadas y por qué mecanismo. Las definiciones están en la Sección C.
Impacto medidoCifras tomadas de una captura real. Todo lo que no está medido se etiqueta como estimación.
Solución recomendadaCambios concretos, ordenados por retorno sobre esfuerzo. El código es ilustrativo, no un parche listo para aplicar.
EvidenciaQué ficheros de captura existen, cuáles faltan, y los gráficos derivados de ellos.
Mejora esperadaAhorro proyectado por optimización, más el multiplicador del mercado remoto. Estimaciones salvo indicación contraria.
ReferenciasFuentes externas que respaldan el diagnóstico y la solución.

Convenciones

Prioridad

La prioridad combina impacto y esfuerzo, no gravedad a secas. Una incidencia de impacto muy alto que exige trabajo de infraestructura entre equipos solo adelanta a una victoria rápida cuando su efecto multiplicador lo justifica — que es exactamente el caso de una de las catorce.

Los colores siguen la misma rampa que los umbrales de Core Web Vitals: rojo, ámbar y verde. La etiqueta siempre acompaña al color, de modo que el documento se lee igual en blanco y negro o con una deficiencia de visión cromática.

NivelSignificadoIncidencias
P0Atacar primero — impacto muy alto, amplifica todo lo demásPERF-005
P1Victorias rápidas de alto impactoPERF-001, PERF-003, PERF-004, PERF-006, PERF-007, PERF-011, PERF-013 Resuelta
P2Mejoras estratégicasPERF-002, PERF-008, PERF-009, PERF-014
P3Acabado e infraestructuraPERF-010, PERF-012

Etiquetas de métrica

Las métricas se agrupan por lo que describen:

LCPFCPTTFB carga  ·  TBTINPTTI interactividad y respuesta  ·  CLS estabilidad visual

Estado de la evidencia

Recopilada la captura existe en el directorio de evidencia. Pendiente falta por capturar; la afirmación que la rodea se apoya en la evidencia que sí está, y se señala como tal.

Los gráficos

Los gráficos de este informe son reconstrucciones vectoriales de los datos de captura, no capturas de pantalla de DevTools. Cada uno indica en su pie el fichero de evidencia del que sale. Ninguna cifra se ha redondeado ni suavizado para que el gráfico quede mejor.

Las cifras

Los valores medidos se dan tal cual. Las proyecciones usan la palabra estimado o un rango, y nombran siempre la línea base de la que extrapolan. Donde un valor es una cota inferior respecto al usuario real — que es prácticamente en todo el documento — el capítulo lo dice.

Sección B

Matriz de prioridades y triaje

Impacto frente a esfuerzo para las catorce incidencias. La zona sombreada es el cuadrante de alto impacto y bajo esfuerzo: el trabajo que conviene planificar primero.

Impacto ↑
Muy alto
PERF-005
Alto
PERF-007PERF-011
PERF-001
Medio-alto
PERF-013
PERF-006
PERF-009
Medio
PERF-003PERF-004PERF-008
PERF-012PERF-014
Medio-bajo
PERF-010
PERF-002
Bajo
Bajo-medio
Medio
Alto
Muy alto
Esfuerzo →
P0 Atacar primero P1 Victoria rápida P2 Estratégica P3 Acabado Sombreado: alto impacto por poco esfuerzo

P0 — Atacar primero

IDTítuloAlcanceMétricasEsfuerzoResponsable
PERF-005Backend alojado en EE. UU. y caché de CDN anulada por las cabeceras del origenTodas las rutasLCPTTIINPMuy altoInfra + Backend
Multiplicador de todo lo demás

Cada una de las otras incidencias de red de este informe queda amplificada por el trayecto de ida y vuelta hasta EE. UU., y otra vez por la distancia hasta el mercado latinoamericano. Corregir PERF-005 no elimina las trece restantes, pero cambia el tamaño de todas sus cifras.

P1 — Victorias rápidas de alto impacto

IDTítuloAlcanceMétricasEsfuerzoResponsable
PERF-013 ResueltaCargar el SDK de mapas bajo demanda/reserva/crearTBTTTIBajoFrontend
PERF-006Aplazar el JavaScript no críticoTodasFCPLCPTBTBajo-medioFrontend + Plataforma
PERF-003Preconectar con los dominios externosTodasLCPFCPBajoFrontend + Plataforma
PERF-004Precargar las fuentesTodasLCPCLSBajoFrontend + Plataforma
PERF-011Deduplicar las llamadas a la API/reserva/crearTTIINPMedioFrontend
PERF-007Consolidar y aplazar los scripts de tercerosTodasFCPLCPTBTINPMedioFrontend + Analítica
PERF-001Mejorar el encadenamiento de peticiones del formulario/reserva/crearLCPTTIAltoFrontend + Backend

P2 — Mejoras estratégicas

IDTítuloAlcanceMétricasEsfuerzoResponsable
PERF-008Precargar los chunks críticos de la aplicación/reserva/crearLCPTTIBajoFrontend + Plataforma
PERF-009Speculation Rules para la navegación de confirmación/crear/confirmadaLCP TTI (página siguiente)MedioFrontend
PERF-014Optimizar el sprite SVG de iconosTodasLCPTBTMedioFrontend + Diseño
PERF-002Cargar solo el CSS necesarioTodasFCPLCPMedioFrontend

P3 — Acabado e infraestructura

IDTítuloAlcanceMétricasEsfuerzoResponsable
PERF-010content-visibility en las secciones fuera de pantalla/reserva/crearTBTINPBajo-medioFrontend
PERF-012HTTP/2 103 Early HintsTodasFCPLCPMedioInfra + Plataforma
Sección C

Referencia de Core Web Vitals

Umbrales a los que se refiere cada capítulo. Las métricas de campo son las que experimenta el usuario; las de laboratorio son aproximaciones que se usan durante el diagnóstico.

MétricaNombre completoUmbral buenoQué mide
LCPLargest Contentful Paint≤ 2,5 sCarga — cuándo aparece el contenido principal
FCPFirst Contentful Paint≤ 1,8 sCarga — cuándo aparece el primer píxel
TTFBTime to First Byte≤ 800 msTiempo de respuesta de servidor y red
TBTTotal Blocking Time≤ 200 msAproximación a la interactividad — solo laboratorio
INPInteraction to Next Paint≤ 200 msRespuesta a todas las interacciones
TTITime to Interactive≤ 3,8 sInteractividad plena — obsoleta, se sigue midiendo aquí
CLSCumulative Layout Shift≤ 0,1Estabilidad visual
Sección D

Entorno de medición y convenciones de evidencia

Aplica a todas las capturas del informe salvo que un capítulo diga lo contrario. Leerlo una vez aquí ahorra leerlo catorce.

Condiciones de la línea base

VariableValor
EntornoProducción, sesión autenticada
Ubicación del analistaEspaña (UE) — unos 120 ms de RTT hasta el origen alojado en EE. UU.
DispositivoMacBook Pro M3
ConexiónFibra simétrica ~600 Mbps, ~10 ms de latencia local
Limitación artificialNinguna, salvo en PERF-010, donde se aplica un factor 4× de CPU y se indica
NavegadorChrome estable. Chrome Canary tiene un fallo en DevTools que oculta las llamadas reales y solo muestra los preflight OPTIONS: no sirve para estas capturas
Locale objetivo/es-mx/reserva/crear — el mercado latinoamericano, el más afectado
Estas cifras son una cota inferior

Un MacBook sobre fibra europea es prácticamente el mejor caso que esta aplicación va a ver nunca. El público que importa — Latinoamérica, en dispositivos modestos y redes móviles — se mueve en RTT reales de 300 a 500 ms. Cada salto serie que quede se multiplica por un factor estimado de 2 a 4× respecto a esta línea base. Léase cada valor medido como el suelo, no como el usuario típico.

Huecos conocidos de la medición

Cómo contribuir con una captura desde el mercado remoto

Una captura tomada desde México, Colombia o Chile es la aportación más valiosa que puede recibir este análisis. Se usa la misma URL /es-mx/reserva/crear, se sigue el CAPTURE-GUIDE.md del directorio de evidencia correspondiente y se dejan los ficheros junto a los existentes con sufijo -mx o -latam — por ejemplo PERF-001-no-cache-mx.har.

Estructura del directorio de evidencia

Cada incidencia guarda su evidencia en su propio subdirectorio. No todos los ficheros existen para todas: la tabla de evidencia de cada capítulo indica el conjunto realmente recopilado.

docs/performance/issues/evidence/
└── PERF-XXX/
    ├── CAPTURE-GUIDE.md                  instrucciones de captura paso a paso
    ├── PERF-XXX-no-cache.har             captura de red sin caché
    ├── PERF-XXX-with-cache.har           captura de red con caché
    ├── PERF-XXX-no-cache-trace.json.gz   traza de rendimiento sin caché
    ├── PERF-XXX-with-cache-trace.json.gz traza de rendimiento con caché
    ├── PERF-XXX-*.png                    capturas de cascada y flame chart
    └── PERF-XXX-recording.mp4            grabación (solo PERF-001, -002, -010)
Una incoherencia que conviene resolver

En el material de partida aparecen dos cifras distintas para el trayecto UE→EE. UU.: unos 120 ms en las notas de evidencia de cada incidencia y unos 250 ms en el resumen de PERF-005. Se han conservado ambas tal como se registraron, sin reconciliarlas. Antes de usar estos números para construir un caso de negocio, conviene confirmar una y corregir la otra.

Parte dos

Las incidencias

Catorce capítulos, uno por incidencia, en orden de identificador. El capítulo n es PERF-0nn.

01PERF-001P1 · Victoria rápida

Mejorar el encadenamiento de peticiones del formulario de reserva

Treinta y dos peticiones de esquema en una sola carga, diecisiete de ellas redundantes, y todas esperando detrás de una llamada de arranque que por sí sola tarda 3,9 segundos.

Alcance
/reserva/crear
Métricas
LCPTTI
Esfuerzo
Alto
Responsable
Frontend + Backend
Estado
Abierta
6 173 ms de ventana entre la primera petición de esquema y la última respuesta, con caché, desde España hacia /es-mx/reserva/crear. Desde el inicio de la navegación hasta el último esquema: ~8 537 ms.

Problema

El formulario de reserva carga su estructura mediante una cascada secuencial de peticiones. Cada una tiene que completarse antes de que arranque la siguiente:

La cascada no es puramente serie: en cuanto /schemas/default responde, el resto de esquemas salen en oleadas paralelas. Pero todas las oleadas están detrás de esa primera respuesta, y esa primera respuesta tarda 3 871 ms.

Impacto en Core Web Vitals

MétricaEfecto
LCPRetrasada — el esqueleto del formulario, que es el candidato a Largest Contentful Paint, no puede renderizarse hasta que hay datos de esquema.
TTIMuy degradada — las peticiones en serie mantienen ocupado el hilo principal y el formulario sin responder.

Cada ida y vuelta adicional se traslada directamente a LCP y TTI. En PERF-005 está el motivo de que cada una de esas idas y vueltas cueste lo que cuesta.

Impacto medido

Capturado con devtools-snippet-perf001.js contra producción, con caché, España (UE) → /es-mx/reserva/crear.

32peticiones de esquema
15endpoints únicos
17refetches redundantes
3 871 mssolo /schemas/default
MétricaValor
Ventana total, primer → último esquema6 173 ms
Suma de las duraciones individuales10 911 ms
Solapamiento aprovechado por el paralelismo4 738 ms
Inicio de navegación → último esquema~8 537 ms
01 5003 0004 5006 0007 5009 000msGET /schemas/default3 871 ms bloqueandoOleada 1 · 12 esquemasOleada 2 · ~4 esquemasOleada 3 · ~6 esquemasOleada 4 · ~4 esquemasOleada 5 · ~3 esquemasÚltimo · v2/travel-documentsse desbloquean los esquemas secundarios
Estructura de la cascada. La barra roja es la puerta: mientras /schemas/default no responde, ninguna de las cinco oleadas siguientes puede empezar. Ocupa el 43 % de la ventana completa. Reconstruido a partir de PERF-001-Highlight-schema-requests.json.

Peticiones duplicadas

El snippet destapó un hallazgo que la cascada por sí sola no muestra: 15 endpoints únicos generan 32 peticiones HTTP dentro de una única carga de página.

v2/extra-services5 peticiones · 4 de másv2/travel-documents4 peticiones · 3 de másroom-details4 peticiones · 3 de másrate-details3 peticiones · 2 de másv2/billing2 peticiones · 1 de máscorporate-agreement2 peticiones · 1 de másguest-details2 peticiones · 1 de másfiscal-data2 peticiones · 1 de mástransfer2 peticiones · 1 de más
Refetches por endpoint. Diecisiete de las treinta y dos peticiones vuelven a pedir un esquema ya cargado en la misma sesión de página. Reconstruido a partir de PERF-001-Highlight-schema-requests.json.

Solución recomendada

1 · Identificar los esquemas invariantes y pedirlos en paralelo

Los esquemas que siempre hacen falta con independencia del tipo de reserva — datos del huésped, habitación, tarifas — pueden pedirse junto al esquema de arranque en lugar de después.

// libs/booking/data-access/src/services/bootstrap.service.ts
forkJoin({
  bootstrap: this.fetchBootstrapResponse(context),
  guestDetails:
    this.httpClient.get(this.configuration.fetchGuestDetailsSchema(context)),
  roomDetails:
    this.httpClient.post(this.configuration.fetchRoomDetailsSchema(context), payload),
}).pipe(...)

2 · Prefetch de esquemas al pasar por encima del enlace

Calentar los esquemas cuando el usuario pasa el ratón sobre el enlace de «Nueva reserva», con una estrategia de prefetch propia o una llamada directa de HttpClient.

3 · Fusionar arranque y esquema por defecto

Evaluar si el backend puede devolver los esquemas más habituales dentro de /reservas/schemas/default y eliminar así las idas y vueltas separadas.

4 · Deduplicar

Diecisiete de las treinta y dos peticiones repiten un esquema ya cargado. Añadir shareReplay(1) por URL en cada servicio de esquema, o un Map<url, Observable> compartido a nivel de interceptor, para que cada endpoint único se pida como mucho una vez por carga. Ver PERF-011, que aborda el mismo tipo de problema a nivel de interacción.

Evidencia

Condiciones de captura

Producción autenticada, España (UE) → /es-mx/reserva/crear, MacBook Pro M3 sobre fibra, sin limitación artificial. Condiciones completas en la Sección D. Los valores son una cota inferior para el mercado latinoamericano: cada salto serie que quede se multiplica por un factor estimado de 2 a 4× con RTT de 300–500 ms.

FicheroEstadoNotas
PERF-001-no-cache-waterfall.pngRecopiladaCascada completa con descarga de assets
PERF-001-with-cache-waterfall.pngRecopiladaCascada solo de API, aislando la cadena de esquemas
PERF-001-recording.mp4RecopiladaGrabación del retardo con caché
PERF-001-Highlight-schema-requests.pngRecopiladaSalida por consola del snippet
PERF-001-Highlight-schema-requests.jsonRecopiladaSalida estructurada — 32 peticiones, 6 173 ms
PERF-001-no-cache.har · with-cache.harRecopilada
PERF-001-*-trace.json.gzRecopiladaTrazas con y sin caché

Mejora esperada

Sobre los datos medidos. El ahorro de las dos primeras filas se solapa: el objetivo combinado no es su suma.

OptimizaciónAhorro estimado
Eliminar los 17 refetches redundantes~400–800 ms
Paralelizar los esquemas secundarios con defaulthasta ~3 871 ms
Fusionar default y esquemas comunes en el servidorhasta ~3 871 ms
Objetivo combinado realista1 500–3 500 ms

Para usuarios en Latinoamérica con RTT de 300–500 ms, cada salto serie restante se multiplica por un factor estimado de 2 a 4× respecto a la línea base europea.

Referencias

02PERF-002P2 · Estratégica

Cargar solo el CSS necesario

Una única hoja de 1,25 MB arrastra todas las secciones de la aplicación, y la hoja del widget de chat bloquea el render 108 ms estando sin usar al 99,3 %.

Alcance
Todas las rutas
foco en /reserva/crear
Métricas
FCPLCP
Esfuerzo
Medio
Responsable
Frontend
Estado
Abierta
99,3 % de assistant-launcher.css no se usa en el render inicial — y aun así cuesta una ida y vuelta bloqueante de 108 ms en cada carga, incluso estando en caché.

Problema

Todos los estilos de la aplicación se empaquetan en un único punto de entrada main.scss que importa cada sección de forma incondicional:

Hoja de estilos¿Crítica?Motivo
body.scssCríticaLayout base
card.scssCríticaContenedor de tarjeta
compact-form-grid.scssCríticaRejilla del formulario
form.scssCríticaControles de formulario
navigation.scssCríticaBarra de navegación
skeleton.scssCríticaEsqueleto de carga
utils.scss · z-index.scssCríticaUtilidades compartidas y apilado
guided-tour.scssNo críticaSolo tras interacción
voucher.scssNo críticaSolo después de confirmar
table.scssNo críticaVistas secundarias
toast.scssDiscutibleRara vez necesaria en el primer pintado

El CSS de secciones que no se ven en el render inicial — facturación, servicios extra, documentación, voucher, tour guiado — se parsea y aplica igualmente, añadiendo recálculo de estilo innecesario. El sprite de iconos (861 KB descomprimidos, ver PERF-014) y los estilos de terceros también se cargan de forma global.

Impacto en Core Web Vitals

MétricaEfecto
FCPRetrasada — el CSS bloqueante debe parsearse por completo antes de que el navegador pinte nada.
LCPRetrasada — el esqueleto del formulario no puede renderizarse hasta que se aplica el CSS.

El CSS bloquea el render por diseño. Cada kilobyte extra en la ruta crítica retrasa FCP directamente.

Impacto medido

Capturado con devtools-snippet-perf002.js, producción, con caché, España (UE) → /es-mx/reserva/crear.

main-[hash].css1 280,8 KB · 88,6 % sin usardesign-system-core.css982,6 KB · 96,7 % sin usarassistant-launcher.css15,5 KB · 99,3 % sin usar
Cobertura de CSS. La pestaña Coverage revela cuatro ficheros, dos más de los que detecta el snippet: design-system-core.css entra por un @import dentro del bundle principal, lo que lo hace invisible al filtro de <link>. Reconstruido a partir de PERF-002-coverage.png y PERF-002-CSS_Stylesheet_Audit.json.
05001 0001 5002 0002 5003 000msmain-[hash].css3 ms · caché de discoassistant-launcher.css108 ms · 304 en red
Posición de las hojas bloqueantes. La del launcher del asistente arranca a los 2 673 ms y cuesta 108 ms reales de red. Reconstruido a partir de PERF-002-CSS_Stylesheet_Audit.json.
Los 108 ms son un coste real

Aunque el fichero está en caché, el navegador hace igualmente un GET condicional y recibe un 304 Not Modified. El render queda bloqueado mientras espera esa respuesta. Un fichero cacheado no es un fichero gratis.

Solución recomendada

1 · Extraer el CSS crítico

Identificar los estilos necesarios para renderizar el esqueleto visible sin desplazar — rejilla, barra de navegación, contenedor de tarjeta, base de los campos — e incorporarlos en línea en un bloque <style> del index.html del shell, o usar inlineStyleLanguage de Angular con un extractor de CSS crítico.

2 · Cargar en diferido los estilos globales no críticos

private loadGuidedTourStyles(): void {
  const link = document.createElement('link');
  link.rel = 'stylesheet';
  link.href = '/assets/guided-tour.css';
  document.head.appendChild(link);
}

3 · Auditar y eliminar utilidades globales sin uso

Pasar PurgeCSS, o @angular-builders/custom-webpack con una pasada de tree-shaking de CSS, contra el uso real en las plantillas.

4 · Mover los estilos específicos de ruta a los componentes

Facturación, documentación y datos de la reserva deberían llevar su SCSS a nivel de componente. Conviene verificar que no queda duplicación en el bundle global.

5 · Aplazar la hoja del asistente

Es de terceros, bloquea el render, se dispara a los 2 673 ms, cuesta 108 ms por carga incluso cacheada y no se usa al 99,3 % en el render inicial. Debe cargarse de forma asíncrona cuando el formulario ya es interactivo, o solo al abrir el chat.

private loadAssistantStyles(): void {
  const link = document.createElement('link');
  link.rel = 'stylesheet';
  // URL tomada del <link> que inyecta el script del asistente
  link.href = 'https://chat.assistant-vendor.com/...assistant-launcher.css';
  document.head.appendChild(link);
}

Evidencia

Condiciones de captura

Producción autenticada, España (UE) → /es-mx/reserva/crear, MacBook Pro M3 sobre fibra, sin limitación. Condiciones completas en la Sección D.

FicheroEstadoNotas
PERF-002-coverage.pngRecopiladaPestaña Coverage con el uso real de CSS
PERF-002-CSS_Stylesheet_Audit.png / .jsonRecopiladaSalida del snippet, en consola y estructurada
PERF-002-recording.mp4RecopiladaGrabación del retardo de render
PERF-002.harRecopiladaCascada con la posición bloqueante y los tamaños
PERF-002-trace.json.gzRecopiladaTraza con entradas Parse Stylesheet y Recalculate Style

Mejora esperada

OptimizaciónAhorro estimado
Aplazar assistant-launcher.css elimina la ida y vuelta de 108 ms~100–120 ms
Diferir los estilos globales no críticos~50–150 ms de FCP
Eliminar o partir el @import de design-system-core.css~100–300 ms de parseo
Objetivo combinado realista200–500 ms de FCP

Para usuarios en Latinoamérica con RTT de 300–500 ms, solo la ida y vuelta de assistant-launcher.css puede costar entre 300 y 500 ms si falla la caché.

Referencias

03PERF-003P1 · Victoria rápida

Preconectar con los dominios externos

Durante la carga se contacta con cuatro orígenes externos y ninguno tiene una pista de conexión. El navegador paga DNS, TCP y TLS completos justo en el momento en que menos se lo puede permitir.

Alcance
Todas las rutas
Métricas
FCPLCP
Esfuerzo
Bajo
Responsable
Frontend + Plataforma
Estado
Abierta
0 pistas de preconnect en el documento. La auditoría de resource hints encontró 2 preload y 1 prefetch, los tres inyectados por terceros — y ni un solo preconnect ni dns-prefetch.

Problema

La página pide recursos a varios orígenes externos sin ninguna pista previa de conexión. El navegador descubre esos orígenes solo después de parsear HTML y CSS o de ejecutar JavaScript, y en ese momento tiene que pagar el coste completo de conexión antes de recibir un solo byte de contenido.

OrigenFunciónSe descubre en
maps.geo-vendor.comSDK de mapas y autocompletado~3,5 s — al ejecutar JS
tiles.geo-vendor.comTeselas y assets del mapaDespués de cargar el SDK
assets.tagmanager-vendor.comGestor de etiquetasParseo del HTML
cdn.vantia.comAssets corporativos: fuentes, cabecera y pieParseo del HTML
chat.assistant-vendor.comAsistente virtualCarga de página

La resolución DNS más el handshake TCP más la negociación TLS cuestan entre 100 y 300 ms por origen desde España, y se pagan íntegros durante la carga.

Impacto en Core Web Vitals

MétricaEfecto
FCPRetrasada — scripts y estilos de orígenes externos bloquean o demoran el render.
LCPRetrasada — los recursos externos descubiertos tarde alargan la ruta crítica.

Impacto medido

assets.tagmanager-vendor64 mscdn.vantia.com208 msDNSTCPTLS
Coste de conexión por origen. Solo cdn.vantia.com gasta 208 ms en el handshake antes siquiera de enviar la petición. Es tiempo que ningún código de aplicación puede recuperar. Reconstruido a partir de PERF-003-tagmanager-timing.png y PERF-003-cdn-timing.png.
preconnect0 · ningunodns-prefetch0 · ningunomodulepreload0 · ningunopreload2 · ambos de tercerosprefetch1 · de terceros
Pistas de recursos presentes en el documento. Las únicas tres que existen las inyectan scripts de terceros. La aplicación no aporta ninguna. Reconstruido a partir de PERF-003-resource-hints-audit.png.

Solución recomendada

1 · Pistas estáticas en el <head> del shell

<!-- Preconexiones críticas: abrir la conexión antes de que dispare el JS -->
<link rel="preconnect" href="https://assets.tagmanager-vendor.com">
<link rel="preconnect" href="https://cdn.vantia.com" crossorigin>

2 · Pista programática para el SDK de mapas, solo con intención

El mapa no debe preconectarse al inicializar: en PERF-013 el problema es precisamente su carga anticipada. La pista se añade cuando el usuario muestra intención pasando el ratón por el campo de dirección.

// libs/core/main/src/ui/places-autocomplete/maps-api.service.ts
private preconnectToMaps(): void {
  ['https://maps.geo-vendor.com', 'https://tiles.geo-vendor.com'].forEach((origin) => {
    if (!document.querySelector(`link[href="${origin}"]`)) {
      const link = this.document.createElement('link');
      link.rel = 'preconnect';
      link.href = origin;
      this.document.head.appendChild(link);
    }
  });
}

Se llama en el mouseenter del campo de dirección: para cuando se añade la etiqueta del script, la conexión ya está establecida.

Evidencia

Condiciones de captura

Producción autenticada, España (UE), MacBook Pro M3 sobre fibra, sin limitación. Condiciones completas en la Sección D. Con RTT de 300–500 ms, el coste por origen es de 2 a 4× mayor.

FicheroEstadoNotas
PERF-003-cold-waterfall.pngRecopiladaCascada sin caché con las columnas de DNS y conexión
PERF-003-cold.harRecopiladaHAR sin caché con el detalle por origen
PERF-003-tagmanager-timing.pngRecopiladaPanel Timing del gestor de etiquetas
PERF-003-cdn-timing.pngRecopiladaPanel Timing de cdn.vantia.com
PERF-003-resource-hints-audit.pngRecopiladaConfirmación de preconnect: 0
PERF-003-maps-timing.pngPendientePanel Timing del SDK de mapas (opcional)
PERF-003-trace.jsonPendienteTraza con el coste DNS/TCP/TLS en la línea temporal

Mejora esperada

Cada handshake inicial que se elimina ahorra entre 100 y 300 ms sobre fibra desde España, con unos 120 ms de RTT hasta orígenes alojados en EE. UU. Con tres o cuatro orígenes externos, un ahorro total de 300 a 900 ms en la primera carga es realista.

Para el mercado latinoamericano con RTT de 300–500 ms, el coste por origen es de 2 a 4× superior, así que la mejora crece en proporción: entre 600 ms y 1,6 s por primera carga.

Referencias

04PERF-004P1 · Victoria rápida

Precargar las fuentes

La primera fuente no se descubre hasta 4 713 ms después de la navegación, porque el navegador tiene que descargar y parsear el bundle de CSS entero antes de enterarse de que existe.

Alcance
Todas las rutas
las fuentes son globales
Métricas
LCPCLS
Esfuerzo
Bajo
Responsable
Frontend + Plataforma
Estado
Abierta
4 713 ms hasta que arranca la primera petición de fuente. El propio snippet lo dice sin rodeos: con un <link rel="preload" as="font"> correcto esto debería ser 0 ms. La ventana completa de carga de fuentes abarca 3 542 ms.

Problema

Las fuentes corporativas se cargan como web fonts referenciadas por reglas @font-face en el CSS. El navegador no puede descubrir la URL de una fuente hasta que ha:

Esta cascada de tres pasos produce entre 300 y 600 ms de texto invisible o con fuente de respaldo antes de que se pinte la real. FOUT y FOIT son dos síntomas visibles del mismo retardo de descubrimiento.

Impacto en Core Web Vitals

MétricaEfecto
LCPRetrasada — el elemento LCP, normalmente un titular o una etiqueta del formulario, no puede pintarse con la fuente correcta hasta que el fichero llega.
CLSEn riesgo — el intercambio de la fuente de respaldo por la definitiva desplaza el layout si las métricas tipográficas difieren.

Impacto medido

01 5003 0004 5006 0007 5009 000msVantiaSans-BoldVantiaSans-RegularVantiaSans-Bold · duplicadaVantiaSans-Regular · duplicadaVantiaSans-LightVantiaSans-Mediumprimera fuente descubierta · debería ser 0 ms
Línea temporal de carga de fuentes. Seis peticiones. Las dos barras rojas son duplicados: Bold y Regular se piden dos veces cada una. Reconstruido a partir de PERF-004-no-cache-font-duplicates.png y la salida del snippet.
4 713 msprimera fuente descubierta
8 255 mstodas las fuentes listas
3 542 msventana de carga
2fuentes pedidas dos veces
Segundo hallazgo — peticiones duplicadas

VantiaSans-Bold.woff2 y VantiaSans-Regular.woff2 se piden dos veces cada una. Cada duplicado gasta ancho de banda y añade carga a la ruta crítica. Conviene investigarlo junto con el trabajo de preload: precargar una fuente que después se pide dos veces no arregla la doble referencia de fondo.

Solución recomendada

1 · Precargar las variantes críticas en el <head>

<!-- Solo las fuentes de la ruta crítica de render -->
<link rel="preload" href="/assets/fonts/VantiaSans-Regular.woff2"
      as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/VantiaSans-Bold.woff2"
      as="font" type="font/woff2" crossorigin>

Solo las que se usan sobre la línea de flotación. Precargar todos los pesos y estilos desperdicia ancho de banda y compite con recursos más críticos.

2 · Añadir font-display: swap a cada regla

@font-face {
  font-family: 'VantiaSans';
  src: url('../fonts/VantiaSans-Regular.woff2') format('woff2');
  font-display: swap; // el texto es visible durante la carga
}

3 · Resolver los duplicados

Rastrear qué dos declaraciones referencian los mismos ficheros Bold y Regular, y consolidarlas.

Evidencia

Condiciones de captura

Producción autenticada, España (UE), MacBook Pro M3 sobre fibra, sin limitación. Condiciones completas en la Sección D.

FicheroEstadoNotas
PERF-004-no-cache.har · with-cache.harRecopiladaCon caché las fuentes vienen de disco, pero el retardo de descubrimiento persiste
PERF-004-*-trace.json.gzRecopiladaConfirman que el hueco no es un problema de caché
PERF-004-no-cache-font-detail.pngRecopiladaDetalle de la petición: «Initiated by main-[hash].css»
PERF-004-no-cache-font-duplicates.pngRecopiladaFiltro de fuentes: Bold y Regular ×2
PERF-004-*-waterfall.pngPendienteCascadas con y sin caché

Mejora esperada

Precargar dos o tres ficheros críticos elimina la cascada de descubrimiento y puede mejorar LCP entre 200 y 400 ms en la primera visita sin caché, medido desde la línea base España/fibra. Las visitas siguientes no se ven afectadas, porque las fuentes ya vienen de caché.

Con RTT de 300–500 ms el retardo de descubrimiento es proporcionalmente mayor, lo que sitúa la mejora estimada entre 2 y 4× por encima: unos 400 a 800 ms por primera carga.

Referencias

05PERF-005P0 · Atacar primero

TTFB alto y caché de CDN desactivada por el origen

El CDN está delante del backend y no cachea nada, porque el origen envía no-store en cada respuesta. Cincuenta y cinco llamadas por sesión, 342 ms de media, todas hasta el origen.

Alcance
Todas las rutas con API
Métricas
LCPTTIINP
Esfuerzo
Muy alto
Responsable
Infra + Backend
Estado
Abierta
18,8 s de tiempo de red acumulado en una única sesión: 55 llamadas a la API, todas con fallo de caché en el CDN. Servidas desde el edge deberían sumar entre 0,5 y 1 s.
La causa raíz que multiplica

Esta es la incidencia que encarece a todas las demás. Incluso después de que PERF-001 elimine el encadenamiento en serie, cada llamada individual seguirá arrastrando entre 235 y 342 ms de sobrecoste. Arreglar el encadenamiento reduce el número de idas y vueltas; arreglar esto reduce el precio de cada una de las que queden.

Problema

El backend de reservas está desplegado en la nube corporativa, en una región de EE. UU., detrás del CDN corporativo. Pero el CDN funciona de hecho como un simple paso a través: cada petición es un fallo de caché, porque el origen envía Cache-Control: no-cache, no-store, max-age=0 en todas sus respuestas. Todas las peticiones llegan a un origen que no está cerca de los usuarios.

La cabecera server-timing de cada respuesta de esquema lo declara sin ambigüedad:

server-timing: cdn-cache; desc=MISS
server-timing: edge; dur=108
server-timing: origin; dur=75

Mediciones confirmadas

Dos capturas, Chrome estable, España, 1 de junio de 2026.

MétricaHAR con caché,
29 llamadas
Snippet, sesión
completa, 55
Servido desde
el edge
Estado de caché en CDNMISS en todasHIT
Duración, peticiones con caché210–345 ms
media ~235 ms
173–957 ms
media ~342 ms
~10–20 ms
Arranque en frío · schemas/default4 328 ms de TTFB~10–20 ms
Arranque en frío · /feature-toggle1 671 ms~10–20 ms
Arranque en frío · /user958 ms~10–20 ms
Proceso en el edge, con caché103–136 ms
Proceso en el origen, con caché70–90 ms
Llamadas totales2955
Duración acumulada18,8 s~0,5–1 s
Sobrecoste frente a un asset estático del CDN~211 ms por llamada~0 ms

El HAR se capturó en una única navegación a /reserva/crear. El snippet se ejecutó sobre una sesión completa con interacciones de formulario, que es lo que destapa el volumen real de llamadas y los arranques en frío más allá de schemas/default.

/feature-toggle1 671 ms · arranque en frío/user958 ms · arranque en frío/integration/user-flags600 ms/countries/mx/info568 ms/schemas/default529 ms/booking-profiles524 ms/schemas/references497 ms/group-booking/settings421 ms/users/privileges411 ms/schemas/v2/extra-services389 ms
Las diez llamadas más lentas de la sesión. Las tres primeras son arranques en frío. La media de las 55 llamadas es de 342 ms; el máximo, 1 671 ms. Reconstruido a partir de PERF-005-TTFB-01.png y -02.png.
guest-details · con caché353 ms de esperaschemas/default · en frío989 ms de esperaedge del CDNorigenred y resto
Dónde se va el tiempo de espera. En una petición con caché el origen ya aporta 77 ms sobre 353. En el arranque en frío, 691 ms de 989 son puro tiempo de origen. Reconstruido a partir de PERF-005-cdn-cache-miss-headers.png y PERF-005-cold-start-timing.png.

Causas raíz

1 · Las cabeceras del propio origen desactivan la caché

cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0

no-store le indica al CDN que no cachee nunca la respuesta. La infraestructura de CDN ya está desplegada y pagada: lo que impide que sea efectiva son las cabeceras.

2 · Penalización de arranque en frío en schemas/default

La primera petición de esquema de cada sesión tarda 4 328 ms de TTFB, de los cuales 3 667 ms se gastan en el origen. Las siguientes bajan a unos 75 ms. Esa firma es coherente con una inicialización perezosa: pool de conexiones, calentamiento de la máquina virtual o un cálculo bajo demanda.

3 · El origen no está cerca de los usuarios

Incluso con caché, 210–345 ms de TTFB indican distancia geográfica real entre el edge y el origen. Con 29 llamadas en la carga del formulario, eso se acumula deprisa.

Nota sobre un hueco previo de investigación

Esta incidencia estuvo aparcada como «pendiente de investigar» porque DevTools de Chrome Canary solo registraba las peticiones preflight OPTIONS y ocultaba las llamadas reales. Chrome estable las captura todas. Todos los HAR adjuntos se tomaron con Chrome estable.

Solución recomendada

Opciones por retorno sobre esfuerzo.

Opción 1 · Quitar no-store de las respuestas de esquema

Máximo retorno, mínimo esfuerzo. La infraestructura ya está; el único bloqueo es el Cache-Control del origen.

Cache-Control: private, max-age=30

O bien, para que cachee el CDN pero no el navegador:

Cache-Control: no-cache, s-maxage=60
Surrogate-Control: max-age=60

Las respuestas de esquema son en gran medida deterministas por cuenta y rol. Un TTL corto con una clave de caché que incluya el identificador de cuenta sería seguro y efectivo, y llevaría el TTFB de ~235 ms a ~10–20 ms en las respuestas cacheadas.

Opción 2 · Corregir el arranque en frío

Los 3,67 s de origen en la primera llamada deben investigarse desde backend. Causas probables: inicialización perezosa del servicio, expiración del pool de conexiones por inactividad o un primer cálculo costoso. Soluciones: conexiones keep-alive, inicialización anticipada en el despliegue o un ping de calentamiento programado.

Opción 3 · Gateway regional

Desplegar una pasarela ligera cerca del mercado que termine TLS regionalmente y haga de proxy contra el origen. Reduce el tramo transatlántico con independencia de la política de caché.

Opción 4 · Despliegue regional completo

Desplegar el backend en una región próxima al mercado. Máxima reducción de latencia; requiere coordinación con backend, infraestructura y los equipos de residencia de datos y cumplimiento.

Opción 5 · Agrupar peticiones en el edge

Usar funciones en el edge del CDN para agrupar varias peticiones de esquema en una sola ida y vuelta al origen, reduciendo el número de trayectos aunque el origen siga donde está.

Evidencia

Condiciones de captura

Producción autenticada, España (UE), MacBook Pro M3 sobre fibra, sin limitación, solo con Chrome estable. Condiciones completas en la Sección D.

FicheroEstadoNotas
PERF-005-warm-cache.harRecopilada29 llamadas, cdn-cache: MISS, TTFB 210–345 ms
PERF-005-with-cache.harRecopiladaSegunda captura con caché; confirma el patrón entre sesiones
PERF-005-no-cache.harRecopiladaSin caché; TTFB de schemas/default ~4 328 ms
PERF-005-cdn-cache-miss-headers.pngRecopiladacdn-cache; desc=MISS junto a Cache-Control: no-store
PERF-005-cold-start-timing.pngRecopiladaPanel Timing con la barra de TTFB en ~4 300 ms
PERF-005-TTFB-01.png · -02.pngRecopiladaSalida del snippet: TTFB por endpoint
PERF-005-network-ttfb.pngPendientePanel de red con la columna TTFB legible
PERF-005-us-baseline.harPendienteLínea base desde EE. UU. para medir el delta de RTT
PERF-005-trace.jsonPendienteTraza con el TTFB por llamada y el pico de arranque en frío

Mejora esperada

Sin cambios345 mssin mejoraActivar caché en el CDN345 ms~10–20 ms con acierto de cachéGateway regional345 ms~30–60 msDespliegue regional completo345 ms~5–15 msactualtras la corrección
TTFB por llamada según la opción elegida. Activar la caché del CDN es la corrección de mayor retorno y menor esfuerzo de todo el informe: la infraestructura ya existe y solo hay que cambiar las cabeceras. Proyecciones sobre la línea base medida de 210–345 ms.
CorrecciónTTFB por llamadaArranque en frío
Línea base actual España/fibra210–345 ms4 328 ms
Activar caché en el CDN~10–20 ms con aciertoeliminado en los endpoints cacheados
Solo corregir el arranque en frío210–345 ms~75 ms
Gateway regional~30–60 ms~30–60 ms
Despliegue regional completo~5–15 ms~5–15 ms

Las cifras base reflejan condiciones de España sobre fibra. Para el mercado latinoamericano con RTT de 300–500 ms, el TTFB con caché se estima entre 500 y 900 ms por llamada, lo que empeora bastante el coste acumulado de sesión (55 llamadas × ~700 ms de media) y hace la corrección de caché proporcionalmente más valiosa.

Referencias

06PERF-006P1 · Victoria rápida

Aplazar el JavaScript no crítico

Tres scripts del <head> del shell no llevan defer ni async. Ninguno hace falta para pintar el esqueleto del formulario, y los tres detienen el parseo del HTML a los 463 ms.

Alcance
Todas las rutas
shell de plataforma
Métricas
FCPLCPTBT
Esfuerzo
Bajo-medio
Responsable
Frontend + Plataforma
Estado
Abierta
193 KB de JavaScript bloqueante — unos 378 KB descomprimidos — repartidos en tres scripts estáticos del <head>, los tres arrancando a los 463 ms y bloqueando el FCP de los 643 ms.

Problema

Varios ficheros JavaScript de tamaño considerable se cargan de forma síncrona en el <head>, bloqueando el parseo y el render.

ScriptTamaño en redComportamiento
chunk de aplicación [hash]62 KB
185 KB descomprimidos
Bloquea el render — estático, sin defer/async
rumagent-[hash].js agente de RUM130 KBBloquea el render — estático, sin defer/async
app-initialization.production-[hash].js1 KBBloquea el render — estático, sin defer/async
03006009001 2001 5001 800mschunk de aplicación62 KBrumagent-[hash].js130 KBapp-initialization.js1 KBdatalayer-core.js · inyectado269 KB · no bloqueanteFCP
Scripts bloqueantes frente al FCP. Los tres arrancan simultáneamente a los 463 ms; el FCP no llega hasta los 643. La barra gris es el data layer, que el navegador no clasifica como bloqueante pero que igualmente ocupa el hilo principal. Reconstruido a partir de PERF-006-network-blocking.png y PERF-006-trace-fcp-blocked.png.
Los scripts inyectados son un problema aparte

datalayer-core.js (~900 KB) y el script de cabecera y pie (~904 KB) los inyecta Angular en tiempo de ejecución. El navegador les asigna prioridad de red baja y no los clasifica como bloqueantes: renderBlockingStatus es "non-blocking". Aun así se ejecutan en el hilo principal durante el arranque y contribuyen al TBT, así que retrasar su inyección a tiempo ocioso (corrección 2) sigue aplicando.

Impacto en Core Web Vitals

MétricaEfecto
FCPMuy bloqueada — el parseo del HTML se detiene en cada <script> síncrono hasta que se descarga, parsea y ejecuta.
LCPRetrasada — el navegador no puede pintar hasta que terminan todos los scripts bloqueantes.
TBTAlto — los scripts grandes generan tareas largas en el hilo principal que bloquean la interactividad.

Impacto medido

MétricaMedidoCondiciones
JS bloqueante transferido193 KBSin caché, España, fibra, sin limitación
JS bloqueante descomprimido~378 KBSin caché, España, fibra, sin limitación
Scripts bloqueantes en <head>3Estáticos, sin defer/async
Inicio de descarga~463 msSin caché
FCP~643 msSin caché
Inicio de inyección del data layer~1 239 msSin caché

Solución recomendada

1 · Añadir defer a todos los scripts estáticos del <head>

<!-- Antes -->
<script src="/booking/assets/app-initialization.production-[hash].js"></script>
<script src="/booking/rumagent-[hash].js"></script>

<!-- Después -->
<script src="/booking/assets/app-initialization.production-[hash].js" defer></script>
<script src="/booking/rumagent-[hash].js" defer></script>

2 · Retrasar la inyección del data layer

// libs/core/main/src/datalayer/datalayer-loader.service.ts
async load(): Promise<void> {
  if (this.configuration.dataLayer.scriptUrl) {
    await new Promise<void>((resolve) =>
      'requestIdleCallback' in window
        ? requestIdleCallback(() => resolve())
        : setTimeout(resolve, 200)
    );
    // lógica de inyección existente...
  }
}

3 · Cuándo async y cuándo defer

AtributoComportamientoPara qué
asyncDescarga en paralelo y ejecuta en cuanto está listo; puede interrumpir el parseoScripts totalmente independientes, como analítica
deferDescarga en paralelo y ejecuta al terminar el parseo, en orden de documentoScripts que dependen del DOM, como el de cabecera y pie

Evidencia

Condiciones de captura

Producción autenticada, España (UE), MacBook Pro M3 sobre fibra, sin limitación. Condiciones completas en la Sección D.

FicheroEstadoNotas
PERF-006-no-cache.har · with-cache.harRecopiladaCon caché el parseo y la evaluación siguen retrasando el render
PERF-006-*-trace.json.gzRecopiladaBloques «Evaluate Script» antes del marcador de FCP
PERF-006-network-blocking.pngRecopiladaLos tres scripts bloqueantes con prioridad alta
PERF-006-trace-fcp-blocked.pngRecopiladaFCP retrasado por tareas de evaluación
PERF-006-coverage.pngRecopiladaPorcentaje sin usar de cada script bloqueante

Mejora esperada

Añadir defer a los tres scripts estáticos elimina la fase de bloqueo del parseo a los 463 ms. Sobre fibra de escritorio en Europa el FCP ya está en ~643 ms, así que el margen en condiciones óptimas es modesto — pero el beneficio se multiplica en conexiones lentas y con RTT de 300–500 ms.

Retrasar la inyección del data layer (269 KB, arrancando a los 1 239 ms) a tiempo ocioso reduce además el TBT.

Referencias

07PERF-007P1 · Victoria rápida

Consolidar y aplazar los scripts de terceros

Analítica y seguimiento son la mayor fuente evitable de coste de red y CPU de la aplicación — y el widget de chat sigue haciendo polling durante ocho minutos para usuarios que nunca lo abren.

Alcance
Todas las rutas
Métricas
FCPLCPTBTINP
Esfuerzo
Medio
Responsable
Frontend + Analítica
Estado
Abierta
5,35 MB descomprimidos — 828 KB transferidos — de código de terceros en una primera carga, sin contar el tracker de personalización ni las respuestas de los beacons. En más de 28 peticiones.

Problema

Gestor de etiquetas

Asistente virtual (widget de chat)

Beacons de analítica

Doce beacons por carga de página hacia el endpoint de recogida. Cada URL lleva un sufijo aleatorio único, que es el mecanismo estándar de la plataforma para evitar la caché, no un identificador por evento.

67%33%8 activaciones de flags sin agrupar4 eventos distintos
Composición de los doce beacons. Ocho corresponden a activaciones de feature flags distintas; cuatro son eventos genuinamente diferentes. Reconstruido a partir de PERF-007-beacon-burst.png.
El problema no son datos duplicados

Ocho de los doce comparten el mismo tipo de evento de activación, pero cada uno lleva un identificador de flag y una variante distintos: son ocho feature flags activas en /reserva/crear, no ocho copias del mismo evento. El problema es que se envían como ocho peticiones HTTP en serie dentro de 430 ms donde bastaría una agrupada. La plataforma de analítica soporta agrupación de eventos; la de experimentación dispara un beacon por activación sin agrupar nada.

Los cuatro restantes sí son eventos distintos: vista de página (×2), formulario listo, y un clic sobre la recepción de esquema de tarifas.

Tracker de personalización — recién identificado

Se carga un tracker adicional desde el CDN de un proveedor de personalización. Su alcance e impacto no están medidos todavía. Debería entrar en la auditoría de carga diferida junto con el gestor de etiquetas.

Impacto medido

Asistente virtual (chat)3 099 KB · 15+ peticionesGestor de etiquetas2 251 KB · 13 peticionesPersonalización— · sin medirAnalítica (beacons)— · 12 peticiones, carga ~0
Peso descomprimido por proveedor. Sin caché, España, fibra, sin limitación. El total medido, excluyendo personalización y beacons, es de 828 KB transferidos y 5 350 KB descomprimidos en más de 28 peticiones. Reconstruido a partir de PERF-007-no-cache.har y las trazas asociadas.

Solución recomendada

Gestor de etiquetas — cargar tras la interactividad

// Sustituir el script anticipado por una inyección posterior al render
afterNextRender(() => {
  const script = document.createElement('script');
  script.src = '//assets.tagmanager-vendor.com/container-xxx.min.js';
  script.async = true;
  document.head.appendChild(script);
});

Asistente — cargar solo con interacción del usuario

// libs/shell/src/services/virtual-assistant-display.service.ts
initOnInteraction(): void {
  const trigger = document.querySelector('[data-chat-trigger]');
  trigger?.addEventListener('click', () => this.loadChatWidget(), { once: true });
}

private loadChatWidget(): void {
  // inyectar aquí el script del asistente, y solo ahora
}

Analítica — agrupar las activaciones de flags

private readonly pendingActivations: FlagActivation[] = [];
private flushScheduled = false;

trackFlagActivation(activation: FlagActivation): void {
  this.pendingActivations.push(activation);
  if (!this.flushScheduled) {
    this.flushScheduled = true;
    queueMicrotask(() => this.flushActivations());
  }
}

private flushActivations(): void {
  // enviar un único beacon con todas las activaciones pendientes
  this.flushScheduled = false;
  this.pendingActivations.length = 0;
}

Personalización — auditar y aplazar

Determinar si el script de personalización hace falta para el render inicial. Si no, aplicarle el mismo patrón de afterNextRender().

Evidencia

Condiciones de captura

Producción autenticada, España (UE), MacBook Pro M3 sobre fibra, sin limitación. Condiciones completas en la Sección D.

FicheroEstadoNotas
PERF-007-no-cache.har · with-cache.harRecopilada
PERF-007-*-trace.json.gzRecopiladaConfirman el EvaluateScript de 51,3 ms y la duplicación por iframe
PERF-007-tracking-waterfall-tagmanager.pngRecopilada13 peticiones, 466 KB / 2 294 KB
PERF-007-tracking-waterfall-assistant.pngRecopiladaDuplicación por iframe visible
PERF-007-beacon-burst.pngRecopiladaLos 12 beacons y el panel de cabeceras
PERF-007-tagmanager-timing.pngPendienteDetalle: encolado 4,06 s, handshake 61 ms, total 138 ms
PERF-007-assistant-zoom.pngPendienteMisma captura ampliada a 0–15 s

Mejora esperada

Referencias

08PERF-008P2 · Estratégica

Precargar los chunks críticos de la aplicación

Dos chunks que suman 2,8 MB son necesarios para pintar el formulario, y el navegador no se entera de que existen hasta que ha ejecutado el bundle principal. Dos huecos, 1 550 ms de espera pura.

Alcance
/reserva/crear
Métricas
LCPTTI
Esfuerzo
Bajo
Responsable
Frontend + Plataforma
Estado
Abierta
~1 550 ms de sobrecoste de descubrimiento en serie sobre fibra — 720 ms antes de la tercera oleada y 830 antes de la cuarta — durante los cuales la red está ociosa esperando a que el JavaScript nombre el siguiente fichero.

Problema

Dos chunks grandes con carga perezosa hacen falta para renderizar el formulario, pero solo se descubren después de que se ejecute el bundle inicial.

ChunkTamañoMomento de descubrimiento
chunk-[hash-a].js~1,5 MB
~396 KB comprimido
Tras ejecutar el bundle principal
chunk-[hash-b].js~1,3 MB
~262 KB comprimido
Tras ejecutar el bundle principal

Como los chunks se descubren mediante import() dinámicos, el navegador no puede empezar a pedirlos hasta que corre el router de Angular, que a su vez depende de que el bundle principal esté parseado y ejecutado. El resultado es una dependencia en serie donde 2,8 MB de código crítico esperan detrás del bundle.

Impacto en Core Web Vitals

MétricaEfecto
LCPRetrasada — el formulario no se pinta hasta que el chunk de 1,5 MB está descargado y parseado.
TTIRetrasada — Angular no puede activar la ruta hasta que los dos chunks están listos.

Impacto medido

05501 1001 6502 2002 7503 300msOleada 1 · bundle principalOleada 2 · dependenciashueco de descubrimiento~720 msOleada 3 · chunk de 1,5 MBhueco de descubrimiento~830 msOleada 4 · chunk de 1,3 MB
Oleadas de carga y huecos de descubrimiento. Las dos barras rojas no son descargas: son tiempo en que la red está parada porque el navegador todavía no sabe qué pedir. Reconstruido a partir de PERF-008-key-chunks-waterfall.png y PERF-008-no-cache.har.

Solución recomendada

Añadir pistas de modulepreload en el <head> del shell:

<link rel="modulepreload" href="/now-web/chunk-[hash-a].js">
<link rel="modulepreload" href="/chunk-[hash-b].js">
Los nombres de chunk llevan hash de contenido

Cambian en cada build, así que una pista escrita a mano se queda obsoleta en silencio. Hay que mantenerla sincronizada por alguno de los tres mecanismos siguientes.

1 · Generación en tiempo de build

// scripts/inject-modulepreload.js (dentro del pipeline de build)
const stats = require('./dist/now-web/stats.json');
const criticalChunks = stats.assets
  .filter(a => a.name.startsWith('chunk-') && a.size > 1_000_000)
  .map(a => `<link rel="modulepreload" href="/${a.name}">`);
// inyectar en la plantilla del shell...

2 · Chunks con nombre

El build ya tiene namedChunks: true. Basta con mapear los nombres a los ficheros con hash en un manifiesto del artefacto de build.

3 · Coordinación con el equipo de plataforma

Si el shell lo genera la plataforma corporativa, las pistas de modulepreload deben incluirse en su propio pipeline.

Evidencia

Condiciones de captura

Producción autenticada, España (UE), MacBook Pro M3 sobre fibra, sin limitación. Condiciones completas en la Sección D.

FicheroEstadoNotas
PERF-008-no-cache.har · with-cache.harRecopiladaCuatro oleadas en serie; los dos chunks grandes en la tercera y la cuarta
PERF-008-*-trace.json.gzRecopiladaEl bundle principal termina y hay un hueco visible antes de los chunks
PERF-008-key-chunks-waterfall.pngRecopiladaCascada con las oleadas y los dos huecos
devtools-snippet-perf008.jsRecopiladaSnippet reutilizable

Mejora esperada

Los dos huecos medidos suman ~1 550 ms sobre fibra. Precargar ambos chunks en paralelo con el bundle principal puede eliminarlos, con una mejora de TTI de 500 a 1 500 ms.

Con RTT de 300–500 ms cada ida y vuelta de descubrimiento cuesta bastante más, y la mejora se estima entre 2 y 4× superior. Los valores de España/fibra son una cota inferior.

Referencias

09PERF-009P2 · Estratégica

Speculation Rules para la navegación de confirmación

El usuario ha invertido entre dos y cinco minutos rellenando el formulario. El clic final — el que confirma la reserva — le cuesta después entre uno y tres segundos más de espera.

Alcance
/reserva/crear
/reserva/confirmada
Métricas
LCPTTI
en la página siguiente
Esfuerzo
Medio
Responsable
Frontend
Estado
Abierta
1–3 s de retardo de navegación en el momento de mayor intención de todo el embudo, medido sobre fibra con caché. Estimado en 3–6 s para el mercado latinoamericano.
Corrección sobre el flujo de la interfaz

En el modo cómodo, pulsar «Ver resumen» abre una capa modal dentro de /reserva/crear sin cambiar la URL. La navegación real ocurre al pulsar el botón de confirmar dentro de esa capa, que lleva a /reserva/confirmada. En el modo compacto, «Confirmar reserva» navega directamente. No existe ninguna ruta intermedia de revisión, aunque borradores anteriores de este seguimiento hablaban de una.

Problema

Completado el formulario, el usuario navega a /reserva/confirmada. En ese momento el navegador tiene que:

No hay ninguna estrategia de prefetch para esta ruta. El retardo cae justo donde la frustración sale más cara: el usuario ya ha invertido minutos de trabajo y está comprometiéndose con una compra.

Impacto en Core Web Vitals

MétricaEfecto en /reserva/confirmada
LCPDeja de ser espera — los assets ya están precargados antes de navegar.
TTIReducida — el chunk específico de confirmación ya está en caché.

Impacto medido

MétricaValor
Retardo de navegación, crear → confirmada1–3 s
LCP en /reserva/confirmadaTraza pendiente
TTI en /reserva/confirmadaTraza pendiente

Solución recomendada

Inyectar las reglas de especulación cuando el usuario ha demostrado progreso real en el formulario — por ejemplo, al validar el primer paso — y no en la carga de página.

// En comfortable-navigation.component.ts — desde viewSummary()
// o desde la suscripción a showViewSummaryButton$
private injectSpeculationRules(): void {
  if (!HTMLScriptElement.supports?.('speculationrules')) return;         // detección
  if (document.querySelector('script[type="speculationrules"]')) return; // ya inyectada

  const script = document.createElement('script');
  script.type = 'speculationrules';
  script.textContent = JSON.stringify({
    prefetch: [{
      urls: ['/booking/es-mx/reserva/confirmada'],
      eagerness: 'moderate',
    }],
  });
  document.head.appendChild(script);
}

Para navegadores sin soporte — Safari y Firefox — se recurre a un prefetch estándar:

<!-- Alternativa para navegadores sin Speculation Rules -->
<link rel="prefetch" href="/booking/es-mx/reserva/confirmada">
Nivel de eagernessComportamiento
immediatePrecarga o prerrenderiza de inmediato
moderateCuando el enlace es visible en el viewport — el valor por defecto y la elección correcta aquí
conservativeSolo al pasar el ratón o pulsar

Con moderate se evita gastar ancho de banda en usuarios que abandonan el formulario pronto.

Evidencia

Condiciones de captura

Producción autenticada, España (UE), MacBook Pro M3 sobre fibra, sin limitación. Condiciones completas en la Sección D.

FicheroEstadoNotas
PERF-009-baseline.harRecopiladaCon caché, sin reglas activas. Todos los recursos de la página de confirmación arrancan de cero al navegar.
devtools-snippet-perf009.jsRecopiladaSnippet reutilizable
PERF-009-baseline-trace.jsonPendienteLínea temporal completa de la navegación; el coste de LCP y TTI que la corrección eliminaría
PERF-009-nav-waterfall.pngPendienteCascada con los recursos arrancando de cero
PERF-009-spec-rules-panel.pngPendientePanel Speculation Rules vacío, confirmando que no hay reglas
Este capítulo todavía no tiene gráficos de medición

A diferencia del resto del informe, PERF-009 se apoya en un único HAR. La línea base de 1–3 s es real, pero gruesa: falta la traza que la descompondría en tiempo de chunk frente a tiempo de API. Las cifras de mejora que siguen son proyecciones, no mediciones.

Mejora esperada

Chrome / Edge · Speculation Rules3 000 ms< 100 ms percibidosSafari / Firefox · rel=prefetch3 000 ms~500 ms de mejoraactualtras la corrección
Navegación proyectada tras la corrección. Estimaciones sobre la línea base medida de 1–3 s, no mediciones. Base: PERF-009-baseline.har.

Para el mercado latinoamericano con RTT de 300–500 ms, la línea base se estima entre 3 y 6 s y la mejora proyectada escala en la misma proporción.

Referencias

10PERF-010P3 · Acabado

Aplicar content-visibility a las secciones fuera de pantalla

Desplegar un menú de la barra lateral repinta el formulario entero. El navegador calcula el layout de 225 de los 242 nodos del documento en una sola pasada, porque nada del formulario está contenido.

Alcance
/reserva/crear
vista compacta
Métricas
TBTINP
Esfuerzo
Bajo-medio
Responsable
Frontend
Estado
Abierta
225 / 242 nodos del documento entran en una única pasada de layout, con #document como raíz. Frames consecutivos de 133,5 ms y 108,4 ms — ocho veces el umbral de 16,6 ms.

Problema

La vista compacta del formulario renderiza todas las secciones en el DOM a la vez, aunque solo una sea visible en cada momento. El panel de resumen renderiza además el estado completo de todos los pasos ya completados.

El navegador hace layout, cálculo de estilo y pintado de todos los nodos del DOM con independencia de su visibilidad. Con quince o más secciones montadas — facturación, servicios extra, datos del huésped, documentación y el resto — eso es mucho trabajo de render innecesario. La configuración de pasos define más de treinta componentes, y todos pueden existir en el DOM durante las transiciones.

Es un problema que se nota sobre todo en dispositivos modestos, donde el cuello de botella es el render y no la red.

Impacto en Core Web Vitals

MétricaEfecto
TBTSe reduce con la corrección — menos trabajo de layout y pintado significa tareas más cortas en el hilo principal.
INPMejora con la corrección — menos elementos renderizados significa respuesta más rápida al desplazamiento y a la entrada de datos.

Impacto medido

93%225/242225 de 242 nodos entran en layoutLa raíz del layout es #document: no hay contención,así que el navegador recalcula prácticamente todoel documento en cada pasada.
Proporción de nodos que entran en cada pasada de layout. El panel de resumen de DevTools para la tarea seleccionada indica 3,41 ms de duración, raíz #document y, como primera invalidación, la detección de cambios de Angular. Reconstruido a partir de PERF-010-flame-chart.png.
Umbral de fluidez (60 fps)16,6 msFrame medido A133,5 ms · 8× el umbralFrame medido B108,4 ms · 6,5× el umbral
Duración de frames frente al umbral de fluidez. Dos frames consecutivos muy por encima de los 16,6 ms que marcan los 60 fps. Reconstruido a partir de la pista Frames de PERF-010-flame-chart.png.
El síntoma más revelador

Con el resaltado de pintado activado en DevTools, desplegar el submenú de la barra lateral provoca destellos de repintado sobre el contenido principal del formulario: campos que no tienen ninguna relación con la interacción. Ocurre porque las secciones no tienen contención de pintado: cualquier cambio del DOM que el navegador no pueda acotar fuerza un repintado del documento entero. content-visibility: auto aplica implícitamente contain: layout style paint, que confina los repintados al límite del elemento.

Solución recomendada

Opción A · CSS content-visibility, sin tocar JavaScript

// En los estilos de sección del formulario compacto
.booking-form-section {
  content-visibility: auto;
  contain-intrinsic-size: auto 300px;  // altura aproximada, evita CLS
}

Opción B · @defer (on viewport) de Angular — recomendada

Difiere además la inicialización del componente y su detección de cambios, no solo el layout y el pintado.

<guest-details-form [state$]="state$" />   <!-- crítico: siempre -->

@defer (on viewport) {
  <billing-form [state$]="state$" />
}
@placeholder {
  <div class="section-placeholder" style="min-height: 300px"></div>
}

@defer (on viewport) {
  <extra-services-form [state$]="state$" />
}
@placeholder {
  <div class="section-placeholder" style="min-height: 300px"></div>
}

Evidencia

Condiciones de captura — atención a la desviación

Producción autenticada, España (UE), MacBook Pro M3 sobre fibra. Sin limitación de red. Se aplica un factor 4× de CPU donde se indica, porque en un problema de render el cuello de botella representativo es la CPU y no la red. Línea base completa en la Sección D.

FicheroEstadoNotas
PERF-010-flame-chart.pngRecopiladaTarea de layout con 225 nodos sucios
PERF-010-paint-flashing.pngRecopiladaRepintado provocado por una interacción ajena
PERF-010-recording.mp4RecopiladaGrabación del repintado
PERF-010-baseline-trace.jsonPendienteDuración total de Layout, Recalculate Style y Paint, con y sin la corrección
PERF-010.harPendientePrioridad baja: es un problema de render, no de red

Mejora esperada

Aplicando content-visibility: auto o @defer (on viewport), las secciones fuera de pantalla quedan excluidas de la pasada de layout y el recuento de nodos sucios baja a los 40–60 que están realmente en el viewport. Mejora esperada: reducción del 40 al 60 % en tiempo de layout y pintado. En dispositivos modestos, entre 200 y 500 ms menos de Time to Interactive y un desplazamiento apreciablemente más suave.

El multiplicador esperado para el mercado remoto está entre 2 y 4×, y aquí lo impulsa el hardware más limitado, no la red.

Referencias

11PERF-011P1 · Victoria rápida

Deduplicar las llamadas a la API

Cambiar un solo campo del formulario lleva la sesión de 32 llamadas a 57. Diecisiete de ellas son idas y vueltas sobrantes a endpoints que ya habían respondido.

Alcance
/reserva/crear
Métricas
TTIINP
Esfuerzo
Medio
Responsable
Frontend
Estado
Abierta
~21,8 s de tiempo de red estimado como desperdiciado tras cambiar un único campo, frente a ~2,4 s en la carga de página. Línea base España/fibra; en el mercado remoto se desperdicia entre 2 y 4× más.
Se compone con PERF-005

Cada llamada duplicada cuesta una ida y vuelta completa hasta el origen. Deduplicar elimina la llamada; corregir PERF-005 abarata las que queden. Las dos merecen la pena, y se multiplican entre sí.

Problema

El análisis de red muestra que varios endpoints se llaman muchas más veces de las necesarias.

v2/extra-services7 llamadas · se esperan 1–2v2/travel-documents7 llamadas · se esperan 1–2room-details6 llamadas · se esperan 1–2rate-details3 llamadas · se esperan 1–2
Llamadas por endpoint tras cambiar un campo. Frente a las una o dos que cabría esperar en cada caso. Reconstruido a partir de PERF-011-call-summary_after.json.
Solo carga de página32 llamadas32 · ~2,4 s desperdiciadosTras cambiar un campo57 llamadas40 esperadas · 17 sobranactualtras la corrección
Volumen total de llamadas. En la carga de página ya hay exceso; una sola interacción lo agrava. Reconstruido a partir de PERF-011-call-summary.json y _after.json.

Cada llamada duplicada desperdicia una ida y vuelta de 150 a 250 ms sobre la línea base España/fibra, CPU de backend para una petición idéntica, y entre 50 y 200 KB de ancho de banda del usuario por respuesta.

Impacto en Core Web Vitals

MétricaEfecto
TTIDegradada — las llamadas redundantes compiten por el ancho de banda de la conexión HTTP/2 y retrasan la disponibilidad de los esquemas.
INPDegradada — las llamadas que dispara el cambio de un campo se lanzan varias veces y producen tirones visibles.

Impacto medido

MétricaCarga de páginaTras un cambio de campo
Llamadas totales3257
Exceso sobre las ~40 esperadas+17
Endpoints con duplicados411
Tiempo de red desperdiciado (estimado)~2,4 s~21,8 s
Peor casov2/extra-services
×5, media 324 ms
v2/extra-services
×7, media 670 ms

Solución recomendada

Nivel 1 · debounceTime y distinctUntilChanged

formValueChanges$.pipe(
  debounceTime(300),
  distinctUntilChanged((a, b) => isEqual(a, b)),           // ignora payloads idénticos
  switchMap((payload) => this.schemaService.fetch(payload)), // cancela las que vuelan
)

switchMap es el operador crítico: cancela las peticiones en vuelo cuando llega un disparo nuevo, lo que evita que una respuesta antigua aterrice después de una reciente.

Nivel 2 · Deduplicar peticiones en vuelo

// libs/shared/schemas/data-access — capa de deduplicación
@Injectable({ providedIn: 'root' })
export class SchemaRequestDeduplicator {
  private readonly inFlight = new Map<string, Observable<unknown>>();

  deduplicate<T>(key: string, request$: Observable<T>): Observable<T> {
    if (!this.inFlight.has(key)) {
      const shared$ = request$.pipe(
        share({ resetOnComplete: true, resetOnError: true }),
        finalize(() => this.inFlight.delete(key)),
      );
      this.inFlight.set(key, shared$);
    }
    return this.inFlight.get(key) as Observable<T>;
  }
}

Como clave, un hash del payload de la reserva.

Nivel 3 · Cachear respuestas deterministas

Para los endpoints cuya respuesta es determinista dado el mismo payload de entrada, cachear en cliente con el hash del payload como clave, e invalidar al reiniciar el estado de la reserva.

Evidencia

Condiciones de captura

Producción autenticada, España (UE), MacBook Pro M3 sobre fibra, sin limitación. Condiciones completas en la Sección D. Con RTT de 300–500 ms cada duplicado cuesta de 2 a 4× más.

FicheroEstadoNotas
PERF-011.harRecopiladaCon caché, todas las llamadas de una carga completa
PERF-011-trace.json.gzRecopiladaBloques XHR repetidos: no hay cancelación ni deduplicación activa
PERF-011-duplicate-calls.pngRecopiladaListado completo, ordenado por nombre
PERF-011-endpoint-detail.pngRecopiladaDetalle del peor caso, con una petición cancelada en vuelo
PERF-011-call-summary.png / .txtRecopiladaSalida del snippet
PERF-011-call-summary.json · _after.jsonRecopiladaLínea base y post-interacción

Mejora esperada

Tras una única interacción se observan 57 llamadas donde se esperan unas 40: 17 idas y vueltas sobrantes. A entre 150 y 670 ms cada una sobre la línea base España/fibra, eliminarlas reduce el tiempo de red desperdiciado estimado de ~21,8 s a prácticamente cero.

Con RTT de 300–500 ms la ganancia escala en la misma proporción: la corrección vale bastante más para el mercado prioritario de lo que sugieren las cifras europeas.

Referencias

12PERF-012P3 · Infraestructura

Activar HTTP/2 103 Early Hints

Durante los primeros 463 ms de una primera carga el navegador ha enviado su petición y no ha recibido nada. No puede descargar ni un recurso. Early Hints convierte esa ventana muerta en tiempo de descarga.

Alcance
Todas las rutas servidas
por el shell
Métricas
FCPLCP
Esfuerzo
Medio
Responsable
Infra + Plataforma
Estado
Abierta
~413 ms de tiempo ocioso recuperable por primera carga: una ventana observada de 463 ms menos unos 50 ms estimados de retardo de la respuesta 103.

Problema

El TTFB del motor de reservas ronda los 250 ms, y en la captura de abajo se midió en 338 ms. Durante toda esa ventana el navegador ha enviado su petición y no ha recibido nada: no puede empezar a pedir ningún recurso porque todavía no sabe cuáles son.

HTTP/2 103 Early Hints permite al servidor enviar una respuesta preliminar con cabeceras Link de preload antes de que la respuesta HTML completa esté lista. El navegador puede actuar sobre esas pistas de inmediato.

0150300450600750msDNS + TCP + TLS81 msEspera de respuesta (TTFB)338 ms sin descargar nadaPrimer recurso críticoCon 103 Early Hintslos recursos empezarían aquí
La ventana ociosa, y lo que ocuparía Early Hints. El handshake completo son 81 ms; la espera de respuesta, 338. La barra verde marca dónde podrían empezar a descargarse los recursos críticos. Reconstruido a partir de PERF-012-ttfb-timing.png y PERF-012-ttfb-summary.png.

Impacto en Core Web Vitals

MétricaEfecto
FCPMejora — el CSS y el JS críticos empiezan a descargarse durante el tiempo de proceso del servidor.
LCPMejora — los recursos críticos llegan antes y desbloquean el render.

La técnica rinde justo cuando el TTFB es alto, que según PERF-005 es aquí la condición normal.

Impacto medido

Conexión nueva — 2 de junio de 2026, España → /es-mx/

FaseDuración
Resolución DNS20,11 ms
Conexión inicial38,98 ms
SSL21,90 ms
Petición enviada0,91 ms
Espera de respuesta — TTFB338,11 ms
Descarga de contenido2,18 ms
Total402,58 ms

Los 338 ms de TTFB están por encima de la línea base de ~250 ms de PERF-005, algo coherente con la variabilidad del servidor. DNS, TCP y TLS juntos suman unos 81 ms: cuatro veces menos que el TTFB por sí solo, lo que convierte la ventana ociosa en el coste dominante con diferencia.

Conexión reutilizada — HTTP/2

FaseDuración
Encolado y bloqueo1,57 ms
Petición enviada0,23 ms
Espera de respuesta — TTFB228,40 ms
Descarga de contenido1,89 ms
Total232,09 ms

El TTFB con la conexión reutilizada es menor porque no hay coste de conexión, pero siguen siendo 228 ms durante los cuales no puede descargarse nada.

Ausencia de pistas confirmada en las dos capas

Solución recomendada

Nginx

location /reserva/crear {
  add_header Link "</now-web/main.js>; rel=preload; as=script";
  add_header Link "</now-web/styles.css>; rel=preload; as=style";
  return 103;
  proxy_pass http://upstream;
}

En el CDN

La mayoría de CDN modernos ofrecen Early Hints como una opción de un solo clic, e incluso infieren las cabeceras Link a partir de la respuesta HTML anterior. Los assets concretos a precargar deben salir del stats.json del build: los chunks iniciales de CSS y JS que hacen falta en todas las cargas.

Evidencia

Condiciones de captura

Producción autenticada, España (UE), MacBook Pro M3 sobre fibra, sin limitación. Condiciones completas en la Sección D. En el mercado latinoamericano el TTFB se estima aún mayor, entre 400 y 600 ms, lo que hace la técnica proporcionalmente más rentable.

FicheroEstadoNotas
PERF-012-no-cache.harRecopiladaTTFB del documento ≈ 338 ms; todo el CSS y JS arrancan después
PERF-012-no-cache-trace.json.gzRecopiladaHueco de TTFB antes de cualquier barra de descarga
PERF-012-ttfb-timing.png · -warm.pngRecopiladaConexión nueva y reutilizada
PERF-012-response-headers-01.png · -02.pngRecopiladaSin cabecera Link:
PERF-012-resource-hints.png / .txtRecopiladaSin preload ni preconnect propios en el DOM
PERF-012-ttfb-summary.pngRecopiladaConfirmación numérica de la ventana ociosa
PERF-012-ttfb-summary.txtPendienteVersión en texto de la salida del snippet

Mejora esperada

103 Early Hints puede recuperar hasta ~413 ms de tiempo ocioso por primera carga. Los recursos críticos empezarían a descargarse durante la espera de TTFB en lugar de después.

Referencias

13PERF-013Resuelta

Cargar el SDK de mapas bajo demanda

666 KB de SDK de mapas se cargaban a los 4,7 segundos de cada visita, sin ningún mapa en pantalla y sin ninguna interacción previa. Había dos disparadores independientes; hubo que corregir los dos.

Alcance
/reserva/crear
campos de dirección
Métricas
TBTTTI
Esfuerzo
Bajo
Responsable
Frontend
Estado
Resuelta
666 KB de JavaScript pedidos a los 4 716 ms sin una sola interacción previa — 186 KB comprimidos, 84 ms de parseo fuera del hilo principal y 20 ms de evaluación dentro, en cada visita, también con caché.

Encuadre estratégico

Primero preguntar si el recurso hace falta

Antes de recurrir a microoptimizaciones — preconnect, preload, compresión — conviene preguntarse si el recurso pinta algo en esta vista. Aquí no: no se renderiza ningún mapa en /reserva/crear; el SDK solo hace falta en el momento en que alguien enfoca un campo de dirección y empieza a escribir; y quien escribe la dirección a mano no lo necesita nunca.

La carga bajo demanda y el patrón de fachada son la primera medida correcta para cualquier script de terceros pesado que no esté en la ruta crítica. Preconnect es una mejora secundaria, útil solo después de arreglar la carga anticipada: mientras el script se carga incondicionalmente no aporta absolutamente nada.

Problema

01 0002 0003 0004 0005 0006 000msPetición del SDK666 KBParseo fuera del hilo principal84 msEvaluación en el hilo principal20 msInteracción del usuarioninguna en toda la secuencia
Secuencia de carga del SDK de mapas. La última fila es la clave: no hay ninguna interacción del usuario en toda la secuencia. Reconstruido a partir de PERF-013-no-cache-trace.json.gz.

El SDK solo hace falta cuando el usuario enfoca un campo de dirección y escribe (autocompletado), cuando selecciona una sugerencia (geocodificación para extraer los componentes de la dirección), o cuando consulta el mapa de la propiedad, que es una ruta distinta y ajena a /reserva/crear.

Causa raíz — dos disparadores, no uno

Disparador 1, principal · suscripción en el constructor

libs/booking/feature-create/src/components/address-input/address-input.component.ts:58

private readonly placesLibraryStatus =
  toSignal(this.mapsApiService.placesLibraryStatus$, {
    requireSync: true,   // ← suscripción síncrona al construir
  });

placesLibraryStatus$ deriva de api$, que inyecta la etiqueta <script> del SDK en el DOM:

Se renderiza el formulario
  → se construye AddressInputComponent
    → toSignal(placesLibraryStatus$, { requireSync: true })
      → placesLibraryStatus$ → api$
        → se inyecta <script src="maps.geo-vendor.com/...">
          → petición de red a los 4 716 ms   ← confirmado en la traza

Es incondicional: se dispara toque o no toque el usuario un campo de dirección.

Disparador 2, secundario · preloadLib() tras inicializar la vista

constructor() {
  effect(() => {
    if (!this.viewInitialized()) return;
    if (this.autocompleteEnabled()) {
      this.autocompleteApiService.preloadLib();  // ← se dispara en AfterViewInit
    }
  });
}

La memoización de api$ hace que el script se inyecte una sola vez, así que el disparador 1 se lleva la petición. Pero preloadLib() la provocaría igualmente si se eliminara el primero. Había que corregir los dos.

Solución aplicada

Corrección 1 · No suscribirse al estado del SDK en el constructor

El componente usaba placesLibraryStatus$ solo para decidir qué interfaz mostrar: campo de búsqueda si el SDK ha cargado, campo manual si ha fallado o sigue cargando. Ese patrón fuerza una carga del SDK únicamente para pintar la interfaz.

Se sustituye por un valor optimista: mostrar el campo de búsqueda por defecto, asumir que el SDK estará disponible, y caer al campo manual solo cuando falle de verdad. El esqueleto de carga sobra: al usuario no le aporta nada saber que el SDK se está inicializando antes de tocar el campo.

// Antes — dispara la carga del SDK al construir:
private readonly placesLibraryStatus =
  toSignal(this.mapsApiService.placesLibraryStatus$, { requireSync: true });

protected readonly showManualAddressInput = computed(
  () => this.placesLibraryStatus().failed
     || this.overrideShowManualAddressInput()
     || this.hasSomeAddressFieldFilledIn(),
);

// Después — nada se carga hasta que hay interacción.
private readonly mapsLoadFailed = signal(false);

protected readonly showManualAddressInput = computed(
  () => this.mapsLoadFailed()
     || this.overrideShowManualAddressInput()
     || this.hasSomeAddressFieldFilledIn(),
);
protected readonly showSearchAddressInput = computed(
  () => !this.showManualAddressInput(),
);

// Lo llama AddressSearchInputComponent cuando el SDK falla:
onMapsLoadFailed(): void {
  this.mapsLoadFailed.set(true);
}

Corrección 2 · Mover preloadLib() al evento de foco

// Después: el SDK carga solo cuando el usuario muestra intención
private handleTextInputFocus(): Subscription {
  return fromEvent(this.textInput, 'focus').subscribe(() => {
    this.autocompleteApiService.preloadLib();    // ← al primer foco
    if (this.currentPredictions().length > 0 || this.showLastAutocompleteOption()) {
      this.showDropdown();
    }
  });
}

preloadLib() ya tiene una guarda interna, así que llamarlo en cada foco es seguro: el script se inyecta una sola vez.

Mejora opcional · preconnect al pasar el ratón

Eliminada la carga anticipada, un preconnect al dominio del SDK en el mouseenter del contenedor de dirección le da al navegador ventaja sobre el handshake antes de que el usuario llegue a enfocar el campo. Importa sobre todo donde el RTT es alto.

Evidencia

Condiciones de captura

Producción autenticada, España (UE), MacBook Pro M3 sobre fibra, sin limitación. Condiciones completas en la Sección D.

FicheroEstadoNotas
PERF-013-no-cache.har · with-cache.harRecopiladaEl SDK carga de forma anticipada incluso con todo lo demás en caché
PERF-013-*-trace.json.gzRecopiladaSin eventos mousedown, click, focus ni keydown antes de la petición
PERF-013-maps-waterfall.pngRecopilada
PERF-013-maps-flamechart.pngRecopilada
devtools-snippet-perf013.jsRecopiladaSnippet reutilizable

Mejora obtenida

EscenarioAntesDespués
El usuario nunca toca un campo de dirección666 KB descargados, parseados y ejecutados0 KB — el SDK no llega a cargarse
El usuario enfoca un campo de dirección666 KB a los 4 716 ms666 KB al primer foco, en diferido
Usuario en el mercado remotoVarios segundos de descarga antes siquiera de parsearTotalmente diferido; con preconnect al pasar el ratón, el coste de RTT se reduce aún más

La línea base es España (UE) sobre fibra: el mejor caso posible. Con RTT de 300–500 ms y dispositivos modestos, las cifras del «antes» eran de 2 a 4× peores.

Referencias

14PERF-014P2 · Estratégica

Optimizar el sprite SVG de iconos

El sprite del sistema de diseño lleva 694 iconos. La aplicación usa 76. Los otros 618 se envían a todos los usuarios en todas las cargas y no se renderizan ni una sola vez.

Alcance
Todas las rutas
el sprite es global
Métricas
LCPTBT
Esfuerzo
Medio
Responsable
Frontend + Diseño
Estado
Abierta
11 % del sprite se usa: 76 iconos de 694. El fichero pesa 861 KB descomprimidos, 292 KB en red, y empieza a descargarse a los 3 077 ms, justo dentro de la ventana de LCP.

Problema

design-system-icons.svg es el sprite de iconos del sistema de diseño corporativo y se carga como asset estático en todas las páginas. Está referenciado en la configuración de assets del proyecto:

{
  "glob": "design-system-icons.svg",
  "input": "node_modules/@vantia/design-system",
  "output": "/assets"
}

Este asset monolítico bloquea el hito de render de los dos segundos y contribuye al Total Blocking Time.

Impacto en Core Web Vitals

MétricaEfecto
LCPBloqueada — el sprite compite con recursos críticos justo en la marca de los dos segundos.
TBTAumenta — la descarga y el parseo de un asset grande alimentan tareas largas del hilo principal.

Auditoría de uso de iconos

11%89%76 iconos usados618 iconos que nunca se renderizan
Proporción real de uso del sprite. De los 76 iconos usados en toda la aplicación, solo 26 aparecen en /reserva/crear. Reconstruido a partir de PERF-014-icon-audit.txt y PERF-014-used-icons.txt.
MétricaValor
Iconos totales en el sprite694
Iconos usados en toda la aplicación76
Iconos visibles en /reserva/crear26
Ratio de uso11 %
Tamaño en red292 KB comprimidos
Tamaño descomprimido861 KB
Momento de inicio de descarga3 077 ms
Duración de la descarga244 ms

Solución recomendada

Paso 1 · Auditar el uso — hecho

grep -r 'icon="[^"]*"' apps/now-web libs --include="*.html" | \
  grep -o 'icon="[^"]*"' | sort -u

Resultado: 76 iconos únicos en todas las plantillas, de 694 disponibles.

Paso 2 · Generar un sprite a medida — preferido

// scripts/build-icon-sprite.js — dentro del pipeline de build
const svgstore = require('svgstore');
const usedIcons = require('./used-icons.json');   // salida del paso 1

const sprite = svgstore();
usedIcons.forEach(iconId => {
  sprite.add(
    iconId,
    fs.readFileSync(`node_modules/@vantia/design-system/icons/${iconId}.svg`)
  );
});
fs.writeFileSync('dist/assets/now-icon-sprite.svg', sprite.toString());

Después se sustituye el glob de assets por el sprite generado.

Paso 3 · Incorporar en línea los iconos visibles al cargar

<!-- En lugar de <vds-icon icon="status_check_small"> -->
<svg aria-hidden="true" width="16" height="16" focusable="false">
  <use href="#status_check_small"></use>
</svg>

Paso 4 · Carga diferida — alternativa si el sprite a medida no es viable

afterNextRender(() => {
  const link = document.createElement('link');
  link.rel = 'preload';
  link.as = 'image';
  link.type = 'image/svg+xml';
  link.href = '/assets/design-system-icons.svg';
  link.setAttribute('fetchpriority', 'low');
  document.head.appendChild(link);
});

Evidencia

Condiciones de captura

Producción autenticada, España (UE), MacBook Pro M3 sobre fibra, sin limitación. Condiciones completas en la Sección D.

FicheroEstadoNotas
PERF-014-no-cache.har · with-cache.harRecopiladaArranque a los 3 077 ms, 292 KB en red / 861 KB descomprimidos
PERF-014-*-trace.json.gzRecopiladaBarra de descarga visible en la ventana de 3–3,3 s
PERF-014-sprite-waterfall.pngRecopiladaEl sprite junto a los recursos críticos concurrentes
PERF-014-sprite-flamechart.pngRecopiladaBarra ancha de descarga solapando la ventana de LCP
PERF-014-icon-audit.txt · used-icons.txtRecopiladaAuditoría completa y los 76 iconos referenciados

Mejora esperada

Sprite completo actual861 KBsin cambiosSprite a medida (76 iconos)861 KB~95–130 KB · −85 %actualtras la corrección
Peso del sprite antes y después. Un sprite que contenga solo los 76 iconos usados se estima entre 95 y 130 KB comprimidos: alrededor de un 85 % menos. Estimación sobre la proporción medida de uso.

Sobre la línea base España/fibra el sprite arranca a los 3 077 ms y tarda 244 ms, completándose hacia los 3 321. Con RTT de 300–500 ms, la ventana de descarga de 861 KB se estima entre 2 y 3× más larga, lo que añade dos o tres segundos para el mercado prioritario.

Referencias

Sección E

Dónde este informe es débil

Dicho sin rodeos, para que nadie tenga que descubrirlo en la reunión.

IncidenciaHueco
TodasNo existe ninguna captura tomada desde el mercado latinoamericano. Todas las cifras de ese mercado son extrapolaciones desde la línea base europea.
PERF-003Faltan el panel Timing del SDK de mapas y la traza de rendimiento.
PERF-004Faltan las capturas de cascada con y sin caché.
PERF-005No hay línea base desde EE. UU., así que el delta de RTT está inferido, no medido. Faltan también la captura de red con la columna TTFB y la traza.
PERF-007Faltan el detalle de tiempos del gestor de etiquetas y la cascada ampliada del asistente. El alcance del tracker de personalización está sin medir.
PERF-009Se apoya en un único HAR. Sin traza, sin capturas y sin desglose del 1–3 s en tiempo de chunk frente a tiempo de API.
PERF-010Faltan la traza de línea base y la comparativa posterior a la corrección.
PERF-012Falta la versión en texto del resumen de TTFB.
Una incoherencia que conviene resolver

En el material de partida aparecen dos cifras distintas para el trayecto UE→EE. UU.: unos 120 ms en las notas de evidencia de cada incidencia y unos 250 ms en el resumen de PERF-005. Ambas se han conservado tal como se registraron. Conviene confirmar una y corregir la otra antes de usar estos números para construir un caso de negocio.

Qué haría falta para cerrar los huecos

Sobre este documento

Informe de muestra elaborado por PerfReviews. Corresponde a una auditoría real, anonimizada: las cifras, los hallazgos y las recomendaciones son los originales; el cliente, el sector y los proveedores de terceros se han ficcionado. Los gráficos son reconstrucciones vectoriales de los datos de captura.

perf.reviews · mail@perf.reviews

Esto es lo que recibes en una auditoría de PerfReviews

El informe que acabas de leer es el entregable completo del plan de Auditoría, con el cliente y los proveedores ficcionados. Sobre tu web, con tus datos y tus prioridades, el formato es este.

Y como se explica justo arriba, un informe es una fotografía: sin datos de campo ni tests de regresión, dentro de tres meses no habrá forma de saber si las correcciones aguantaron. Para eso están los planes con monitorización continua y detección de regresiones en el pipeline.

Hablemos de tu web Ver planes y monitorización continua Descargar en PDF

También disponible en inglés. Escríbenos a mail@perf.reviews.