Speculation Rules API: Selectores y carga dinámica
Tags: webdev, javascript, webperf
Published: 2024-07-07 08:50:53

En un [post anterior](https://dev.to/dezkareid/instant-navigation-con-speculation-rules-api-1dnc) escribí un poco sobre [Speculation Rules API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API) y como podíamos usarla para hacer un prerender de páginas para dar una sensación de navegación instantanea a los usuarios.

En este post vamos a hablar sobre como cargar dinámicamente un script  con nuestras Speculation Rules y como usar selectores para seleccionar las páginas a las que queremos hacer prerender. Para ello planteemos el siguiente escenario.

> Basados en la analítica de nuestro sitio web, determinamos que los usuarios tipo A, en un 80% de los casos hacen click en el link 1 y los usuarios tipo B, en un 89% de los casos hacen click en link 2.

Al analizar este caso vemos que tenemos un caso concreto donde podemos hacer prerender. Recuerden, el prerender es útil pero también es peligroso ya que al final quieras o no estas consumiento recursos.

Bueno para este caso necesitamos

* Nuestra web (página principal, pagina a la que apunte el link 1 y link 2 respectivamente)
* Un script para cargar nuestras reglas de manera dinámica

## Nuestra web

Primero nuestra web, algo simple para evitar mas carga cognitiva de la necesaria

Pagina principal

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Main page</title>
  </head>
  <body>
    <h1>Main page</h1>
    <a href="/link1.html" class="link1">Link 1</a>
    <br />
    <a href="/link2.html" class="link2">Link 2</a>
    <script async defer type="application/javascript">
      /* Todo nuestro código irá aquí */
    </script>
  </body>
</html>
```

Pagina link 1

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Link 1</title>
  </head>
  <body>
    <h1>Link 1</h1>
    <a href="/">Back to main page</a>
  </body>
</html>
```

Página link 2

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Link 2</title>
  </head>
  <body>
    <h1>Link 2</h1>
    <a href="/">Back to main page</a>
  </body>
</html>
```

## Script
Nuestro script tendrá 2 partes primero una función que creará nuestro speculation rules object, este recibirá al usuario para decidir el contenido de la regla y la segunda parte es la que carga el script

```javascript
function createSpeculationRulesObject(user) {
  const mapHrefByType = {
    A: '/link1.html',
    B: '/link2.html',
  };
  return {
    prerender: [
      {
        where: {
          href_matches: mapHrefByType[user.type],
        },
      },
    ],
  };
}

function injectSpeculationRules(speculationRulesObject) {
  const scriptType = 'speculationrules';
  if (HTMLScriptElement.supports && HTMLScriptElement.supports(scriptType)) {
    const specScript = document.createElement('script');
    specScript.type = scriptType;
    specScript.textContent = JSON.stringify(speculationRulesObject);
    document.body.append(specScript);
  }
}

const user = {
  type: 'A',
};
const speculationRulesObject = createSpeculationRulesObject(user);
injectSpeculationRules(speculationRulesObject);
```

Vamos analizar directamente el speculation rules object

```javascript
{
  prerender: [
    {
      where: {
        href_matches: mapHrefByType[user.type],
      },
    },
  ],
};
```
En ejemplos anteriores usabamos una lista de urls usando la propiedad `urls: ["link1", "link2", "etc"]`([doc](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/speculationrules#urls)). En este ejemplo podríamos agregar a la lista la página sin ningún problema. Pero queremos usar la propiedad `where`([doc](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/speculationrules#where)) ya que es más flexible, en este momento usamos la propiedad `href_matches`([doc](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/speculationrules#href_matches)) con la cual se hace el prerender de los elementos que apunten a las urls que hagan match pero también podríamos usar la propiedad `selector_matches`([doc](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/speculationrules#selector_matches)). Si ejecutas el ejemplo con un servidor y abres tus devtools en Application veras lo siguiente:


![El prerender no se realizó](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rrdgea331gi6yd0x2ze0.png)

¿Qué es lo que pasó?

Antes de explicar lo que pasó haz lo siguiente, con las devtools abiertas en Speculative loads, ve al enlace link1 y haz un click largo, veras lo siguiente

![El prerender se hace al hacer un click](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xt0o4bn8q0lluzxi7dmg.png)

Ahora si se viene lo chido, el speculation rules object hay una propiedad llamada `eagerness` ([info aquí](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/speculationrules#eagerness)). Esta indica cuando debe hacerse el prerender y toma un valor por default dependiendo de que propiedades declare el objeto.

Cuando el objeto define la propiedad `urls` el valor por defecto de eagerness será "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/speculationrules#immediate", lo que quiere decir que el prerender se hará lo más rápido posible.

Cuando el objeto define la propiedad `where` el valor por defecto de eagerness será "[conservative](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/speculationrules#conservative)", lo que quiere decir que el prerender se hará hasta que ocurra un evento como un click sobre el link.

> El objeto speculation rule no puede tener la propiedad `urls` y `where` a la vez

Para este ejemplo queremos que cargue inmediatamente así que definamos eagerness con el valor immediate y cambiemos el usuario que le pasamos con type B (solo para variar). Nota, esta API es muy sensible a typos, así que si te equivocas al definir alguna propiedad  dejará de funcionar. 

```javascript
function createSpeculationRulesObject(user) {
  const mapHrefByType = {
    A: '/link1.html',
    B: '/link2.html',
  };
  return {
    prerender: [
      {
        where: {
          href_matches: mapHrefByType[user.type],
        },
        eagerness: 'immediate',
      },
    ],
  };
}
```

Ahora si recargas la página veras como el prerender se realiza sin necesidad de una interacción.


![Prerender con immediate](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ytsfx91ymvgwqytwn9cy.png)

Para finalizar, vamos a cambiar un poco el script y en lugar de usar urls usaremos selectores.

```javascript
function createSpeculationRulesObject(user) {
  const mapSelectorsByType = {
    A: '.link1',
    B: '.link2',
  };
  return {
    prerender: [
      {
        where: {
          selector_matches: mapSelectorsByType[user.type],
        },
        eagerness: 'immediate',
      },
    ],
  };
}
```

![Prerender con selector](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8m7kygi0qlk96zp0hyj3.png)

Tenemos exactamente el mismo resultado. Como dato extra, si al momento de agregar la regla no hay algún elemento que haga match, no hay ningún problema, en cuanto exista el prerender se ejecutará.

## Conclusiones

Puedes usar Speculation Rules API para hacer prerender dinámico basada en la data de tu aplicación. En este caso usamos "immediate" pero en casos de la vida real podemos tomar en cuenta mas factores para hacer la inyección de reglas o incluso usar un eagerness con el valor `moderate` ([doc](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/speculationrules#moderate)) que inicia el render cuando el elemento esta en el viewport y se hace un hover. Si bien podemos cargar una páginas más rápido también hay que ser cuidadosos con el consumo de recursos