Pasar al contenido principal
ID.R

idiazroncero.com

Drupal: lazy load de contenido para un mejor rendimiento, UX y SEO

Uno de los secretos mejor guardados de Drupal es cuándo y cómo se renderiza el contenido utilizando «placeholders» que aumentan el rendimiento mediante la carga deferida (lazy load) de contenido altamente dinámico.
Una imagen no informativa e ilustrativa para el artículo. Filas de cubos que forman dos estructuras en forma de pared, con algunos cubos en diferentes colores o atenuados. Intenta transmitir la idea de tener una estructura en la que algunos elementos podrían «salirse» de la norma.

Esto previene una excesiva invalidación de la caché y normalmente ocurre automáticamente, pero también podemos tomar el control y beneficiarnos de ello. En este artículo aprenderemos sobre lazy loaders, BigPipe, placeholders automáticos y placeholder previews (vistas previas).

Aprenderemos cómo generar contenido altamente dinámico que no invalide nuestra caché y proporcione una buena experiencia de usuario sin los temidos flashes, saltos de página y repintados que pueden desencadenar CLS (Cumulative Layout Shifts) y afectar a nuestro rendimiento y SEO. Todo esto mientras proporcionamos una mejor experiencia de usuario (UX).

También veremos cómo los siguientes módulos y conceptos de Drupal se relacionan entre sí, cómo interactúan y cómo aprovecharlos conscientemente a nuestro favor:

  • Page Cache
  • Dynamic Page Cache
  • Lazy Builders
  • Placeholders y auto placeholders
  • Big Pipe

Este artículo está basado en un 99% en la charla que di en la DrupalCon de Barcelona, en 2024: aquí están las diapositivas y la página del evento!

Back to top

¿Por qué preocuparse? La relación entre rendimiento, SEO y UX

¿Deberíamos realmente preocuparnos acerca del rendimiento? La respuesta es: sí.

Rendimiento, SEO y UX están estrechamente interrelacionados. Un ejemplo - quizás el mejor - son los Core Web Vitals. No podemos entrar en más detalles aquí, si quieres saber más consulta la documentación oficial.

The core web vitals
LOos tres ángeles caídos

Los Core Web Vitals, desarrollados por Google, son tres indicadores de la velocidad de carga de tu sitio web (LCP), el rendimiento de la interactividad (INP) y la estabilidad visual (CLS).

Dos de ellos -especialmente LCP- están estrechamente relacionados con el rendimiento de tu web.

El tercero, CLS, puede verse afectado negativamente por las soluciones que apliquemos para aliviar los otros dos, en concreto las técnicas de lazy loading.

Todos ellos están relacionados con una mejor experiencia de usuario (UX), ya sea porque la página es menos propensa a cambios que afecten a la interfaz (CLS), carga más rápido (LCP) y sus interacciones son rápidas y eficientes (INP)

Los motores de búsqueda tienen en cuenta el rendimiento

El SEO se ve afectado también por los Core Web Vitals

Al clasificar tu página, Google afirma tener en cuenta lo siguiente:

«Los Core Web Vitals son utilizados por nuestros sistemas de clasificación. Recomendamos a los propietarios de sitios web que obtengan unos buenos Core Web Vitals para tener éxito con la búsqueda y para garantizar una gran experiencia de usuario en general.»

Dado que Google impulsa alrededor del 90% de las búsquedas, esto es bastante relevante: tu puntuación en Core Web Vitals afecta a tu SEO. Es decir, un mal rendimiento y una mala UX pueden perjudicar tu posicionamiento en buscadores.

El resto de motores de búsqueda hacen referencias más genéricas a la velocidad del sitio, que al fin y al cabo es otro nombre para el rendimiento.

La caché es fundamental

Cuando busques información acerca de cómo mejorar los Core Web Vitals, «caché» es una palabra que aparecerá mucho.

En efecto, la caché es importante, y por más razones de las aparentes.

  • Obviamente, ahorra tiempo a tus usuarios, lo que redunda en una mejor experiencia de usuario y tiene relación con las conversiones.
  • Ahorra emisiones de carbono para el planeta, ya que tu página será más eficiente. Como desarrolladores, también tenemos un deber para con nuestro planeta.
  • Hace felices a los motores de búsqueda, y también al departamento de marketing.
  • Hace que su web sea más inclusiva, ya que las personas con malas conexiones o dispositivos antiguos podrán navegar por su sitio.

Durante este artículo, nos centraremos en la caché de Drupal (aplicación). No tocaremos otras capas que pueden ser optimizadas como: caché del navegador, proxies inversos, caché de base de datos, etc....

Back to top

La caché (y sus problemas) en Drupal

El almacenamiento en caché es difícil. No sólo en Drupal, sino en general.

Asumimos que estás familiarizado con conceptos como cache hit / miss, cache tags, keys, contexts y max-age. Si no es así, lee la documentación primero y vuelve después: ese conocimiento es necesario para entender este artículo.

Históricamente, los principales problemas con la caché de Drupal han sido los siguientes:

Basta un solo caché miss para invalidar una página entera.

Esto puede sorprender si no has investigado mucho temas de caché (por lo menos a mí me sorprendió): basta que un solo elemento de la página pierde su caché para que sea necesario volver a renderizar toda la página.

En otras palabras: la caché de tu página es tan débil como su elemento más débil.

Si tienes una página perfectamente cacheable que durará días, pero un solo elemento de tu página está siendo invalidado cada tres minutos, entonces toda tu página está infectada y será reconstruida cada tres minutos.

¿Esto significa que habrá que renderizar todo el HTML de nuevo? No del todo. Sí, la página tendrá que volver a renderizarse y almacenarse en caché, pero la mayoría de los fragmentos de la página seguirán teniendo su propia caché de renderizado (render cache) independiente como caché de último recurso. Más información sobre esto en el próximo artículo.

Llamemos a estas partes ofensivas de tu página «contenido altamente dinámico» y pintémoslas de naranja:

A page layout with some static, cacheable content and a single highly dynamic part, marked in orange.
Una sola parte muy dinámica (naranja) puede afectar a las partes almacenables en caché de la página (azul).

Esto tiene grandes implicaciones para el rendimiento si tu contenido cambia a menudo.

Tomemos, por ejemplo, la etiqueta de caché node_list. Esta etiqueta se invalida cada vez que se crea, edita o elimina cualquier nodo.

Si su sitio tiene mucho contenido y muchas operaciones, esto significa que cualquier parte de nuestra página que dependa de esta etiqueta se invalidará cada pocos minutos o incluso segundos. Como resultado, la caché de su página se invalidará con demasiada frecuencia: se comportará casi como si no tuviera caché.

Esto es un gran problema para el rendimiento. Pero no es el único.

Algunos contextos de caché tienen una cardinalidad enorme

Una render array que muestra el nombre de nuestro sitio sólo tiene una dependencia (la configuración del nombre del sitio) y ningún contexto (es global - ¡sólo hay uno!).

Esto significa que es muy cacheable. Una vez procesado, permanecerá en la caché hasta que cambie la configuración de system.site - lo que ocurre raramente - y sólo tendrá una entrada en la caché.

$site_name = $this->config('system.site')->get('name');
$build = [
  '#markup' => $this->t('The site name is: @site_name', ['@site_name' => $site_name]),
  '#cache' => [
    'tags' => ['config:system.site']
  ]
];

Por el contrario, un render array contextual como user.name tendrá tantas variaciones posibles como usuarios haya en tu sitio. Necesitaremos una entrada de caché distinta para cada usuario.

$current_user = \Drupal::currentUser();
$user_name = $current_user->getDisplayName();
$build = [
  '#markup' => $user_name,
  '#cache' => [
    'contexts' => ['user'], // <--- Necesitamos una variación por cada usuario
  ]
];

Si tu página tiene decenas de miles de usuarios, esto puede convertirse en un problema. La base de datos puede saturarse y se pueden producir cuellos de botella.

Ni siquiera necesitas una cardinalidad enorme. Si unimos varios contextos, podemos hacer crecer exponencialmente las necesidades de almacenamiento. Piensa en el siguiente código:

$message = [
  '#markup' => "Welcome $user! You are using the $theme theme."
  '#cache' => [
    'contexts' => ['user', 'theme']
  ]
]

Ahora necesitarás una entrada de caché para cada combinación posible de usuario y tema. Si tienes 3000 usuarios y 3 temas, tendrás 3000 x 3 = hasta 9000 combinaciones posibles, ¡y cada una de ellas necesitará una entrada de caché distinta!

En resumen: es relativamente fácil tener páginas no cacheables

$build = [
  '#markup' => 'Related content written by you: { contents }'
  '#cache' => [
    'tags' => ['node_list'],
    'contexts' => ['user', 'url.path'],
    'max-age' => 60
  ]
]

Es posible que haya partes de tu código que, sin querer, hagan que tu página sea casi imposible de cachear o que puedan introducir problemas de rendimiento en la base de datos.

  • Si las caché tags son invalidadas demasiado pronto, con demasiada frecuencia.
  • Si los contextos de caché tienen una cardinalidad muy grande o se aplican muchos a la vez.
  • En valores bajos de max-age.

Para hacer las cosas más difíciles, algunos de los contextos incorporados por defecto en Drupal suelen tener una cardinalidad alta por defecto.

Piensa en los siguientes contextos: user, url.path, session, timezone, languages, cookies. Por su naturaleza, pueden variar mucho.

Esto significa que, especialmente, la caché de los usuarios con una sesión tiene la posibilidad de volverse frágil y sufrir problemas de rendimiento debido a la gran cantidad de contextos interrelacionados y etiquetas de caché fácilmente invalidables.

 

Back to top

El placer oculto de los placeholders

¿Significa esto que estamos condenados a un mal rendimiento? ¡No! Drupal tiene herramientas a nuestra disposición para lidiar con esas molestas e infecciosas partes altamente dinámicas o altamente variables de la página.

Pero, lo primero es lo primero, tenemos que aprender acerca de los placeholders, lazy builders y placeholder strategies.

Un placeholder es un sustituto de una parte de una página que se renderizará más tarde. Se generan cuando se cumplen ciertas condiciones.

Un lazy builder indica cómo renderizar el contenido final de un placeholder. Están estrechamente relacionados, pero no son lo mismo. Un lazy builder se ejecuta para reemplazar el placeholder.

Los placeholders necesitan una estrategia para ser renderizados. Las estrategias deciden cómo será el ciclo de vida de los placeholders.  

Por ahora, mantén estos tres conceptos en mente: tenemos placeholders (sustitutos) de una parte de una página, lazy builders (que renderizan el contenido real) y una estrategia para invocar a los lazy builders.

Lazy builders: el qué

  public function build() {
    $build['time'] = [
      '#lazy_builder' => [self::class . '::lazyBuilder', []],
      '#cache' => ['max-age' => 60]
    ];
    return $build;
  }
  
  public static function lazyBuilder() {
    $now = new DateTime();
    return [
      '#markup' => t("Today is @date", ['@date' => $now->format('d-m-Y H:i')]),
    ];
  }

Un #lazy_builder es básicamente un callback. En cualquier render array de Drupal podemos sustituir el contenido por una referencia a un callable (normalmente una función o método). Este método se utilizará cuando llegue el momento de renderizar el contenido real.

¿Sabías que... ya estás usando lazy builders?

Quizá lo sepas, quizá no, pero tus sitios ya se están beneficiando de muchos lazy builders establecidos por Drupal Core.

Accede a BlockViewBuilder.php para ver cómo:

public function viewMultiple() {
  foreach ($entities as $entity) {
    (...)
    if ($plugin instanceof ...) {
      $build[$entity_id] +=
        static::buildPreRenderableBlock($entity, $this->moduleHandler());
    }
    else {
      // Assign a #lazy_builder callback, which will generate a #pre_render-
      // able block lazily (when necessary).
      $build[$entity_id] += [
        '#lazy_builder' => [static::class . '::lazyBuilder', [
            $entity_id,
            $view_mode,
            $langcode
          ]
        ],
      ];
    }
  }
}

Este código básicamente envuelve cada bloque de Drupal en un #lazy_builder (con algunas excepciones, como el MainContentBlock). ¡Esto significa que casi todos los bloques de nuestro sitio web ya tienen un lazy builder!

¿Por qué? Supongo que es porque los lazy builders son obligatorios para que los placeholders funcionen. Así que, para habilitar los placeholders out of the box, Drupal crea automáticamente las condiciones necesarias sin que te des cuenta.

Esto hace que algunas cosas funcionen automágicamente, pero de alguna manera oculta el funcionamiento interno y podría ser la razón por la que nosotros, los desarrolladores de Drupal, por lo general sabemos poco sobre el funcionamiento interno de Big Pipe, Dynamic Cache y placeholders. Puesto que simplemente funciona, permanece oculto.

Placeholders: el cuándo

Los lazy builders son la parte más fácil: son simplemente un callback que se llamará si es necesario. Pero, ¿quién decide cuándo es necesario?

Como hemos dicho antes, los placeholders y los lazy builders están estrechamente relacionados, pero no son lo mismo.

Cualquier cosa que tenga un lazy builder tiene la posibilidad de convertirse en un placeholder, si se cumplen ciertas condiciones.

Estas condiciones son definidas por:

  1. Drupal, a través de las auto placeholder conditions definidas en services.yml
  2. ...o el desarrollador (¡tú!)

Auto placeholder conditions

Mira tu archivo services.yml y localiza estas líneas de código:

parameters:
  renderer.config:
    auto_placeholder_conditions:
      # Max-age at or below which caching is not considered worthwhile.
      max-age: 0
      # Cache contexts with a high cardinality.
      contexts: ["session", "user"]
      # Tags with a high invalidation frequency.
      tags: []

Estas condiciones definen max-age, contexts y tags que se consideran problemáticos debido a su alta cardinalidad o alta tasa de invalidación.

En otras palabras, estas condiciones de caché identifican el tipo de contenido (las partes naranjas) que queremos separar de las azules. Las zonas infecciosas.

Así, cuando el Renderer.php de Drupal ejecute tu código, creará automáticamente un placeholder para cada render array que:

  • Cumpla las condiciones de auto_placeholder_conditions y
  • Tenga un #lazy_builder

Creación manual de placeholders

También puedes decidir manualmente cuándo usar placeholders, incluso si no se cumplen las condiciones anteriores.

Es tan fácil como añadir una propiedad #create_placeholder a tus render arrays.

$build = [
  '#lazy_builder' => [static::class . '::renderMessages', [$element['#display']]],
  '#create_placeholder' => TRUE,
];

Por supuesto, la regla de oro sigue siendo válida: necesitas un #lazy_builder. De lo contrario, tu #create_placeholder será ignorado.

¿Pero esto se usa de verdad?

De hecho, esta técnica se usa bastante en Drupal Core. Comprueba los siguientes ejemplos:

Puedes ver que la creación manual de placeholders no es nada extraño. ¡Úsala!

¿Cómo se procesan los placeholders?

Como dijimos antes, cada vez que Drupal encuentra una render array que 1) tiene un #lazy_builder y 2) debería convertirse en un placeholder (ya sea a través de auto_placeholdering_conditions o #create_placeholder), lo procesa de una manera diferente a una render array normal.

La información se divide de la siguiente manera:

  • Un #markup cuyo contenido es un pseudo-html que representa el placeholder.
  • Una entrada de #attached con la información sobre cómo reemplazar el #markup anterior.

Algo así:

[
  "#markup" => "<drupal-render-placeholder callback arguments token >
    </drupal-render-placeholder>",
  "#attached" => [
    "placeholders" => [
      "<drupal-render-placeholder token...> </drupal-render-placeholder>" => [
        "#cache" => [
          "tags": ...,
        ],
        "#lazy_builder" => [
          "Drupal\block\BlockViewBuilder::lazyBuilder",
          [...]
        ]
      ]
    ]
  ]
]

Parece un cambio menor, pero algo interesante ha sucedido bajo la superficie: los metadatos de #cache se han movido de la render array a los #attached adjuntos. Esto será crucial para el almacenamiento en caché - ¡volveremos sobre esto más adelante!

Estrategias: el cómo

Por último, tenemos que decidir cómo será el ciclo de vida de los placeholders.

Como hemos visto, los placeholders se generarán en las render arrays de Drupal si se cumplen ciertas condiciones, y se enviarán junto con la información sobre cómo renderizarlos - una referencia al #lazy_builder.

Pero necesitamos a alguien que realmente decida el punto exacto y la forma en que esto sucederá. Eso es lo que hacen las PlaceholderStrategies (que son un tagged service).

Hablaremos con más detalle sobre esto más adelante, cuando BigPipe entre en acción. Por el momento, quedémonos con que SingleFlushStrategy es la estrategia para los placeholders por defecto y que simplemente renderiza los placeholders en la misma petición en la que fueron generados, pero en una fase posterior. Proporciona algunos beneficios, como veremos cuando hablemos de la caché dinámica de páginas, pero no defiere su carga a una fase posterior y por tanto es la estrategia que sirve de último recurso.

 

Back to top

Caché y placeholders

En la conferencia DrupalCon Barcelona, esta sección era mucho más larga y entraba en detalles sobre cómo interactúa cada subsistema entre sí. Aquí lo trataré en un artículo aparte ya que probablemente es un rodeo demasiado extenso.

No importa la estrategia - y olvidémonos por un momento de BigPipe - los placeholders tienen un gran impacto en el almacenamiento en caché.

En Drupal, tenemos dos módulos de caché de página activados por defecto:

  • Page Cache, que almacena en caché el renderizado completa de cada página, pero sólo para las peticiones sin sesion
  • Dynamic Page Cache, que puede almacenar en caché páginas para peticiones con y sin sesión.

Ten en cuenta que no hablamos de peticiones anónimas y logadas, sino de peticiones con y sin sesión. Esto se debe a que, en realidad, se puede ser usuario anónimo y tener una sesión (piensa en carritos de compra anónimos en un sitio de comercio electrónico).

Para simplificar las cosas, sólo nos centraremos en la caché dinámica, ya que es el principal módulo afectado por los placeholders.
 

¿Qué tiene de diferente la caché de página dinámica?

Empecemos con un poco de historia: hasta Drupal 8, no existía caché de página para las peticiones con sesión (normalmente, usuarios logados)

Esto significa que cada página era procesada y reconstruida en cada petición. Por supuesto, esto tenía un enorme impacto en el rendimiento.

¿Por qué? Porque almacenar en caché páginas completas para peticiones con sesión, como vimos antes, es difícil. Hay muchos contextos -usuario, sesión- y, por tanto, muchas variaciones.

La dynamic page cache evita este problema utilizando placeholders. Veamos cómo funciona.

Cómo afectan los placeholders al bubbling de los metadatos de la caché

Los metadatos de la caché «burbujean» hacia la superficie. Esto significa que, en el árbol que es nuestra página, los metadatos de caché fluyen hacia arriba hasta alcanzar la página completa. Los metadatos de caché de la página son la suma de los metadatos de caché de todas sus partes individuales.

En otras palabras, si una sola parte de nuestra página tiene un cache context de user, la página tiene un cache context de user. Por eso decimos que la caché de una página es tan débil como su parte más débil. Es como tener un plato lleno de fruta y verdura: si sólo una se estropea -sobre todo si es una manzana- todo el plato puede pudrirse rápidamente.

Pero...

¿Recuerdas cuando dijimos que...

«algo interesante ha sucedido bajo la superficie: los metadatos de #cache se han movido de la render array a los #attached adjuntos»

Esto significa que, cuando la página es cacheada por Dynamic Page Cache, los placeholders ya han eliminado esas partes infecciosas de nuestra página - y los metadatos de caché asociada a ellas.

Este es el truco de magia que Dynamic Page Cache se saca de la chistera: guardará el resultado de la página en el almacenamiento de caché, pero sin las partes altamente dinámicas. Los metadatos de caché peligrosos no figurarán en los metadatos de nuestra página porque no estarán presentes en absoluto y no podrán «burbujear».

Puedes hacer la prueba. Comprueba en la tabla cache_dynamic_page_cache la entrada de cualquier página y probablemente encontrarás algo como esto:

<div class="page__sidebar col-lg-3">
  <drupal-render-placeholder
    callback="Drupal\block\BlockViewBuilder::lazyBuilder"
    arguments="0=heli_timeblock&amp;1=full&amp;2"
    token="zwKpkDD6cjGP-7xbKPtQswFLKRq0zkJDjbaCAJ3X4gk"
  ></drupal-render-placeholder>
</div>

Puedes interpretar esto como si Drupal dijera: «¡Hey, hemos encontrado un bloque problemático, lo hemos eliminado por completo (incluyendo los metadatos de caché) y en su lugar hemos dejado un placeholder con la información mínima necesaria! Podemos almacenar la página en caché de forma segura y ya veremos cómo renderizar esta cosa más adelante».

No necesitas lazy loading para beneficiarte de los placeholders

Hemos estado hablando de cómo BigPipe y los placeholders están relacionados pero no son lo mismo. La caché dinámica es un buen ejemplo: incluso sin BigPipe, puedes beneficiarte de los placeholders.

Al eliminar las partes altamente dinámicas de los datos almacenados en caché:

  • El número de HITs de caché aumenta, ya que eliminamos las condiciones que crean una mala cacheabilidad.
  • El rendimiento de la base de datos mejora, ya que disminuimos la variación. Si tuviéramos 1.000 usuarios, necesitaríamos hasta 1.000 entradas de caché para cada página con información basada en el usuario. Ahora sólo necesitamos una.

Esto es una victoria, incluso con la SingleFlushStrategy básica. Los placeholders siguen sustituyéndose en la misma petición, lo que no es ideal, pero se añadirán sobre un HIT de caché, no como parte de un reprocesamiento completo de toda la página.

No obstante, el sistema está claramente orientado a ser utilizado con BigPipe. Hablemos de las técnicas de carga deferida, o «lazy loading».

 

Back to top

La estrategia de lazy loading BigPipe

Mucha gente -incluido yo mismo- tiende a pensar que BigPipe y los placeholders son en realidad dos caras de lo mismo, y que se necesita el segundo para obtener el primero (o al revés).

Esto puede deberse a que BigPipe es la principal placeholder strategy en Drupal.

Como dijimos antes, podemos mejorar perfectamente nuestra caché utilizando placeholders sin otra estrategia que la SingleFlushStrategy del core - pero los marcadores de posición destacan verdaderamente cuando se combinan con una técnica de lazy loading como BigPipe.

Renderizado del lado del cliente

Big Pipe funciona, en primer lugar, enviando los placeholders al navegador y aplazando su sustitución a una fase posterior.

BigPipeStrategy transformará los placeholders en código HTML real como éste:

<span
  data-big-pipe-placeholder-id="
    callback=Drupal%5Cblock%5CBlockViewBuilder%3A%3AlazyBuilder&amp;
    args%5B0%5D=heli_timeblock&amp;
    args%5B1%5D=full&amp;
    args%5B2%5D&amp;
    token=zwKpkDD6cjGP-7xbKPtQswFLKRq0zkJDjbaCAJ3X4gk"
></span>

Puedes comprobarlo. Abre tu Drupal e inspecciona el código fuente del HTML - no uses el inspector, vaya al código fuente - y encontrarás que muchas partes de nuestra página son en realidad enviadas como <span>s vacíos en la respuesta del servidor.

En realidad, el servidor transmite (stream) la respuesta en fragmentos (chunks). El navegador puede procesar y renderizar partes de la página mientras sigue recibiendo datos. Esto significa que el rendimiento percibido aumentará mucho aunque el tiempo total de renderizado sea el mismo.

Ten en cuenta que este es el único requisito para BigPipe: el servidor debe ser capaz de transmitir (stream) respuestas - cuidado: algunos proxies, como Varnish, pueden almacenar la respuesta en un buffer y ser incompatibles con BigPipe. Consulta la documentación para más información.

Los fragmentos enviados son los siguientes:

BigPipe chunks
  1. El primer fragmento incluye toda la página con los placeholders en forma de <span> y puede beneficiarse de un mejor almacenamiento en caché, con menos invalidaciones y variaciones. Además, comienza a renderizarse inmediatamente, lo que afecta positivamente tanto al usuario como a métricas de rendimiento como LCP. Por supuesto, los placeholders no son visibles en esta fase - son <span>s vacíos
  2. Se envían una serie de respuestas AJAX, acompañadas de un script con datos que mapean a esta respuesta y que se inyectan en la página. La librería javascript big_pipe está a la escucha de estos scripts, y utiliza su información para reemplazar los placeholders. La parte interesante de esta técnica es que no son peticiones/respuestas AJAX separadas, sino que ocurren dentro de la misma petición HTTP y no necesitan una inicialización adicional de Drupal.

    Nota: Me equivoqué en este punto en mi charla. Gracias a catch por señalármelo.

  3. La página se cierra y la petición finaliza.

Tenga en cuenta que esto significa que la petición necesita que cada sustitución termine de ejecutarse para finalizar. Es decir, si una única sustitución de placeholder se convierte en un cuello de botella, seguirá afectando al tiempo de carga de la página - pero todo lo que vino antes ya será visible. Se trata de una enorme ganancia para el rendimiento percibido, pero hay que tener en cuenta que podría ocultar problemas de rendimiento individuales.

En resumen, BigPipe es la guinda del pastel: difiere la carga de los placeholders al lado del cliente, transmite (stream) la respuesta para obtener el HIT de la caché de la página dinámica lo antes posible (primer fragmento), ejecuta cada sustitución de placeholder por separado y obtiene mejoras inmediatas en el rendimiento percibido. 

Big Pipe puede perjudicar a tu Core Web Vitals

BigPipe no siempre es bueno. Esto puede ser una sorpresa: un uso poco cuidadoso de BigPipe puede ser incluso perjudicial para nuestros Core Web Vitals.

Puede afectar especialmente al CLS (Cumulative Layout Shift) y LCP (Largest Contentful Paint).

¿Cómo? Vamos a verlo en la siguiente sección, donde profundizaremos en cómo tomar conscientemente el control de todas estas herramientas y técnicas que hemos visto y utilizarlas en nuestro beneficio.

 

Back to top

Cómo tomar el control y mejorar nuestros resultados

Previsualización de placeholders contra los problemas de CLS

Cuando se utiliza BigPipe, la el lazy loading de partes de la página puede causar reflows, repintados y movimientos indeseados, ya que el contenido aparece con el tiempo. Este es exactamente el tipo de comportamiento que puede ser penalizado en la puntuación CLS (Content Layout Shift).

 
 

Afortunadamente, desde febrero de 2023, tenemos una herramienta para mitigar esto y mejorar nuestra UX: #lazy_builder_preview (ver el changelog)

Usando estas vistas previas de la interfaz, podemos reemplazar fácilmente el espacio vacío (el <span>) de cada placeholder con una UI alternativa. Esta interfaz puede tomar la forma de una render array:

[
  'build' => [
    '#lazy_builder' => ['', []],
    '#lazy_builder_preview' => [
      '#type' => 'container',
      '#markup' => 'Waiting.... ... ... ',
    ],
  ],
]

O también puedes usar directamente una plantilla de twig, ya que hay una sugerencia de tema para cada placeholder de tu página bajo el namespace big-pipe-interface-preview:

<!-- THEME DEBUG -->
<!-- THEME HOOK: 'big_pipe_interface_preview' -->
<!-- FILE NAME SUGGESTIONS:
   ▪️ big-pipe-interface-preview--block--full.html.twig
   ▪️ big-pipe-interface-preview--block--heli-myrelatedcontent.html.twig
   ▪️ big-pipe-interface-preview--block.html.twig
   ✅ big-pipe-interface-preview.html.twig
-->
<!-- BEGIN OUTPUT from 'core/modules/big_pipe/templates/big-pipe-interface-preview.html.twig' -->
<!-- END OUTPUT from 'core/modules/big_pipe/templates/big-pipe-interface-preview.html.twig' -->
<span data-big-pipe-placeholder-id="callback=Drupa..."></span>

Sea cual sea nuestra preferencia, ahora somos libre de ofrecer una vista previa del contenido: un spinner, un bloque vacío, algún texto, un esqueleto visual de la forma del contenido que vendrá...

An example of a placeholder preview (after BigPipe loads content) and the actual content being rendered afterwards
A la izquierda, un placeholder preview a la espera de ser sustituido. A la derecha, el contenido real.

¡Genial!. Ahora, en la respuesta, el primer fragmento - el caché HIT, idealmente - incluirá algunas previsualizaciones para los placeholders.

Hablemos ahora del otro Core Web Vital que puede verse afectado (LCP) y también de otras cuestiones relacionadas con la caché.

 

Conoce tu caché: evita los errores más comunes

No pongas el LCP dentro de un placeholder

Una conclusión errónea de este artículo podría ser: «bueno, pongamos todo dentro de un placeholder». O, al menos, pongamos las partes más pesadas de la página.

Pero recordemos que la métrica LCP (Largest Contentful Paint) mide la rapidez con la que se carga la página mirando cuándo se renderiza el elemento más grande.

En otras palabras, queremos que el LCP ocurra lo antes posible. Si está dentro de un placeholder, puede afectar a tu puntuación LCP, ya que su renderización se retrasará.

Busca indicadores de que algo no va bien

Hay algunos indicios que deberían levantar sospechas.

  • X-Drupal-Dynamic-Cache: UNCACHEABLE debería ocurrir muy pocas veces o ninguna. Compruebe si hay tags, contexts o max-ages que han «burbujeado» y deberían haber sido placeholders.
  • max-age: 0 debería ser nuestro último recurso, incluso para los placeholders.
  • Una elevada proporción de X-Drupal-Dynamic-Cache: MISS significa que alguna etiqueta comúnmente invalidada o una max-age corta podrían ser placeholders. Revise y adapte sus auto_placeholder_conditions.

Adapta las auto placeholder conditions a tu sitio web

Como hemos dicho antes, las condiciones por defecto de auto_placeholder_conditions son valores por defecto que puede que no sean idóneas para nuestro sitio. Adáptalas a tus necesidades!

Estos son los valores por defecto de Drupal:

parameters:
  renderer.config:
    auto_placeholder_conditions:
      # Max-age at or below which caching is not considered worthwhile.
      max-age: 0
      # Cache contexts with a high cardinality.
      contexts: ["session", "user"]
      # Tags with a high invalidation frequency.
      tags: []

Algunos ejemplos que pueden servir para tomar ideas:

  • ¿Tu sitio tiene mucho contenido, con actualizaciones constantes que invalidan la caché con demasiada frecuencia? Quizá sea buena idea añadir etiquetas como node_list (y similares) a las auto placeholder conditions.
  • ¿Tienes pocos usuarios, principalmente administradores? Tal vez puedas eliminar el contexto user.
  • ¿Tu página web está traducida a muchos idiomas? Considera si te interesa añadir el contexto languages.
  • ¿Tienes algún contenido basado en tiempo, como un bloque que actualice su información cada 60 segundos? Eso invalidaría la caché con demasiada frecuencia. Podrías utilizar la opción max-age: 60.

Prueba siempre cualquier cambio que hagas, ya que el uso excesivo de placeholders también puede ser perjudicial.

Puede que no lo necesites todo

Volveré sobre esto con más detalle en un próximo artículo, pero de momento vamos a introducir una idea simple (pero que se suele olvidar).

Drupal incluye BigPipe, Page Cache y Dynamic Page Cache.

Sin embargo, no siempre los necesitaremos todos.

  • Si nuestra página sólo sirve peticiones sin sesión y no tiene problemas frecuentes de invalidación de caché, Page Cache puede ser más que suficiente y simplificará mucho las cosas.
  • Dynamic Page Cache puede servir peticiones con y sin sesión, pero delegará las peticiones sin sesión a Page Cache si ambos módulos están activos. Bajo ciertas condiciones, tener sólo Dynamic Page Cache puede ser beneficioso.
  • BigPipe puede ser innecesario o problemático si se utilizan algunos proxies inversos como Varnish.

 

Tomando (conscientemente) el control de los placeholders

Es bueno recordar que podemos optar por los placeholders independientemente de lo que Drupal opine.

La mayoría de los placeholders son automágicos y afectan a los bloques (y, en Drupal, casi todo es un bloque o acaba dentro de un bloque). No obstante, podemos decidir nosotros mismos cuándo es apropiado generar un placeholder.

Esto puede ser útil para situaciones excepcionales en las que sabemos que un contenido en particular será difícil de almacenar en caché, pero al mismo tiempo no podemos identificarlo mediante las condiciones de auto_placeholder_conditions.

Siempre podemos utilizar la opción #create_placeholder => TRUE para beneficiarnos de todas las ventajas de los placeholders.

Por ejemplo, digamos que tenemos un render array compuesta por dos partes altamente estáticas y una parte altamente dinámica. Podríamos hacer lo siguiente:

$build = [
  'static-1' => [
    '#markup' => 'A lot of static text, render arrays and whatever'
  ]
 
  'dynamic' => [
    '#lazy_builder' => [self::class . '::lazyBuilder', []]
    '#lazy_builder_preview' => [
      ...
    ],
    '#create_placeholder' => TRUE,
    '#cache' => [
      'keys' => ['random-content-sample'],
      'max-age' => 180,
      'tags' => ['node_list']
    ]
  ]
 
  'static-2' => [
    '#markup' => 'More static content, with no dependencies
      or rather static dependencies such as { site_name }'
  ]
]

¡Cuidado! Cuando se crean manualmente placeholders, a menudo es necesario añadir una caché key que muchas veces necesita ser nueva, ya que estamos almacenando en caché algo que Drupal desconoce. 

Esto asegura que el resultado será almacenado en la render_cache y podrá ser recuperado. Hablaré sobre la render caché en el próximo artículo.

 

Back to top

Algunos módulos interesantes

Como punto final, aquí tienes algunos módulos interesantes que pueden ayudarte a mejorar el rendimiento, depurar la caché y hacerte la vida más fácil:

  • AjaxBigPipe es, hasta donde yo sé, el único módulo que se atreve a ofrecer una PlaceholderStrategy diferente. Utiliza un IntersectionObserver para ser aún más perezoso, ya que esperará a que el bloque aparezca en pantalla para renderizar el placeholder. Sólo se aplica a bloques y es una estrategia opcional.
  • Sesionless BigPipe permite que las peticiones sin sesión se beneficien de BigPipe. Las peticiones sin sesión son normalmente servidas por Page Cache, y esto significa que el primer MISS tiene un rendimiento peor. Este módulo acelera ese MISS usando BigPipe para ello, y luego delega a Page Cache los HITs. Genial.
  • Cache Review ayuda a depurar los caché hits/misses/lazy builders/placeholders añadiendo un envoltorio con información alrededor de cada elemento. También proporciona algunas páginas de prueba para jugar con la caché.
  • BigPipe Demo proporciona bloques de ayuda para mostrar cómo se carga el contenido de BigPipe, activar/desactivar BigPipe (muy útil para realizar pruebas A/B de rendimiento) e iniciar una sesión como usuario anónimo.

 

Back to top

Para terminar

A modo de conclusión, resumamos los principales puntos clave que hemos tratado durante este (bastante largo) artículo.

  • El rendimiento (real y percibido) es clave no sólo para la UX, sino también para el SEO.
  • La caché es clave para el rendimiento, y la caché de tu página es tan buena (¡o mala!) como su eslabón más débil.
  • Las altas tasas de invalidación de la caché y el exceso de variaciones son algunos de los principales problemas a los que nos podemos enfrentar.
  • Drupal Core utiliza automáticamente placeholders para extraer contenido altamente dinámico o variable y cargarlo por separado, separando así las partes cacheables de la página de los fragmentos menos cacheables.
  • Podemos optar por los placeholders manualmente y modificar las condiciones por las que se crean los auto placeholders.
  • Los placeholders no necesitan un sistema de caché de página para funcionar, pero están pensados para trabajar conjuntamente con el módulo Dynamic Page Cache.
  • Además de Dynamic Page Cache, puede habilitar el módulo BigPipe para que realice la sustitución de los placeholders en el lado del cliente, lo que mejora mucho el rendimiento percibido.
  • Un mal uso de BigPipe puede ser perjudicial para Core Web Vitals como CLS y LCP. Los problemas de CLS pueden solucionarse utilizando placeholder previews..

Espero que hayas encontrado este artículo interesante. Mi idea es escribir un artículo de seguimiento profundizando en las interacciones que ocurren entre Page Cache y Dynamic Page Cache, algo que no siempre está 100% claro - al menos, no lo estaba para mí.

Si tienes alguna pregunta o duda, no dudes en ponerte en contacto conmigo en [email protected] o Bluesky..

¡Disfruta de tus placeholders!

Back to top