Éliminer le CLS en optimisant les images avec Vite
J'adore Vite, non seulement parce qu'il est rapide, mais aussi pour son incroyable système de plugins.
À la base, un plugin Vite vous permet d'éditer, à la volée, n'importe quel fichier importé, qu'il existe ou non, dans votre projet.
Une fois que vous avez compris cela, tout devient possible. Tout en tant que plugin Vite !
Depuis des années maintenant, je construis des sites web axés sur le contenu où celui-ci est écrit en Markdown. J'aime aussi utiliser des images pour illustrer mon contenu.
Cependant, les images sont délicates. Pour améliorer les performances, vous voulez les charger en différé (lazy loading), mais le lazy loading provoque inévitablement du Cumulative Layout Shift (CLS) si vous ne réservez pas d'espace. Pour réserver cet espace, le navigateur doit connaître à l'avance les dimensions de l'image. Mais vous ne voulez pas toujours les mêmes dimensions partout, donc pour chaque image vous avez besoin de ses dimensions afin que le navigateur puisse réserver la bonne quantité d'espace.
Au-delà de ça, les images peuvent rapidement devenir volumineuses, et les héberger dans votre dépôt n'est pas idéal. Vous pourriez donc vouloir les transférer vers un bucket dédié. Cela signifie qu'elles seront servies depuis un domaine différent, ce qui, combiné au chargement différé, même avec les bonnes dimensions, laissera un espace vide jusqu'à ce qu'elles soient entièrement chargées.
C'est un problème soluble, mais il nécessite beaucoup de travail manuel. Je déteste le travail manuel et répétitif qui peut être automatisé !
Note
Cet article est dédié au contenu HTML. Un autre article couvre la gestion du contenu Markdown en utilisant Markdown Exit.
Vite à la rescousse
La première question qui vient à l'esprit est : Comment savoir si un plugin Vite est le bon outil pour le travail ?
C'est une question importante. Je pourrais simplement vous donner la solution, mais vous ne seriez pas en mesure d'appliquer une approche similaire à vos propres problèmes. Malheureusement, la plupart des tutoriels manquent cette étape.
Analysons le problème en regardant ses entrées et ses sorties.
L'entrée est probablement un extrait HTML brut comme celui-ci :
<div>
<img src="/path/to/image.jpg" alt="An image">
</div>La sortie devrait être un extrait HTML optimisé comme celui-ci :
<div>
<img
src="https://cdn.example.com/path/to/image-optimized.jpg"
alt="An image"
width="600"
height="400"
loading="lazy"
style="background-image: url('data:image/svg+xml;base64,...'); background-size: cover;"
>
</div>Vite est construit autour d'un pipeline qui traite les fichiers au fur et à mesure qu'ils sont importés. Cela signifie que nous pouvons nous greffer sur le pipeline, détecter quand le code contient une image, et le transformer en conséquence.
Notre cas d'utilisation correspond parfaitement aux capacités de Vite. Les plugins Vite peuvent avoir des effets de bord sur les fichiers importés. Ils débloquent des cas d'utilisation vraiment puissants comme la compression automatique d'images, le redimensionnement, ...
Note
Cet article suppose que les images sont disponibles localement. La gestion des images distantes ne sera pas couverte ici, mais je donnerai quelques pistes à la fin de l'article.
Construire le plugin
Quel est le plan ? C'est bien d'avoir une idée de ce que nous voulons accomplir, mais comment y parvenir ?
Nous devons nous greffer sur le pipeline de Vite et rechercher les templates Vue. Ensuite, nous devons trouver toutes les balises <img>, extraire leurs attributs src, charger les images, obtenir leurs dimensions, générer le placeholder flouté, et enfin remplacer les balises <img> originales par des versions optimisées.
Note
J'utilise Vue pour tous mes projets, mais la même approche peut être appliquée à n'importe quel framework frontend supporté par Vite.
Cela semble plus facile que ça ne l'est, surtout quand on ne sait pas par où commencer et quel outil utiliser.
Pour gérer tout le traitement d'image, j'utilise unpic. C'est un ensemble fantastique de primitives pour gérer tout ce qui concerne les images. Pour manipuler le code et générer des source maps, j'utilise MagicString.
C'est tout ce dont nous avons besoin !
Maintenant, nous pouvons créer un plugin Vite qui fait exactement ce que nous voulons.
Note
Vous pouvez utiliser un projet Vite + Vue comme point de départ. Le code source du plugin final est disponible sur GitHub.
Dans le fichier vite.config.ts de notre projet, créons d'abord un plugin :
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
vue(),
(() => {
return {
name: 'unpic',
}
})()
],
})Au minimum, un plugin Vite doit avoir un nom. Notre plugin est une IIFE (Immediately Invoked Function Expression). Cela nous permet d'avoir un contexte pour stocker l'état sans dépendre de variables globales.
Ensuite, nous devons nous greffer sur la bonne partie du pipeline. Il existe beaucoup de hooks disponibles, mais en pratique, le plus utile est transform. Ce hook est appelé pour chaque fichier importé dans le projet. Il reçoit le code du fichier et son id (chemin). Nous voulons seulement traiter les fichiers Vue, donc nous pouvons filtrer par extension de fichier.
(() => {
return {
name: 'unpic',
async transform(code, id) {
if (!id.endsWith('.vue'))
return
// Some magic will happen here
return {
code,
map: null,
}
}
}
})()Mais nous pouvons le rendre encore plus performant. Appeler notre plugin sur chaque fichier est inutile, surtout avec Rolldown, où la communication entre Rust et JavaScript entraîne un petit surcoût. Pour éviter cela, nous pouvons utiliser des filtres pour ne traiter que les fichiers correspondant à certains critères. Dans notre cas, nous voulons seulement traiter les fichiers Vue.
(() => {
const imgTagRegex = /<img\s[^>]*src=["']([^"']+)["'][^>]*>/g
return {
name: 'unpic',
enforce: 'pre',
transform: {
filter: {
id: /\.vue$/,
code: imgTagRegex,
},
async handler(code) {},
}
}
})()Ce filter garantit que notre plugin ne s'exécute que sur les fichiers Vue contenant au moins une balise <img>. Tellement mieux !
Une dernière chose avant de pouvoir commencer la magie : nous devons nous assurer que le plugin s'exécute avant le plugin Vue. Vue transforme les fichiers Vue en fichiers JavaScript, et les fichiers Single-File Component (SFC) sont beaucoup plus simples à traiter. Pour ce faire, nous avons deux options :
- Placer notre plugin avant le plugin Vue dans le tableau
plugins. - Utiliser l'option
enforce: 'pre'dans la définition de notre plugin.
(() => {
return {
name: 'unpic',
enforce: 'pre',
transform: {
// ...
}
}
})()Maintenant, nous pouvons commencer à traiter les fichiers Vue pour améliorer les balises <img>.
Transformer le code
Dans le hook transform, voici ce qui doit être fait :
- Analyser le code pour trouver toutes les balises
<img>. - Pour chaque balise
<img>, extraire l'attributsrc. - Lire le fichier depuis le système de fichiers.
- Traiter l'image avec
unpicpour obtenir ses dimensions et un placeholder flouté. - Générer et remplacer la balise
<img>originale par la balise<img>optimisée avec tous les attributs nécessaires.
Avant de commencer, nous devons installer les dépendances :
pnpm install -D @unpic/pixels @unpic/placeholder blurhash magic-stringImplémentons cela étape par étape.
- Analyser le code pour trouver toutes les balises
<img>.
// eslint-disable-next-line prefer-const
let match = imgTagRegex.exec(code) // On réutilise la regex définie plus tôt- Pour chaque balise
<img>, extraire l'attributsrc.
do {
const srcValue = match[1]
match = imgTagRegex.exec(code)
} while (match !== null)- Lire le fichier depuis le système de fichiers.
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
const img = await readFile(join(resolvedConfig.publicDir, srcValue))- Traiter l'image avec
unpicpour obtenir ses dimensions et un placeholder flouté.
import { getPixels } from '@unpic/pixels'
import { blurhashToDataUri } from '@unpic/placeholder'
import { encode } from 'blurhash'
const data = await getPixels(img)
const blurhash = encode(Uint8ClampedArray.from(data.data), data.width, data.height, 4, 4)- Générer et remplacer la balise
<img>originale par la balise<img>optimisée avec tous les attributs nécessaires.
import { blurhashToDataUri } from '@unpic/placeholder'
const newImgTag = match[0].replace(
/<img(\s+)/,
`<img$1width="${data.width}" height="${data.height}" style="background-size: cover; background-image: url(${blurhashToDataUri(blurhash)});" loading="lazy" `
)Après avoir implémenté toutes ces étapes, notre plugin final ressemble à ceci :
(() => {
let resolvedConfig: ResolvedConfig
const imgTagRegex = /<img\s[^>]*src=["']([^"']+)["'][^>]*>/g
return {
name: 'unpic',
enforce: 'pre',
transform: {
filter: {
id: /\.vue$/,
code: /<img\s[^>]*src=["']([^"']+)["'][^>]*>/g,
},
async handler(code, id) {
const s = new MagicString(code)
let match = imgTagRegex.exec(code)
do {
const srcValue = match![1]
const img = await readFile(join(resolvedConfig.publicDir, srcValue))
const data = await getPixels(img)
const blurhash = encode(Uint8ClampedArray.from(data.data), data.width, data.height, 4, 4)
const imgTagStart = match!.index
const imgTagEnd = imgTagStart + match![0].length
const newImgTag = match![0].replace(
/<img(\s+)/,
`<img$1width="${data.width}" height="${data.height}" style="background-size: cover; background-image: url(${blurhashToDataUri(blurhash)});" loading="lazy" `
)
s.overwrite(imgTagStart, imgTagEnd, newImgTag)
match = imgTagRegex.exec(code)
} while (match !== null)
return {
code: s.toString(),
map: s.generateMap({ hires: true })
}
}
},
configResolved(config) {
resolvedConfig = config
}
}
})()Note
Le hook configResolved est utilisé pour obtenir la configuration Vite résolue, dont nous avons besoin pour accéder au publicDir où les images sont stockées.
Le code source complet est également disponible sur Barbapapazes/vite-vue-unpic.
Avec ce plugin, nous offrons maintenant une meilleure expérience utilisateur sans ajouter de travail manuel à notre processus de création de contenu.
Lorsque la page se charge, l'espace de l'image est déjà réservé grâce aux attributs width et height, et un placeholder flouté est affiché en utilisant le style background-image jusqu'à ce que l'image complète soit chargée. Cela élimine efficacement les problèmes de CLS liés aux images chargées en différé et améliore l'expérience utilisateur globale en fournissant un repère visuel pendant le chargement de l'image.
Observer le pipeline
Nous pouvons observer comment notre plugin interagit avec le pipeline Vite en utilisant vite-plugin-inspect.
pnpm add -D vite-plugin-inspectEnsuite, ajoutez-le au fichier vite.config.ts :
import { defineConfig } from 'vite'
import inspect from 'vite-plugin-inspect'
export default defineConfig({
plugins: [
inspect()
// ...
]
})Ensuite, lancez le serveur de développement :
pnpm devEt ouvrez le panneau d'inspection à http://localhost:5173/__inspect/.
Vous pouvez voir tous les fichiers traités par Vite. En cliquant sur App.vue, vous pouvez voir comment notre plugin a transformé le code étape par étape.

Aller plus loin
À travers cet exemple relativement simple mais utile, j'espère que vous avez appris la puissance des plugins Vite.
Une fois que vous avez compris l'état d'esprit et les bases, vous pouvez automatiser presque tout dans votre projet. Depuis que j'ai appris à créer des plugins Vite, je les utilise tout le temps pour résoudre des problèmes que je ne savais même pas solubles d'une autre manière.
Pour en savoir plus sur les plugins, je recommande vivement de lire le code source de plugins Vite populaires comme vite-plugin-vue ou unplugin-icons et ceux de VitePress. vite-plugin-inspect aide vraiment à comprendre comment les plugins interagissent entre eux et modifient le code.
De plus, c'est un exemple simple. Si vous avez beaucoup d'images, vous voudrez peut-être ajouter de la mise en cache et du traitement parallèle pour accélérer le temps de construction. Vous pourriez également gérer les images distantes en les téléchargeant avant de les traiter.
En parlant d'images distantes, au lieu de lire le système de fichiers, vous pourriez récupérer les images depuis votre bucket, les traiter, et préfixer l'attribut src avec l'URL de votre CDN. Cela évite d'avoir à commiter des images dans votre dépôt et garde l'URL du CDN à un seul endroit.
J'envisage de créer une petite conférence sur les plugins Vite. Ils sont sous-estimés et peu documentés. Faites-moi savoir si cela vous intéresserait !
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 !
Discussions
Ajouter un commentaire
Vous devez être connecté pour accéder à cette fonctionnalité.