Markdown Exit brise les règles avec le rendu asynchrone

Cet article est la suite de Éliminer le CLS en optimisant les images avec Vite et se concentre sur un cas d'utilisation plus concret : l'optimisation automatique des images dans les fichiers Markdown.

C'est très technique, et à moins que vous ne traitiez des fichiers Markdown, cela pourrait être un peu abstrait. Néanmoins, je vous encourage vivement à lire l'article précédent et celui-ci, car cela pourrait vous donner de bonnes idées.


Pourquoi écrire un article dédié à ce sujet ? Parce que, jusqu'à présent, ce n'était pas possible. Le principal parseur Markdown dans le monde JavaScript est markdown-it, et il est entièrement synchrone.

Cela pourrait ne pas être un problème, mais cela signifie que toutes les règles doivent être synchrones également. Vous ne pouvez pas avoir de règles asynchrones. Vous ne pouvez pas effectuer de fetch dans une règle, et vous ne pouvez rien paralléliser.

Un petit récapitulatif est nécessaire pour s'assurer que nous sommes tous sur la même longueur d'onde.

Écrire un Parseur Markdown

Je n'entrerai pas dans les détails, mais essentiellement, le parseur va diviser le texte Markdown en tokens puis appliquer des règles, qui sont essentiellement des fonctions, à ces tokens pour générer le HTML.

Par exemple, le texte suivant **bold text** sera tokenisé en : ['strong_open', 'text', 'strong_close'] et les règles correspondantes généreront le HTML : <strong>bold text</strong>. Le token strong_open sera traité par la règle strong_open, qui retourne <strong>, et ainsi de suite.

Explorez l'implémentation de la règle d'emphase dans markdown-exit

La règle dont nous parlons depuis le début transforme un token en sa représentation HTML correspondante. La partie intéressante est que vous pouvez attacher vos propres règles au parseur pour étendre ses fonctionnalités.

Imaginez que vous vouliez appliquer automatiquement l'attribut loading="lazy" à toutes les images de vos fichiers Markdown. Vous pourriez écrire une règle qui cible la règle image.

ts
md.use((md) => {
  const imageRule = md.renderer.rules.image!
  md.renderer.rules.image = (tokens, idx, options, env, self) => {
    const token = tokens[idx]

    token.attrSet('loading', 'lazy') 

    return imageRule(tokens, idx, options, env, self)
  }
})

Maintenant, le Markdown suivant ![alt text](image.jpg) sera rendu comme <img src="image.jpg" alt="alt text" loading="lazy" />. C'est un rêve : appliquer automatiquement des changements aux fichiers sans les toucher.

Limitations Synchrones

C'est un rêve jusqu'à ce que vous réalisiez que tout cela se passe de manière synchrone. Les règles sont appliquées une par une, sans aucun moyen de paralléliser le processus. Pour la règle de surlignage du code, cela peut réduire considérablement les performances car vous ne pouvez pas rendre tous les blocs de code en même temps, c'est un par un.

Note

Anthony Fu en était bien conscient et il a créé markdown-it-async pour rendre cela possible.

En plus de paralléliser le processus, les règles synchrones signifient que vous ne pouvez effectuer aucune opération asynchrone au sein d'une règle. Par exemple, vous ne pouvez pas récupérer de données depuis une API. Cela peut rapidement devenir limitant.

Mais, avec markdown-exit, un remplacement direct et une réécriture complète de markdown-it en utilisant TypeScript, c'est désormais possible. Les règles asynchrones sont maintenant une réalité.

Optimiser les Images à la Volée

Dans l'article précédent, nous avons exploité un plugin Vite pour appliquer automatiquement des optimisations aux images dans notre HTML. C'était un bon début utile.

Pour moi, ce n'était pas suffisant car je traite principalement des fichiers Markdown, et construire une règle personnalisée est un meilleur choix. C'est ce que nous allons voir maintenant.

Tout le code source est disponible sur GitHub : Barbapapazes/markdown-exit-unpic. Pour garder cet article simple, je ne montrerai pas le code non lié comme la configuration du projet.

Imaginons que nous ayons un fichier src/index.ts. Ce fichier est notre script principal, et le but est de rendre du Markdown brut en HTML tout en optimisant les images.

Nous voulons exécuter la commande suivante :

sh
node src/index.ts

avec ce contenu Markdown :

md
# Hello World

![This is a placeholder](https://images.unsplash.com/photo-1762707826575-d48c35b9b666)

et avoir la sortie suivante :

html
<h1>Hello World</h1>
<p><img src="https://images.unsplash.com/photo-1762707826575-d48c35b9b666?auto=format&amp;fit=crop&amp;q=80&amp;w=800" alt="This is a placeholder" loading="lazy" width="2567" height="2000" style="background-size: cover; background-image: url(data:image/bmp;base64,Qk32BAAAAAAAADYAAAAoAAAACAAAAAgAAAABABgAAAAAAMAAAAATCwAAEwsAAAAAAAAAAAAACQYICgcIDAoKEQ8PFBISFRISEQ4OCgYGCAYGDAoKExESGhgYHRsbHBoaFhMTDAgICAcGDgwMGBcXIB8fIyIiISAgGhgYDgoLCQcGDw0NGRgYIiEhJiUlJCIiHBoaDwsMCgcHDgwMFxYWHx4eIyIiISAgGhgYDgsLCQYGDAkJEhAQGBcXGxoaGxkZFRMTDAgICAUECQYGDAkJEA0OEhAQEhAQDwwMCQYFBwUEBwUECQYGCwcJDQoKDQsKCwgHCQUE);"></p>

Construire une Règle Asynchrone Personnalisée

La logique centrale de la règle ne différera pas de ce que nous avons construit dans le plugin Vite. Au lieu de lire le fichier depuis le système de fichiers local, nous le récupérerons depuis Unsplash pour simuler un bucket distant. Ensuite, nous récupérerons la largeur et la hauteur, et générerons le blurhash. À la fin, nous appliquerons également quelques paramètres de requête personnalisés pour éviter que le client ne charge l'image complète, comme vous pouvez le faire avec Cloudflare Images.

D'abord, nous devons créer une nouvelle instance de markdown-exit et rendre le Markdown. C'est vraiment simple.

ts
import { createMarkdownExit } from 'markdown-exit'

const md = createMarkdownExit()

async function render() {
  const html = await md.renderAsync(`# Hello World

![This is a placeholder](https://images.unsplash.com/photo-1762707826575-d48c35b9b666)`)

  console.log(html)
}

render()

Rien d'extraordinaire dans ce script. Cependant, la sortie est la suivante :

html
<h1>Hello World</h1>
<p><img src="https://images.unsplash.com/photo-1762707826575-d48c35b9b666" alt="This is a placeholder"></p>

Il manque quelques parties, mais c'est un bon début.

Pour changer le comportement de rendu de l'image, nous allons nous accrocher à la règle image, ajuster le token à nos besoins, et utiliser l'implémentation par défaut pour générer la sortie.

Pour créer une nouvelle règle, nous pouvons utiliser la fonction use. C'est comme créer un plugin Vue. Ensuite, liez la fonction que nous voulons à la règle image.

ts
import type { MarkdownExit } from 'markdown-exit'
import { createMarkdownExit } from 'markdown-exit'

const md = createMarkdownExit()

md.use((md: MarkdownExit) => {
  const imageRule = md.renderer.rules.image!
  md.renderer.rules.image = async (tokens, idx, options, env, self) => {
    return imageRule(tokens, idx, options, env, self)
  }
})

async function render() {
  // ...
}

render()

Notre fonction ne fait rien à part appeler la règle image originale, ce qui est déjà une bonne étape car nous ne voulons pas la réimplémenter nous-mêmes.

Ensuite, nous pouvons saisir le token actuel et essayer de lui ajouter un attribut.

La variable tokens contient tous les tokens disponibles. C'est un tableau de tokens. Pour accéder à l'actuel, nous devons utiliser la variable idx, qui est l'index actuel du token.

ts
const token = tokens[idx]

Maintenant, nous pouvons modifier le token. Par exemple, nous pouvons ajouter un attribut loading avec la valeur lazy.

ts
md.renderer.rules.image = async (tokens, idx, options, env, self) => {
  const token = tokens[idx]

  token.attrSet('loading', 'lazy')

  return imageRule(tokens, idx, options, env, self)
}

La sortie de notre script est maintenant la suivante :

html
<h1>Hello World</h1>
<p><img src="https://images.unsplash.com/photo-1762707826575-d48c35b9b666" alt="This is a placeholder" loading="lazy"></p>

Facile, non ?

Basé sur notre comportement attendu, nous devons ajouter la largeur, la hauteur, le blurhash, et quelques paramètres de requête à l'image. Pour ce faire, nous utiliserons la même logique que dans Éliminer le CLS en optimisant les images avec Vite, sauf que nous récupérerons l'image depuis Unsplash, comme vous pourriez le faire avec n'importe quel bucket distant.

ts
const img = await fetch(src).then(res => res.bytes())
const data = await getPixels(img)

const blurhash = encode(Uint8ClampedArray.from(data.data), data.width, data.height, 4, 4)

Maintenant, nous pouvons définir les attributs width, height et style sur le token.

ts
import { blurhashToDataUri } from '@unpic/placeholder'

token.attrSet('width', data.width.toString())
token.attrSet('height', data.height.toString())
token.attrSet('style', `background-size: cover; background-image: url(${blurhashToDataUri(blurhash)});`)

Enfin, nous pouvons ajuster l'attribut src pour ajouter quelques paramètres de requête.

ts
token.attrSet('src', `${src}?auto=format&fit=crop&q=80&w=800`)

Ces requêtes sont spécifiques à Unsplash, mais vous pourriez faire la même chose avec Cloudflare Images ou n'importe quel service d'optimisation d'image.

Une petite note sur cette approche : elle fonctionne bien. Le parallélisme améliore légèrement les performances car la plupart des calculs se produisent dans la fonction encode qui transforme l'image en blurhash de manière synchrone. Sur mon ordinateur, récupérer l'image prend environ 100ms, tandis que l'encoder prend environ 3 secondes.

Aller Plus Loin

L'implémentation actuelle est simple, et c'est intentionnel. Maintenant que vous savez comment optimiser vos images et créer des règles personnalisées, vous pouvez facilement adapter cette approche à vos besoins, et même en créer de nouvelles qui correspondent à vos cas d'utilisation spécifiques.

Pour gérer le goulot d'étranglement des performances, vous avez de nombreuses possibilités. Trois d'entre elles sont :

  1. Pré-traitement local et stockage local : Si vous stockez des images dans votre code source, vous pouvez créer un script qui parcourra toutes les images et générera un fichier JSON contenant la largeur, la hauteur et le blurhash. Ensuite, dans la règle, vous n'aurez qu'à lire ce fichier, et plus aucun calcul n'est nécessaire. Cela réduira également le temps de build.
  2. Pré-traitement local et stockage distant : Si vos images sont stockées à distance, vous pouvez créer un script similaire qui récupérera les images, les traitera et générera le fichier JSON. Ce fichier peut être poussé vers un bucket pour une utilisation ultérieure. Dans la règle, au lieu de lire un fichier, vous pouvez le récupérer depuis le bucket. Sans règles asynchrones, ce n'était pas possible !
  3. Pré-traitement distant et stockage distant : Si vos images sont stockées à distance, vous pouvez créer un workflow qui sera déclenché chaque fois qu'un nouveau fichier est ajouté. Le workflow génère le fichier JSON et le stocke dans le même bucket. Dans la règle, vous pouvez récupérer le fichier JSON depuis le bucket et l'utiliser directement. L'optimisation maximale. Avec les notifications R2 et les Queues, c'est simple à implémenter.

Vous pourriez aussi créer un pool de workers en utilisant les threads Node, mais c'est excessif pour ce cas d'utilisation.

Je suis sûr qu'il existe de nombreuses autres façons d'automatiser l'optimisation des images. Maintenant, c'est à vous de trouver la meilleure approche qui correspond à votre flux de travail.

J'espère que vous avez apprécié l'article ! Si vous implémentez quelque chose de similaire, tenez-moi au courant !

Pd

Merci de me lire ! Je m'appelle Estéban, et j'adore écrire sur le développement web et le parcours humain qui l'entoure.

Je code depuis plusieurs années maintenant, et j'apprends encore de nouvelles choses chaque jour. J'aime partager mes connaissances avec les autres, car j'aurais aimé avoir accès à des ressources aussi claires et complètes lorsque j'ai commencé à apprendre la programmation.

Si vous avez des questions ou souhaitez discuter, n'hésitez pas à commenter ci-dessous ou à me contacter sur Bluesky, X, et LinkedIn.

J'espère que vous avez apprécié cet article et appris quelque chose de nouveau. N'hésitez pas à le partager avec vos amis ou sur les réseaux sociaux, et laissez un commentaire ou une réaction ci-dessous, cela me ferait très plaisir ! Si vous souhaitez soutenir mon travail, vous pouvez me sponsoriser sur GitHub !

Réactions

Discussions

Ajouter un commentaire

Vous devez être connecté pour accéder à cette fonctionnalité.

Soutenez mon travail
Suivez-moi sur