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

El patrón de fachada: pagar la incrustación solo si se usa

Cómo sustituir una incrustación de un megabyte por unos kilobytes que se ven igual, la implementación accesible con los disparadores de intención, y las cuatro incrustaciones donde más rinde.

⏱ 19 min

Una incrustación de vídeo carga alrededor de un megabyte de JavaScript y decenas de peticiones para mostrar una imagen fija con un triángulo encima. Una fachada muestra esa misma imagen fija con ese mismo triángulo por unos pocos kilobytes, y carga el reproductor de verdad solo cuando alguien demuestra intención de usarlo. Es la técnica con mejor relación entre esfuerzo y resultado de todo el track, y la mayor parte de la dificultad está en los detalles que casi todas las implementaciones hacen mal.

🎯 Al terminar esta lección sabrás
  • Implementar una fachada accesible y visualmente idéntica a la incrustación real.
  • Escalonar los disparadores de intención para que la carga empiece antes del clic.
  • Aplicar el patrón a las cuatro familias de incrustación con sus particularidades.
  • Reconocer los tres casos donde una fachada está contraindicada.

La idea y su implementación

La fachada es un componente ligero que reproduce el estado inicial de la incrustación —lo que se ve antes de interactuar— y la sustituye por la incrustación real en cuanto el usuario da una señal de que la va a usar. El ahorro es todo el coste del tercero para la gran mayoría de visitantes, que nunca pulsan el botón.

Los números que justifican el esfuerzo: una incrustación de vídeo estándar está en torno al megabyte de recursos y varias decenas de peticiones a orígenes que la página no usaba; la fachada equivalente son una imagen de la miniatura, del orden de 20 a 40 kilobytes en un formato moderno, más uno o dos kilobytes de código. En una página de contenido con un vídeo incrustado, sustituirlo suele ser la mayor mejora individual disponible.

La implementación como elemento personalizado, sin dependencias:

class VideoFachada extends HTMLElement {
  connectedCallback() {
    const id = this.getAttribute('video');
    const titulo = this.getAttribute('titulo') || 'Reproducir video';
    if (!id || this.querySelector('button')) return;

    const boton = document.createElement('button');
    boton.type = 'button';
    boton.className = 'fachada-boton';
    // Un boton de verdad: accesible por teclado y anunciado por lectores.
    boton.setAttribute('aria-label', titulo);
    boton.innerHTML = '<span class="fachada-triangulo" aria-hidden="true"></span>';
    this.append(boton);

    // Intencion debil: preparar la conexion, no cargar nada.
    const preparar = () => {
      this.#preconectar();
      this.removeEventListener('pointerenter', preparar);
      this.removeEventListener('focusin', preparar);
    };
    this.addEventListener('pointerenter', preparar, { once: true });
    this.addEventListener('focusin', preparar, { once: true });

    // Intencion fuerte: cargar de verdad.
    boton.addEventListener('click', () => this.#activar(id, titulo));
  }

  #preconectar() {
    for (const origen of [
      'https://www.youtube-nocookie.com',
      'https://i.ytimg.com',
      'https://www.google.com',
    ]) {
      const l = document.createElement('link');
      l.rel = 'preconnect';
      l.href = origen;
      document.head.append(l);
    }
  }

  #activar(id, titulo) {
    const marco = document.createElement('iframe');
    marco.src =
      `https://www.youtube-nocookie.com/embed/${id}?autoplay=1&rel=0`;
    marco.title = titulo;
    marco.allow =
      'accelerometer; autoplay; encrypted-media; picture-in-picture';
    marco.allowFullscreen = true;
    marco.loading = 'eager';
    this.replaceChildren(marco);
    // El foco viaja al reproductor: quien llego con el teclado sigue ahi.
    marco.focus();
  }
}

customElements.define('video-fachada', VideoFachada);
<video-fachada video="dQw4w9WgXcQ" titulo="Reproducir la demostracion del producto">
  <img
    src="/miniaturas/demo.webp"
    alt=""
    width="1280" height="720"
    loading="lazy" decoding="async" />
</video-fachada>
video-fachada {
  display: block;
  position: relative;
  aspect-ratio: 16 / 9;   /* reserva el hueco: cero CLS */
  background: #000;
}
video-fachada img,
video-fachada iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  object-fit: cover;
}
video-fachada .fachada-boton {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  width: 100%;
  border: 0;
  background: transparent;
  cursor: pointer;
}
video-fachada .fachada-boton:focus-visible {
  outline: 3px solid #89b4fa;
  outline-offset: -3px;
}

Los detalles que separan una fachada buena de una mala están todos en ese código y ninguno es opcional:

Un button de verdad, no un div con un manejador. Es la diferencia entre funcionar y no funcionar con teclado y con lectores de pantalla. Un div con onclick no recibe foco, no responde a la barra espaciadora y no se anuncia como control.

El aria-label describe el vídeo concreto, no dice “reproducir”. Una página con seis fachadas y seis botones llamados “reproducir” es inservible para quien navega por lista de controles.

El alt de la imagen está vacío a propósito. La imagen es decorativa: la información la da el botón. Poner texto en los dos hace que se anuncie dos veces.

El aspect-ratio reserva el hueco desde el primer momento, así que la sustitución de la imagen por el iframe no desplaza nada. Sin él, la fachada arregla el LCP y estropea el CLS.

El foco viaja al iframe al activarse. Sin esa línea, quien llegó con teclado se queda con el foco en un botón que ya no existe, y el punto de partida vuelve al principio del documento.

Los disparadores de intención

Una fachada que solo reacciona al clic añade una espera perceptible justo en el momento en que el usuario ha decidido actuar. La solución es escalonar señales de intención crecientes, cada una con un coste proporcional a la certeza que aporta:

Señal Certeza Qué se hace Coste si te equivocas
El elemento entra en el viewport baja Nada, o dns-prefetch Despreciable
Puntero encima o foco dentro media preconnect a los orígenes Un apretón de manos inútil
pointerdown o touchstart alta Empezar a cargar el script Se carga y no se usa
click total Sustituir por la incrustación Ninguno

El escalón de pointerdown merece atención porque es donde está la ganancia percibida. Entre que el dedo baja y que el evento de clic se dispara pasan unas decenas de milisegundos —más en móvil, donde hay que descartar que sea un desplazamiento—, y esos milisegundos son gratis para adelantar trabajo. Es el mismo razonamiento que la precarga por intención de las rutas, y aquí funciona igual de bien.

Con la conexión ya establecida por el preconnect del paso anterior, la carga del reproductor tras el clic pierde las tres idas y vueltas de establecimiento, que en móvil son fácilmente doscientos milisegundos. La diferencia entre una fachada con preconnect y una sin él es perfectamente perceptible.

Las cuatro familias

Vídeo. El caso canónico y el de mayor ahorro. La miniatura conviene servirla desde tu propio dominio y en un formato moderno, no desde el del proveedor: así controlas el tamaño, evitas un origen más y puedes ponerla en srcset. Existen implementaciones de código abierto probadas para las plataformas principales; el elemento de arriba es esencialmente lo que hacen.

Mapas. La fachada es una imagen estática del mapa —muchas plataformas tienen un endpoint de mapa estático— con los marcadores dibujados, y encima un botón de “ver mapa interactivo”. El ahorro es del mismo orden que el del vídeo. Y hay una pregunta previa que hay que hacerse siempre: la mayoría de los mapas incrustados en páginas de contacto no necesitan ser interactivos. Una imagen estática con un enlace a la aplicación de mapas del usuario cumple mejor la función real, que es “cómo llego”, y cuesta cuarenta kilobytes.

Chat de soporte. El más rentable en tiempo de hilo principal porque son de los scripts más pesados que existen. La fachada es el propio botón flotante, dibujado por ti en CSS, que carga el widget real al pulsarlo. Aquí aparece la objeción de negocio de forma inevitable: los widgets de chat tienen funciones de apertura proactiva —“¿te ayudo en algo?” a los treinta segundos— que una fachada desactiva. Es una negociación real y tiene una salida razonable: cargar el widget de verdad, pero al cabo de un tiempo de inactividad y en tiempo libre, con requestIdleCallback, de modo que la apertura proactiva siga funcionando y el coste caiga fuera de la carga.

Redes sociales. Publicaciones incrustadas, botones de compartir, contadores. La fachada es una cita estática con el texto, la imagen y un enlace. Es la familia donde más se gana con menos esfuerzo, porque el estado inicial de una publicación incrustada es texto e imagen, exactamente lo que sabes reproducir. Y los botones de compartir con contador se sustituyen por enlaces normales a las URL de compartir de cada plataforma, que no requieren ningún script:

<!-- Cero JavaScript, cero terceros, misma funcion -->
<a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fejemplo.com%2Fpost"
   rel="noopener">Compartir</a>

Cuándo no

Tres situaciones en las que la fachada es la respuesta equivocada.

Cuando la incrustación es el contenido principal y se usa casi siempre. En una página cuyo único propósito es ver un vídeo, la fachada añade un clic y retrasa lo que el usuario venía a hacer. Ahí lo correcto es cargar el reproductor con prioridad y optimizar su carga.

Cuando la fachada no puede parecerse lo suficiente. Si el estado inicial de la incrustación tiene información dinámica que no puedes reproducir, la fachada produce un salto visual desagradable al sustituirse. Más vale una carga diferida honesta que una imitación mala.

Cuando el tercero prohíbe reproducir su interfaz. Algunos términos de servicio exigen usar el código de incrustación tal cual. Es un problema legal, no técnico, y conviene comprobarlo antes de implementar y no después.

La fachada no es una técnica de carga diferida: es la decisión de que el coste lo pague quien usa la funcionalidad, y esa idea se aplica mucho más allá de las incrustaciones

Es tentador archivar esto en la carpeta de “cargar cosas más tarde” junto a loading="lazy" y el import dinámico, y perderse lo que tiene de distinto. La carga diferida normal no cambia quién paga: todo el mundo acaba pagando el coste completo, solo que repartido en el tiempo. Una fachada sí cambia quién paga: si el noventa y dos por ciento de los visitantes no pulsa el botón, el noventa y dos por ciento no paga nada, nunca. Has pasado de un coste fijo por visita a un coste variable por uso, y eso no es una optimización de la carga, es un cambio en la estructura del gasto. La distinción parece filosófica y tiene consecuencias muy concretas: significa que la magnitud del ahorro no la determina el peso del tercero sino la tasa de uso de la funcionalidad, y esa tasa es un dato que casi nunca se mide y que casi siempre sorprende cuando se mide. Instrumenta el clic de tus fachadas antes de discutir con nadie: he visto vídeos incrustados en portadas con tasas de reproducción por debajo del dos por ciento y mapas de la página de contacto que nadie arrastra jamás. Con ese número en la mano, la conversación deja de ser sobre milisegundos y pasa a ser sobre por qué el noventa y ocho por ciento de los usuarios financia la experiencia del dos por ciento. Y en cuanto ves la idea en esos términos, la reconoces en sitios donde no habías pensado en ella. El editor de texto enriquecido que solo se abre al pulsar “escribir un comentario”. La biblioteca de gráficas de la pestaña de estadísticas que casi nadie abre. El selector de fechas complejo del formulario avanzado. El paquete de traducciones de los otros seis idiomas. El corrector ortográfico. Todos son el mismo patrón: una funcionalidad cuyo coste se cobra por adelantado a todo el mundo y cuyo uso está concentrado en una minoría. La pregunta que hay que aprender a hacerse ante cualquier bulto de código es “¿qué fracción de mis usuarios ejecuta esto?”, y cuando la respuesta está por debajo de un tercio, casi siempre hay una fachada esperando a que alguien la escriba. Es, con diferencia, el patrón mental de esta lección que más veces vas a reutilizar.

⚔️ Convierte tu incrustación más cara
  1. Localiza la incrustación más pesada de tu sitio y mide sus bytes y sus peticiones antes de tocar nada.
  2. Instrumenta cuántos visitantes la activan de verdad. Ese porcentaje es el tamaño de tu ahorro.
  3. Implementa la fachada con el elemento personalizado de arriba y verifica los cinco detalles de accesibilidad, incluido el recorrido con teclado de principio a fin.
  4. Añade los cuatro escalones de intención y mide el tiempo entre el clic y el primer fotograma del reproductor con y sin preconnect.
  5. Busca en tu propio código tres funcionalidades que use menos de un tercio de tus usuarios y aplícales la misma idea.