[{"data":1,"prerenderedAt":2665},["ShallowReactive",2],{"post-\u002Ffr\u002Fposts\u002Ferase-cls-by-automatically-optimizing-images-with-vite":3,"post-link-fr-external":2658},{"id":4,"title":5,"body":6,"contentId":2651,"date":2652,"description":2653,"extension":2654,"meta":2655,"navigation":42,"path":2656,"rawbody":2657,"robots":2658,"schemaOrg":2658,"seo":2659,"sitemap":2660,"stem":2663,"__hash__":2664},"posts\u002Ffr\u002Fposts\u002Ferase-cls-by-automatically-optimizing-images-with-vite.md","Éliminer le CLS en optimisant les images avec Vite",{"type":7,"value":8,"toc":2644},"minimark",[9,13,16,23,26,29,32,35,53,59,82,87,90,96,99,102,176,179,303,306,312,319,323,326,340,347,350,364,367,370,382,389,552,555,568,728,731,935,945,2640],[10,11,12],"p",{},"J'adore Vite, non seulement parce qu'il est rapide, mais aussi pour son incroyable système de plugins.",[10,14,15],{},"À 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.",[10,17,18,19],{},"Une fois que vous avez compris cela, tout devient possible. ",[20,21,22],"em",{},"Tout en tant que plugin Vite !",[24,25],"hr",{},[10,27,28],{},"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.",[10,30,31],{},"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.",[10,33,34],{},"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.",[36,37,38,39,38,49],"figure",{},"\n  ",[40,41,43,44,38],"video",{"autoPlay":42,"loop":42,"muted":42,"playsInline":42},true,"\n    ",[45,46],"source",{"src":47,"type":48},"https:\u002F\u002Fimages.soubiran.dev\u002Fposts\u002Ferase-cls-by-automatically-optimizing-images-with-vite\u002Fcls-issue.mp4","video\u002Fmp4",[50,51,52],"figcaption",{},"Exemple de problème de Cumulative Layout Shift (CLS) lors du chargement différé d'une image sans espace réservé.",[10,54,55,56],{},"C'est un problème soluble, mais il nécessite beaucoup de travail manuel. ",[20,57,58],{},"Je déteste le travail manuel et répétitif qui peut être automatisé !",[60,61,62],"blockquote",{},[10,63,64,68,69,74,75,81],{},[65,66,67],"span",{},"!NOTE","\nCet article est dédié au contenu HTML. ",[70,71,73],"a",{"href":72},".\u002Fmarkdown-exit-breaks-the-rules-with-async-rendering","Un autre article"," couvre la gestion du contenu Markdown en utilisant ",[70,76,80],{"href":77,"rel":78},"https:\u002F\u002Fmarkdown-exit.pages.dev\u002F",[79],"nofollow","Markdown Exit",".",[83,84,86],"h2",{"id":85},"vite-à-la-rescousse","Vite à la rescousse",[10,88,89],{},"La première question qui vient à l'esprit est : Comment savoir si un plugin Vite est le bon outil pour le travail ?",[10,91,92,93],{},"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. ",[20,94,95],{},"Malheureusement, la plupart des tutoriels manquent cette étape.",[10,97,98],{},"Analysons le problème en regardant ses entrées et ses sorties.",[10,100,101],{},"L'entrée est probablement un extrait HTML brut comme celui-ci :",[103,104,109],"pre",{"className":105,"code":106,"language":107,"meta":108,"style":108},"language-html shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003Cdiv>\n  \u003Cimg src=\"\u002Fpath\u002Fto\u002Fimage.jpg\" alt=\"An image\">\n\u003C\u002Fdiv>\n","html","",[110,111,112,127,166],"code",{"__ignoreMap":108},[65,113,116,120,124],{"class":114,"line":115},"line",1,[65,117,119],{"class":118},"sMK4o","\u003C",[65,121,123],{"class":122},"swJcz","div",[65,125,126],{"class":118},">\n",[65,128,130,133,136,140,143,146,150,152,155,157,159,162,164],{"class":114,"line":129},2,[65,131,132],{"class":118},"  \u003C",[65,134,135],{"class":122},"img",[65,137,139],{"class":138},"spNyl"," src",[65,141,142],{"class":118},"=",[65,144,145],{"class":118},"\"",[65,147,149],{"class":148},"sfazB","\u002Fpath\u002Fto\u002Fimage.jpg",[65,151,145],{"class":118},[65,153,154],{"class":138}," alt",[65,156,142],{"class":118},[65,158,145],{"class":118},[65,160,161],{"class":148},"An image",[65,163,145],{"class":118},[65,165,126],{"class":118},[65,167,169,172,174],{"class":114,"line":168},3,[65,170,171],{"class":118},"\u003C\u002F",[65,173,123],{"class":122},[65,175,126],{"class":118},[10,177,178],{},"La sortie devrait être un extrait HTML optimisé comme celui-ci :",[103,180,182],{"className":105,"code":181,"language":107,"meta":108,"style":108},"\u003Cdiv>\n  \u003Cimg\n    src=\"https:\u002F\u002Fcdn.example.com\u002Fpath\u002Fto\u002Fimage-optimized.jpg\"\n    alt=\"An image\"\n    width=\"600\"\n    height=\"400\"\n    loading=\"lazy\"\n    style=\"background-image: url('data:image\u002Fsvg+xml;base64,...'); background-size: cover;\"\n  >\n\u003C\u002Fdiv>\n",[110,183,184,192,199,214,228,243,258,273,288,294],{"__ignoreMap":108},[65,185,186,188,190],{"class":114,"line":115},[65,187,119],{"class":118},[65,189,123],{"class":122},[65,191,126],{"class":118},[65,193,194,196],{"class":114,"line":129},[65,195,132],{"class":118},[65,197,198],{"class":122},"img\n",[65,200,201,204,206,208,211],{"class":114,"line":168},[65,202,203],{"class":138},"    src",[65,205,142],{"class":118},[65,207,145],{"class":118},[65,209,210],{"class":148},"https:\u002F\u002Fcdn.example.com\u002Fpath\u002Fto\u002Fimage-optimized.jpg",[65,212,213],{"class":118},"\"\n",[65,215,217,220,222,224,226],{"class":114,"line":216},4,[65,218,219],{"class":138},"    alt",[65,221,142],{"class":118},[65,223,145],{"class":118},[65,225,161],{"class":148},[65,227,213],{"class":118},[65,229,231,234,236,238,241],{"class":114,"line":230},5,[65,232,233],{"class":138},"    width",[65,235,142],{"class":118},[65,237,145],{"class":118},[65,239,240],{"class":148},"600",[65,242,213],{"class":118},[65,244,246,249,251,253,256],{"class":114,"line":245},6,[65,247,248],{"class":138},"    height",[65,250,142],{"class":118},[65,252,145],{"class":118},[65,254,255],{"class":148},"400",[65,257,213],{"class":118},[65,259,261,264,266,268,271],{"class":114,"line":260},7,[65,262,263],{"class":138},"    loading",[65,265,142],{"class":118},[65,267,145],{"class":118},[65,269,270],{"class":148},"lazy",[65,272,213],{"class":118},[65,274,276,279,281,283,286],{"class":114,"line":275},8,[65,277,278],{"class":138},"    style",[65,280,142],{"class":118},[65,282,145],{"class":118},[65,284,285],{"class":148},"background-image: url('data:image\u002Fsvg+xml;base64,...'); background-size: cover;",[65,287,213],{"class":118},[65,289,291],{"class":114,"line":290},9,[65,292,293],{"class":118},"  >\n",[65,295,297,299,301],{"class":114,"line":296},10,[65,298,171],{"class":118},[65,300,123],{"class":122},[65,302,126],{"class":118},[10,304,305],{},"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.",[10,307,308,309],{},"Notre cas d'utilisation correspond parfaitement aux capacités de Vite. ",[20,310,311],{},"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, ...",[60,313,314],{},[10,315,316,318],{},[65,317,67],{},"\nCet 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.",[83,320,322],{"id":321},"construire-le-plugin","Construire le plugin",[10,324,325],{},"Quel est le plan ? C'est bien d'avoir une idée de ce que nous voulons accomplir, mais comment y parvenir ?",[10,327,328,329,332,333,336,337,339],{},"Nous devons nous greffer sur le pipeline de Vite et rechercher les templates Vue. Ensuite, nous devons trouver toutes les balises ",[110,330,331],{},"\u003Cimg>",", extraire leurs attributs ",[110,334,335],{},"src",", charger les images, obtenir leurs dimensions, générer le placeholder flouté, et enfin remplacer les balises ",[110,338,331],{}," originales par des versions optimisées.",[60,341,342],{},[10,343,344,346],{},[65,345,67],{},"\nJ'utilise Vue pour tous mes projets, mais la même approche peut être appliquée à n'importe quel framework frontend supporté par Vite.",[10,348,349],{},"Cela semble plus facile que ça ne l'est, surtout quand on ne sait pas par où commencer et quel outil utiliser.",[10,351,352,353,358,359,81],{},"Pour gérer tout le traitement d'image, j'utilise ",[70,354,357],{"href":355,"rel":356},"https:\u002F\u002Funpic.pics\u002F",[79],"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 ",[70,360,363],{"href":361,"rel":362},"https:\u002F\u002Fgithub.com\u002FRich-Harris\u002Fmagic-string",[79],"MagicString",[10,365,366],{},"C'est tout ce dont nous avons besoin !",[10,368,369],{},"Maintenant, nous pouvons créer un plugin Vite qui fait exactement ce que nous voulons.",[60,371,372],{},[10,373,374,376,377,81],{},[65,375,67],{},"\nVous pouvez utiliser un projet Vite + Vue comme point de départ. Le code source du plugin final est disponible sur ",[70,378,381],{"href":379,"rel":380},"https:\u002F\u002Fgithub.com\u002FBarbapapazes\u002Fvite-vue-unpic",[79],"GitHub",[10,383,384,385,388],{},"Dans le fichier ",[110,386,387],{},"vite.config.ts"," de notre projet, créons d'abord un plugin :",[103,390,394],{"className":391,"code":392,"filename":387,"language":393,"meta":108,"style":108},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import vue from '@vitejs\u002Fplugin-vue'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: [\n    vue(),\n    (() => {\n      return {\n        name: 'unpic',\n      }\n    })()\n  ],\n})\n","ts",[110,395,396,418,441,446,463,474,485,498,505,521,526,535,543],{"__ignoreMap":108},[65,397,398,402,406,409,412,415],{"class":114,"line":115},[65,399,401],{"class":400},"s7zQu","import",[65,403,405],{"class":404},"sTEyZ"," vue ",[65,407,408],{"class":400},"from",[65,410,411],{"class":118}," '",[65,413,414],{"class":148},"@vitejs\u002Fplugin-vue",[65,416,417],{"class":118},"'\n",[65,419,420,422,425,428,431,434,436,439],{"class":114,"line":129},[65,421,401],{"class":400},[65,423,424],{"class":118}," {",[65,426,427],{"class":404}," defineConfig",[65,429,430],{"class":118}," }",[65,432,433],{"class":400}," from",[65,435,411],{"class":118},[65,437,438],{"class":148},"vite",[65,440,417],{"class":118},[65,442,443],{"class":114,"line":168},[65,444,445],{"emptyLinePlaceholder":42},"\n",[65,447,448,451,454,457,460],{"class":114,"line":216},[65,449,450],{"class":400},"export",[65,452,453],{"class":400}," default",[65,455,427],{"class":456},"s2Zo4",[65,458,459],{"class":404},"(",[65,461,462],{"class":118},"{\n",[65,464,465,468,471],{"class":114,"line":230},[65,466,467],{"class":122},"  plugins",[65,469,470],{"class":118},":",[65,472,473],{"class":404}," [\n",[65,475,476,479,482],{"class":114,"line":245},[65,477,478],{"class":456},"    vue",[65,480,481],{"class":404},"()",[65,483,484],{"class":118},",\n",[65,486,487,490,492,495],{"class":114,"line":260},[65,488,489],{"class":404},"    (",[65,491,481],{"class":118},[65,493,494],{"class":138}," =>",[65,496,497],{"class":118}," {\n",[65,499,500,503],{"class":114,"line":275},[65,501,502],{"class":400},"      return",[65,504,497],{"class":118},[65,506,507,510,512,514,516,519],{"class":114,"line":290},[65,508,509],{"class":122},"        name",[65,511,470],{"class":118},[65,513,411],{"class":118},[65,515,357],{"class":148},[65,517,518],{"class":118},"'",[65,520,484],{"class":118},[65,522,523],{"class":114,"line":296},[65,524,525],{"class":118},"      }\n",[65,527,529,532],{"class":114,"line":528},11,[65,530,531],{"class":118},"    }",[65,533,534],{"class":404},")()\n",[65,536,538,541],{"class":114,"line":537},12,[65,539,540],{"class":404},"  ]",[65,542,484],{"class":118},[65,544,546,549],{"class":114,"line":545},13,[65,547,548],{"class":118},"}",[65,550,551],{"class":404},")\n",[10,553,554],{},"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.",[10,556,557,558,563,564,567],{},"Ensuite, nous devons nous greffer sur la bonne partie du pipeline. Il existe ",[70,559,562],{"href":560,"rel":561},"https:\u002F\u002Frollupjs.org\u002Fplugin-development\u002F#build-hooks",[79],"beaucoup de hooks"," disponibles, mais en pratique, le plus utile est ",[110,565,566],{},"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.",[103,569,572],{"className":570,"code":571,"filename":387,"language":393,"meta":108,"style":108},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight has-highlighted","(() => {\n  return {\n    name: 'unpic',\n    async transform(code, id) {\n      if (!id.endsWith('.vue')) \u002F\u002F [!code highlight]\n        return\n\n      \u002F\u002F Some magic will happen here\n\n      return {\n        code,\n        map: null,\n      }\n    }\n  }\n})()\n",[110,573,574,584,591,606,630,663,668,672,678,682,688,695,705,709,715,721],{"__ignoreMap":108},[65,575,576,578,580,582],{"class":114,"line":115},[65,577,459],{"class":404},[65,579,481],{"class":118},[65,581,494],{"class":138},[65,583,497],{"class":118},[65,585,586,589],{"class":114,"line":129},[65,587,588],{"class":400},"  return",[65,590,497],{"class":118},[65,592,593,596,598,600,602,604],{"class":114,"line":168},[65,594,595],{"class":122},"    name",[65,597,470],{"class":118},[65,599,411],{"class":118},[65,601,357],{"class":148},[65,603,518],{"class":118},[65,605,484],{"class":118},[65,607,608,611,614,616,619,622,625,628],{"class":114,"line":216},[65,609,610],{"class":138},"    async",[65,612,613],{"class":122}," transform",[65,615,459],{"class":118},[65,617,110],{"class":618},"sHdIc",[65,620,621],{"class":118},",",[65,623,624],{"class":618}," id",[65,626,627],{"class":118},")",[65,629,497],{"class":118},[65,631,634,637,640,643,646,648,651,653,655,658,660],{"class":632,"line":230},[114,633],"highlighted",[65,635,636],{"class":400},"      if",[65,638,639],{"class":122}," (",[65,641,642],{"class":118},"!",[65,644,645],{"class":404},"id",[65,647,81],{"class":118},[65,649,650],{"class":456},"endsWith",[65,652,459],{"class":122},[65,654,518],{"class":118},[65,656,657],{"class":148},".vue",[65,659,518],{"class":118},[65,661,662],{"class":122},")) ",[65,664,665],{"class":114,"line":245},[65,666,667],{"class":400},"        return\n",[65,669,670],{"class":114,"line":260},[65,671,445],{"emptyLinePlaceholder":42},[65,673,674],{"class":114,"line":275},[65,675,677],{"class":676},"sHwdD","      \u002F\u002F Some magic will happen here\n",[65,679,680],{"class":114,"line":290},[65,681,445],{"emptyLinePlaceholder":42},[65,683,684,686],{"class":114,"line":296},[65,685,502],{"class":400},[65,687,497],{"class":118},[65,689,690,693],{"class":114,"line":528},[65,691,692],{"class":404},"        code",[65,694,484],{"class":118},[65,696,697,700,702],{"class":114,"line":537},[65,698,699],{"class":122},"        map",[65,701,470],{"class":118},[65,703,704],{"class":118}," null,\n",[65,706,707],{"class":114,"line":545},[65,708,525],{"class":118},[65,710,712],{"class":114,"line":711},14,[65,713,714],{"class":118},"    }\n",[65,716,718],{"class":114,"line":717},15,[65,719,720],{"class":118},"  }\n",[65,722,724,726],{"class":114,"line":723},16,[65,725,548],{"class":118},[65,727,534],{"class":404},[10,729,730],{},"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.",[103,732,734],{"className":570,"code":733,"filename":387,"language":393,"meta":108,"style":108},"(() => {\n  const imgTagRegex = \u002F\u003Cimg\\s[^>]*src=[\"']([^\"']+)[\"'][^>]*>\u002Fg \u002F\u002F [!code highlight]\n\n  return {\n    name: 'unpic',\n    enforce: 'pre',\n    transform: {\n      filter: {\n        id: \u002F\\.vue$\u002F, \u002F\u002F [!code highlight]\n        code: imgTagRegex, \u002F\u002F [!code highlight]\n      },\n      async handler(code) {},\n    }\n  }\n})()\n",[110,735,736,746,808,812,818,832,847,856,865,888,899,904,921,925,929],{"__ignoreMap":108},[65,737,738,740,742,744],{"class":114,"line":115},[65,739,459],{"class":404},[65,741,481],{"class":118},[65,743,494],{"class":138},[65,745,497],{"class":118},[65,747,749,752,755,758,761,764,767,770,773,776,779,782,785,787,790,792,795,797,799,801,804],{"class":748,"line":129},[114,633],[65,750,751],{"class":138},"  const",[65,753,754],{"class":404}," imgTagRegex",[65,756,757],{"class":118}," =",[65,759,760],{"class":118}," \u002F",[65,762,763],{"class":148},"\u003Cimg\\s",[65,765,766],{"class":118},"[^",[65,768,769],{"class":148},">",[65,771,772],{"class":118},"]*",[65,774,775],{"class":148},"src=",[65,777,778],{"class":118},"[",[65,780,781],{"class":148},"\"'",[65,783,784],{"class":118},"]([^",[65,786,781],{"class":148},[65,788,789],{"class":118},"]+)[",[65,791,781],{"class":148},[65,793,794],{"class":118},"][^",[65,796,769],{"class":148},[65,798,772],{"class":118},[65,800,769],{"class":148},[65,802,803],{"class":118},"\u002F",[65,805,807],{"class":806},"sbssI","g",[65,809,810],{"class":114,"line":168},[65,811,445],{"emptyLinePlaceholder":42},[65,813,814,816],{"class":114,"line":216},[65,815,588],{"class":400},[65,817,497],{"class":118},[65,819,820,822,824,826,828,830],{"class":114,"line":230},[65,821,595],{"class":122},[65,823,470],{"class":118},[65,825,411],{"class":118},[65,827,357],{"class":148},[65,829,518],{"class":118},[65,831,484],{"class":118},[65,833,834,837,839,841,843,845],{"class":114,"line":245},[65,835,836],{"class":122},"    enforce",[65,838,470],{"class":118},[65,840,411],{"class":118},[65,842,103],{"class":148},[65,844,518],{"class":118},[65,846,484],{"class":118},[65,848,849,852,854],{"class":114,"line":260},[65,850,851],{"class":122},"    transform",[65,853,470],{"class":118},[65,855,497],{"class":118},[65,857,858,861,863],{"class":114,"line":275},[65,859,860],{"class":122},"      filter",[65,862,470],{"class":118},[65,864,497],{"class":118},[65,866,868,871,873,875,878,881,884,886],{"class":867,"line":290},[114,633],[65,869,870],{"class":122},"        id",[65,872,470],{"class":118},[65,874,760],{"class":118},[65,876,877],{"class":404},"\\.",[65,879,880],{"class":148},"vue",[65,882,883],{"class":400},"$",[65,885,803],{"class":118},[65,887,621],{"class":118},[65,889,891,893,895,897],{"class":890,"line":296},[114,633],[65,892,692],{"class":122},[65,894,470],{"class":118},[65,896,754],{"class":404},[65,898,621],{"class":118},[65,900,901],{"class":114,"line":528},[65,902,903],{"class":118},"      },\n",[65,905,906,909,912,914,916,918],{"class":114,"line":537},[65,907,908],{"class":138},"      async",[65,910,911],{"class":122}," handler",[65,913,459],{"class":118},[65,915,110],{"class":618},[65,917,627],{"class":118},[65,919,920],{"class":118}," {},\n",[65,922,923],{"class":114,"line":545},[65,924,714],{"class":118},[65,926,927],{"class":114,"line":711},[65,928,720],{"class":118},[65,930,931,933],{"class":114,"line":717},[65,932,548],{"class":118},[65,934,534],{"class":404},[10,936,937,938,941,942,944],{},"Ce ",[110,939,940],{},"filter"," garantit que notre plugin ne s'exécute que sur les fichiers Vue contenant au moins une balise ",[110,943,331],{},". Tellement mieux !",[946,947,950,953,970,1047,1052,1056,1062,1095,1098,1129,1132,1138,1171,1179,1252,1256,1338,1344,1485,1493,1620,1623,2384,2398,2405,2408,2418,2431,2435,2443,2462,2468,2558,2561,2573,2579,2586,2592,2596,2599,2602,2626,2629,2635,2637],"callout-read-more",{"text":948,"href":949},"Filtres de hook de plugin","https:\u002F\u002Frolldown.rs\u002Fapis\u002Fplugin-hook-filters",[10,951,952],{},"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 :",[954,955,956,963],"ol",{},[957,958,959,960,81],"li",{},"Placer notre plugin avant le plugin Vue dans le tableau ",[110,961,962],{},"plugins",[957,964,965,966,969],{},"Utiliser l'option ",[110,967,968],{},"enforce: 'pre'"," dans la définition de notre plugin.",[103,971,973],{"className":570,"code":972,"filename":387,"language":393,"meta":108,"style":108},"(() => {\n  return {\n    name: 'unpic',\n    enforce: 'pre', \u002F\u002F [!code highlight]\n    transform: {\n      \u002F\u002F ...\n    }\n  }\n})()\n",[110,974,975,985,991,1005,1020,1028,1033,1037,1041],{"__ignoreMap":108},[65,976,977,979,981,983],{"class":114,"line":115},[65,978,459],{"class":404},[65,980,481],{"class":118},[65,982,494],{"class":138},[65,984,497],{"class":118},[65,986,987,989],{"class":114,"line":129},[65,988,588],{"class":400},[65,990,497],{"class":118},[65,992,993,995,997,999,1001,1003],{"class":114,"line":168},[65,994,595],{"class":122},[65,996,470],{"class":118},[65,998,411],{"class":118},[65,1000,357],{"class":148},[65,1002,518],{"class":118},[65,1004,484],{"class":118},[65,1006,1008,1010,1012,1014,1016,1018],{"class":1007,"line":216},[114,633],[65,1009,836],{"class":122},[65,1011,470],{"class":118},[65,1013,411],{"class":118},[65,1015,103],{"class":148},[65,1017,518],{"class":118},[65,1019,621],{"class":118},[65,1021,1022,1024,1026],{"class":114,"line":230},[65,1023,851],{"class":122},[65,1025,470],{"class":118},[65,1027,497],{"class":118},[65,1029,1030],{"class":114,"line":245},[65,1031,1032],{"class":676},"      \u002F\u002F ...\n",[65,1034,1035],{"class":114,"line":260},[65,1036,714],{"class":118},[65,1038,1039],{"class":114,"line":275},[65,1040,720],{"class":118},[65,1042,1043,1045],{"class":114,"line":290},[65,1044,548],{"class":118},[65,1046,534],{"class":404},[10,1048,1049,1050,81],{},"Maintenant, nous pouvons commencer à traiter les fichiers Vue pour améliorer les balises ",[110,1051,331],{},[83,1053,1055],{"id":1054},"transformer-le-code","Transformer le code",[10,1057,1058,1059,1061],{},"Dans le hook ",[110,1060,566],{},", voici ce qui doit être fait :",[954,1063,1064,1069,1077,1080,1086],{},[957,1065,1066,1067,81],{},"Analyser le code pour trouver toutes les balises ",[110,1068,331],{},[957,1070,1071,1072,1074,1075,81],{},"Pour chaque balise ",[110,1073,331],{},", extraire l'attribut ",[110,1076,335],{},[957,1078,1079],{},"Lire le fichier depuis le système de fichiers.",[957,1081,1082,1083,1085],{},"Traiter l'image avec ",[110,1084,357],{}," pour obtenir ses dimensions et un placeholder flouté.",[957,1087,1088,1089,1091,1092,1094],{},"Générer et remplacer la balise ",[110,1090,331],{}," originale par la balise ",[110,1093,331],{}," optimisée avec tous les attributs nécessaires.",[10,1096,1097],{},"Avant de commencer, nous devons installer les dépendances :",[103,1099,1103],{"className":1100,"code":1101,"language":1102,"meta":108,"style":108},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","pnpm install -D @unpic\u002Fpixels @unpic\u002Fplaceholder blurhash magic-string\n","bash",[110,1104,1105],{"__ignoreMap":108},[65,1106,1107,1111,1114,1117,1120,1123,1126],{"class":114,"line":115},[65,1108,1110],{"class":1109},"sBMFI","pnpm",[65,1112,1113],{"class":148}," install",[65,1115,1116],{"class":148}," -D",[65,1118,1119],{"class":148}," @unpic\u002Fpixels",[65,1121,1122],{"class":148}," @unpic\u002Fplaceholder",[65,1124,1125],{"class":148}," blurhash",[65,1127,1128],{"class":148}," magic-string\n",[10,1130,1131],{},"Implémentons cela étape par étape.",[954,1133,1134],{},[957,1135,1066,1136,81],{},[110,1137,331],{},[103,1139,1141],{"className":391,"code":1140,"filename":387,"language":393,"meta":108,"style":108},"\u002F\u002F eslint-disable-next-line prefer-const\nlet match = imgTagRegex.exec(code) \u002F\u002F On réutilise la regex définie plus tôt\n",[110,1142,1143,1148],{"__ignoreMap":108},[65,1144,1145],{"class":114,"line":115},[65,1146,1147],{"class":676},"\u002F\u002F eslint-disable-next-line prefer-const\n",[65,1149,1150,1153,1156,1158,1160,1162,1165,1168],{"class":114,"line":129},[65,1151,1152],{"class":138},"let",[65,1154,1155],{"class":404}," match ",[65,1157,142],{"class":118},[65,1159,754],{"class":404},[65,1161,81],{"class":118},[65,1163,1164],{"class":456},"exec",[65,1166,1167],{"class":404},"(code) ",[65,1169,1170],{"class":676},"\u002F\u002F On réutilise la regex définie plus tôt\n",[954,1172,1173],{"start":129},[957,1174,1071,1175,1074,1177,81],{},[110,1176,331],{},[110,1178,335],{},[103,1180,1182],{"className":391,"code":1181,"filename":387,"language":393,"meta":108,"style":108},"do {\n  const srcValue = match[1]\n\n  match = imgTagRegex.exec(code)\n} while (match !== null)\n",[110,1183,1184,1191,1211,1215,1234],{"__ignoreMap":108},[65,1185,1186,1189],{"class":114,"line":115},[65,1187,1188],{"class":400},"do",[65,1190,497],{"class":118},[65,1192,1193,1195,1198,1200,1203,1205,1208],{"class":114,"line":129},[65,1194,751],{"class":138},[65,1196,1197],{"class":404}," srcValue",[65,1199,757],{"class":118},[65,1201,1202],{"class":404}," match",[65,1204,778],{"class":122},[65,1206,1207],{"class":806},"1",[65,1209,1210],{"class":122},"]\n",[65,1212,1213],{"class":114,"line":168},[65,1214,445],{"emptyLinePlaceholder":42},[65,1216,1217,1220,1222,1224,1226,1228,1230,1232],{"class":114,"line":216},[65,1218,1219],{"class":404},"  match",[65,1221,757],{"class":118},[65,1223,754],{"class":404},[65,1225,81],{"class":118},[65,1227,1164],{"class":456},[65,1229,459],{"class":122},[65,1231,110],{"class":404},[65,1233,551],{"class":122},[65,1235,1236,1238,1241,1244,1247,1250],{"class":114,"line":230},[65,1237,548],{"class":118},[65,1239,1240],{"class":400}," while",[65,1242,1243],{"class":404}," (match ",[65,1245,1246],{"class":118},"!==",[65,1248,1249],{"class":118}," null",[65,1251,551],{"class":404},[954,1253,1254],{"start":168},[957,1255,1079],{},[103,1257,1259],{"className":391,"code":1258,"filename":387,"language":393,"meta":108,"style":108},"import { readFile } from 'node:fs\u002Fpromises'\nimport { join } from 'node:path'\n\nconst img = await readFile(join(resolvedConfig.publicDir, srcValue))\n",[110,1260,1261,1281,1301,1305],{"__ignoreMap":108},[65,1262,1263,1265,1267,1270,1272,1274,1276,1279],{"class":114,"line":115},[65,1264,401],{"class":400},[65,1266,424],{"class":118},[65,1268,1269],{"class":404}," readFile",[65,1271,430],{"class":118},[65,1273,433],{"class":400},[65,1275,411],{"class":118},[65,1277,1278],{"class":148},"node:fs\u002Fpromises",[65,1280,417],{"class":118},[65,1282,1283,1285,1287,1290,1292,1294,1296,1299],{"class":114,"line":129},[65,1284,401],{"class":400},[65,1286,424],{"class":118},[65,1288,1289],{"class":404}," join",[65,1291,430],{"class":118},[65,1293,433],{"class":400},[65,1295,411],{"class":118},[65,1297,1298],{"class":148},"node:path",[65,1300,417],{"class":118},[65,1302,1303],{"class":114,"line":168},[65,1304,445],{"emptyLinePlaceholder":42},[65,1306,1307,1310,1313,1315,1318,1320,1322,1325,1328,1330,1333,1335],{"class":114,"line":216},[65,1308,1309],{"class":138},"const",[65,1311,1312],{"class":404}," img ",[65,1314,142],{"class":118},[65,1316,1317],{"class":400}," await",[65,1319,1269],{"class":456},[65,1321,459],{"class":404},[65,1323,1324],{"class":456},"join",[65,1326,1327],{"class":404},"(resolvedConfig",[65,1329,81],{"class":118},[65,1331,1332],{"class":404},"publicDir",[65,1334,621],{"class":118},[65,1336,1337],{"class":404}," srcValue))\n",[954,1339,1340],{"start":216},[957,1341,1082,1342,1085],{},[110,1343,357],{},[103,1345,1347],{"className":391,"code":1346,"filename":387,"language":393,"meta":108,"style":108},"import { getPixels } from '@unpic\u002Fpixels'\nimport { blurhashToDataUri } from '@unpic\u002Fplaceholder'\nimport { encode } from 'blurhash'\n\nconst data = await getPixels(img)\nconst blurhash = encode(Uint8ClampedArray.from(data.data), data.width, data.height, 4, 4)\n",[110,1348,1349,1369,1389,1409,1413,1429],{"__ignoreMap":108},[65,1350,1351,1353,1355,1358,1360,1362,1364,1367],{"class":114,"line":115},[65,1352,401],{"class":400},[65,1354,424],{"class":118},[65,1356,1357],{"class":404}," getPixels",[65,1359,430],{"class":118},[65,1361,433],{"class":400},[65,1363,411],{"class":118},[65,1365,1366],{"class":148},"@unpic\u002Fpixels",[65,1368,417],{"class":118},[65,1370,1371,1373,1375,1378,1380,1382,1384,1387],{"class":114,"line":129},[65,1372,401],{"class":400},[65,1374,424],{"class":118},[65,1376,1377],{"class":404}," blurhashToDataUri",[65,1379,430],{"class":118},[65,1381,433],{"class":400},[65,1383,411],{"class":118},[65,1385,1386],{"class":148},"@unpic\u002Fplaceholder",[65,1388,417],{"class":118},[65,1390,1391,1393,1395,1398,1400,1402,1404,1407],{"class":114,"line":168},[65,1392,401],{"class":400},[65,1394,424],{"class":118},[65,1396,1397],{"class":404}," encode",[65,1399,430],{"class":118},[65,1401,433],{"class":400},[65,1403,411],{"class":118},[65,1405,1406],{"class":148},"blurhash",[65,1408,417],{"class":118},[65,1410,1411],{"class":114,"line":216},[65,1412,445],{"emptyLinePlaceholder":42},[65,1414,1415,1417,1420,1422,1424,1426],{"class":114,"line":230},[65,1416,1309],{"class":138},[65,1418,1419],{"class":404}," data ",[65,1421,142],{"class":118},[65,1423,1317],{"class":400},[65,1425,1357],{"class":456},[65,1427,1428],{"class":404},"(img)\n",[65,1430,1431,1433,1436,1438,1440,1443,1445,1447,1450,1452,1455,1457,1460,1462,1465,1467,1469,1471,1474,1476,1479,1481,1483],{"class":114,"line":245},[65,1432,1309],{"class":138},[65,1434,1435],{"class":404}," blurhash ",[65,1437,142],{"class":118},[65,1439,1397],{"class":456},[65,1441,1442],{"class":404},"(Uint8ClampedArray",[65,1444,81],{"class":118},[65,1446,408],{"class":456},[65,1448,1449],{"class":404},"(data",[65,1451,81],{"class":118},[65,1453,1454],{"class":404},"data)",[65,1456,621],{"class":118},[65,1458,1459],{"class":404}," data",[65,1461,81],{"class":118},[65,1463,1464],{"class":404},"width",[65,1466,621],{"class":118},[65,1468,1459],{"class":404},[65,1470,81],{"class":118},[65,1472,1473],{"class":404},"height",[65,1475,621],{"class":118},[65,1477,1478],{"class":806}," 4",[65,1480,621],{"class":118},[65,1482,1478],{"class":806},[65,1484,551],{"class":404},[954,1486,1487],{"start":230},[957,1488,1088,1489,1091,1491,1094],{},[110,1490,331],{},[110,1492,331],{},[103,1494,1496],{"className":391,"code":1495,"filename":387,"language":393,"meta":108,"style":108},"import { blurhashToDataUri } from '@unpic\u002Fplaceholder'\n\nconst newImgTag = match[0].replace(\n  \u002F\u003Cimg(\\s+)\u002F,\n  `\u003Cimg$1width=\"${data.width}\" height=\"${data.height}\" style=\"background-size: cover; background-image: url(${blurhashToDataUri(blurhash)});\" loading=\"lazy\" `\n)\n",[110,1497,1498,1516,1520,1546,1564,1616],{"__ignoreMap":108},[65,1499,1500,1502,1504,1506,1508,1510,1512,1514],{"class":114,"line":115},[65,1501,401],{"class":400},[65,1503,424],{"class":118},[65,1505,1377],{"class":404},[65,1507,430],{"class":118},[65,1509,433],{"class":400},[65,1511,411],{"class":118},[65,1513,1386],{"class":148},[65,1515,417],{"class":118},[65,1517,1518],{"class":114,"line":129},[65,1519,445],{"emptyLinePlaceholder":42},[65,1521,1522,1524,1527,1529,1532,1535,1538,1540,1543],{"class":114,"line":168},[65,1523,1309],{"class":138},[65,1525,1526],{"class":404}," newImgTag ",[65,1528,142],{"class":118},[65,1530,1531],{"class":404}," match[",[65,1533,1534],{"class":806},"0",[65,1536,1537],{"class":404},"]",[65,1539,81],{"class":118},[65,1541,1542],{"class":456},"replace",[65,1544,1545],{"class":404},"(\n",[65,1547,1548,1551,1554,1556,1559,1562],{"class":114,"line":216},[65,1549,1550],{"class":118},"  \u002F",[65,1552,1553],{"class":148},"\u003Cimg",[65,1555,459],{"class":118},[65,1557,1558],{"class":148},"\\s",[65,1560,1561],{"class":118},"+)\u002F",[65,1563,484],{"class":118},[65,1565,1566,1569,1572,1575,1578,1580,1582,1584,1587,1589,1591,1593,1595,1597,1600,1602,1605,1608,1610,1613],{"class":114,"line":230},[65,1567,1568],{"class":118},"  `",[65,1570,1571],{"class":148},"\u003Cimg$1width=\"",[65,1573,1574],{"class":118},"${",[65,1576,1577],{"class":404},"data",[65,1579,81],{"class":118},[65,1581,1464],{"class":404},[65,1583,548],{"class":118},[65,1585,1586],{"class":148},"\" height=\"",[65,1588,1574],{"class":118},[65,1590,1577],{"class":404},[65,1592,81],{"class":118},[65,1594,1473],{"class":404},[65,1596,548],{"class":118},[65,1598,1599],{"class":148},"\" style=\"background-size: cover; background-image: url(",[65,1601,1574],{"class":118},[65,1603,1604],{"class":456},"blurhashToDataUri",[65,1606,1607],{"class":404},"(blurhash)",[65,1609,548],{"class":118},[65,1611,1612],{"class":148},");\" loading=\"lazy\" ",[65,1614,1615],{"class":118},"`\n",[65,1617,1618],{"class":114,"line":245},[65,1619,551],{"class":404},[10,1621,1622],{},"Après avoir implémenté toutes ces étapes, notre plugin final ressemble à ceci :",[103,1624,1626],{"className":391,"code":1625,"filename":387,"language":393,"meta":108,"style":108},"(() => {\n  let resolvedConfig: ResolvedConfig\n\n  const imgTagRegex = \u002F\u003Cimg\\s[^>]*src=[\"']([^\"']+)[\"'][^>]*>\u002Fg\n\n  return {\n    name: 'unpic',\n    enforce: 'pre',\n    transform: {\n      filter: {\n        id: \u002F\\.vue$\u002F,\n        code: \u002F\u003Cimg\\s[^>]*src=[\"']([^\"']+)[\"'][^>]*>\u002Fg,\n      },\n      async handler(code, id) {\n        const s = new MagicString(code)\n\n        let match = imgTagRegex.exec(code)\n\n        do {\n          const srcValue = match![1]\n\n          const img = await readFile(join(resolvedConfig.publicDir, srcValue))\n          const data = await getPixels(img)\n          const blurhash = encode(Uint8ClampedArray.from(data.data), data.width, data.height, 4, 4)\n\n          const imgTagStart = match!.index\n          const imgTagEnd = imgTagStart + match![0].length\n\n          const newImgTag = match![0].replace(\n            \u002F\u003Cimg(\\s+)\u002F,\n            `\u003Cimg$1width=\"${data.width}\" height=\"${data.height}\" style=\"background-size: cover; background-image: url(${blurhashToDataUri(blurhash)});\" loading=\"lazy\" `\n          )\n\n          s.overwrite(imgTagStart, imgTagEnd, newImgTag)\n\n          match = imgTagRegex.exec(code)\n        } while (match !== null)\n\n        return {\n          code: s.toString(),\n          map: s.generateMap({ hires: true })\n        }\n      }\n    },\n    configResolved(config) {\n      resolvedConfig = config\n    }\n  }\n})()\n",[110,1627,1628,1638,1651,1655,1700,1704,1710,1724,1738,1746,1754,1772,1816,1820,1838,1860,1864,1886,1891,1899,1919,1924,1958,1977,2033,2038,2056,2086,2091,2117,2133,2177,2183,2188,2214,2219,2239,2259,2264,2272,2291,2324,2330,2335,2341,2356,2367,2372,2377],{"__ignoreMap":108},[65,1629,1630,1632,1634,1636],{"class":114,"line":115},[65,1631,459],{"class":404},[65,1633,481],{"class":118},[65,1635,494],{"class":138},[65,1637,497],{"class":118},[65,1639,1640,1643,1646,1648],{"class":114,"line":129},[65,1641,1642],{"class":138},"  let",[65,1644,1645],{"class":404}," resolvedConfig",[65,1647,470],{"class":118},[65,1649,1650],{"class":1109}," ResolvedConfig\n",[65,1652,1653],{"class":114,"line":168},[65,1654,445],{"emptyLinePlaceholder":42},[65,1656,1657,1659,1661,1663,1665,1667,1669,1671,1673,1675,1677,1679,1681,1683,1685,1687,1689,1691,1693,1695,1697],{"class":114,"line":216},[65,1658,751],{"class":138},[65,1660,754],{"class":404},[65,1662,757],{"class":118},[65,1664,760],{"class":118},[65,1666,763],{"class":148},[65,1668,766],{"class":118},[65,1670,769],{"class":148},[65,1672,772],{"class":118},[65,1674,775],{"class":148},[65,1676,778],{"class":118},[65,1678,781],{"class":148},[65,1680,784],{"class":118},[65,1682,781],{"class":148},[65,1684,789],{"class":118},[65,1686,781],{"class":148},[65,1688,794],{"class":118},[65,1690,769],{"class":148},[65,1692,772],{"class":118},[65,1694,769],{"class":148},[65,1696,803],{"class":118},[65,1698,1699],{"class":806},"g\n",[65,1701,1702],{"class":114,"line":230},[65,1703,445],{"emptyLinePlaceholder":42},[65,1705,1706,1708],{"class":114,"line":245},[65,1707,588],{"class":400},[65,1709,497],{"class":118},[65,1711,1712,1714,1716,1718,1720,1722],{"class":114,"line":260},[65,1713,595],{"class":122},[65,1715,470],{"class":118},[65,1717,411],{"class":118},[65,1719,357],{"class":148},[65,1721,518],{"class":118},[65,1723,484],{"class":118},[65,1725,1726,1728,1730,1732,1734,1736],{"class":114,"line":275},[65,1727,836],{"class":122},[65,1729,470],{"class":118},[65,1731,411],{"class":118},[65,1733,103],{"class":148},[65,1735,518],{"class":118},[65,1737,484],{"class":118},[65,1739,1740,1742,1744],{"class":114,"line":290},[65,1741,851],{"class":122},[65,1743,470],{"class":118},[65,1745,497],{"class":118},[65,1747,1748,1750,1752],{"class":114,"line":296},[65,1749,860],{"class":122},[65,1751,470],{"class":118},[65,1753,497],{"class":118},[65,1755,1756,1758,1760,1762,1764,1766,1768,1770],{"class":114,"line":528},[65,1757,870],{"class":122},[65,1759,470],{"class":118},[65,1761,760],{"class":118},[65,1763,877],{"class":404},[65,1765,880],{"class":148},[65,1767,883],{"class":400},[65,1769,803],{"class":118},[65,1771,484],{"class":118},[65,1773,1774,1776,1778,1780,1782,1784,1786,1788,1790,1792,1794,1796,1798,1800,1802,1804,1806,1808,1810,1812,1814],{"class":114,"line":537},[65,1775,692],{"class":122},[65,1777,470],{"class":118},[65,1779,760],{"class":118},[65,1781,763],{"class":148},[65,1783,766],{"class":118},[65,1785,769],{"class":148},[65,1787,772],{"class":118},[65,1789,775],{"class":148},[65,1791,778],{"class":118},[65,1793,781],{"class":148},[65,1795,784],{"class":118},[65,1797,781],{"class":148},[65,1799,789],{"class":118},[65,1801,781],{"class":148},[65,1803,794],{"class":118},[65,1805,769],{"class":148},[65,1807,772],{"class":118},[65,1809,769],{"class":148},[65,1811,803],{"class":118},[65,1813,807],{"class":806},[65,1815,484],{"class":118},[65,1817,1818],{"class":114,"line":545},[65,1819,903],{"class":118},[65,1821,1822,1824,1826,1828,1830,1832,1834,1836],{"class":114,"line":711},[65,1823,908],{"class":138},[65,1825,911],{"class":122},[65,1827,459],{"class":118},[65,1829,110],{"class":618},[65,1831,621],{"class":118},[65,1833,624],{"class":618},[65,1835,627],{"class":118},[65,1837,497],{"class":118},[65,1839,1840,1843,1846,1848,1851,1854,1856,1858],{"class":114,"line":717},[65,1841,1842],{"class":138},"        const",[65,1844,1845],{"class":404}," s",[65,1847,757],{"class":118},[65,1849,1850],{"class":118}," new",[65,1852,1853],{"class":456}," MagicString",[65,1855,459],{"class":122},[65,1857,110],{"class":404},[65,1859,551],{"class":122},[65,1861,1862],{"class":114,"line":723},[65,1863,445],{"emptyLinePlaceholder":42},[65,1865,1867,1870,1872,1874,1876,1878,1880,1882,1884],{"class":114,"line":1866},17,[65,1868,1869],{"class":138},"        let",[65,1871,1202],{"class":404},[65,1873,757],{"class":118},[65,1875,754],{"class":404},[65,1877,81],{"class":118},[65,1879,1164],{"class":456},[65,1881,459],{"class":122},[65,1883,110],{"class":404},[65,1885,551],{"class":122},[65,1887,1889],{"class":114,"line":1888},18,[65,1890,445],{"emptyLinePlaceholder":42},[65,1892,1894,1897],{"class":114,"line":1893},19,[65,1895,1896],{"class":400},"        do",[65,1898,497],{"class":118},[65,1900,1902,1905,1907,1909,1911,1913,1915,1917],{"class":114,"line":1901},20,[65,1903,1904],{"class":138},"          const",[65,1906,1197],{"class":404},[65,1908,757],{"class":118},[65,1910,1202],{"class":404},[65,1912,642],{"class":118},[65,1914,778],{"class":122},[65,1916,1207],{"class":806},[65,1918,1210],{"class":122},[65,1920,1922],{"class":114,"line":1921},21,[65,1923,445],{"emptyLinePlaceholder":42},[65,1925,1927,1929,1932,1934,1936,1938,1940,1942,1944,1947,1949,1951,1953,1955],{"class":114,"line":1926},22,[65,1928,1904],{"class":138},[65,1930,1931],{"class":404}," img",[65,1933,757],{"class":118},[65,1935,1317],{"class":400},[65,1937,1269],{"class":456},[65,1939,459],{"class":122},[65,1941,1324],{"class":456},[65,1943,459],{"class":122},[65,1945,1946],{"class":404},"resolvedConfig",[65,1948,81],{"class":118},[65,1950,1332],{"class":404},[65,1952,621],{"class":118},[65,1954,1197],{"class":404},[65,1956,1957],{"class":122},"))\n",[65,1959,1961,1963,1965,1967,1969,1971,1973,1975],{"class":114,"line":1960},23,[65,1962,1904],{"class":138},[65,1964,1459],{"class":404},[65,1966,757],{"class":118},[65,1968,1317],{"class":400},[65,1970,1357],{"class":456},[65,1972,459],{"class":122},[65,1974,135],{"class":404},[65,1976,551],{"class":122},[65,1978,1980,1982,1984,1986,1988,1990,1993,1995,1997,1999,2001,2003,2005,2007,2009,2011,2013,2015,2017,2019,2021,2023,2025,2027,2029,2031],{"class":114,"line":1979},24,[65,1981,1904],{"class":138},[65,1983,1125],{"class":404},[65,1985,757],{"class":118},[65,1987,1397],{"class":456},[65,1989,459],{"class":122},[65,1991,1992],{"class":404},"Uint8ClampedArray",[65,1994,81],{"class":118},[65,1996,408],{"class":456},[65,1998,459],{"class":122},[65,2000,1577],{"class":404},[65,2002,81],{"class":118},[65,2004,1577],{"class":404},[65,2006,627],{"class":122},[65,2008,621],{"class":118},[65,2010,1459],{"class":404},[65,2012,81],{"class":118},[65,2014,1464],{"class":404},[65,2016,621],{"class":118},[65,2018,1459],{"class":404},[65,2020,81],{"class":118},[65,2022,1473],{"class":404},[65,2024,621],{"class":118},[65,2026,1478],{"class":806},[65,2028,621],{"class":118},[65,2030,1478],{"class":806},[65,2032,551],{"class":122},[65,2034,2036],{"class":114,"line":2035},25,[65,2037,445],{"emptyLinePlaceholder":42},[65,2039,2041,2043,2046,2048,2050,2053],{"class":114,"line":2040},26,[65,2042,1904],{"class":138},[65,2044,2045],{"class":404}," imgTagStart",[65,2047,757],{"class":118},[65,2049,1202],{"class":404},[65,2051,2052],{"class":118},"!.",[65,2054,2055],{"class":404},"index\n",[65,2057,2059,2061,2064,2066,2068,2071,2073,2075,2077,2079,2081,2083],{"class":114,"line":2058},27,[65,2060,1904],{"class":138},[65,2062,2063],{"class":404}," imgTagEnd",[65,2065,757],{"class":118},[65,2067,2045],{"class":404},[65,2069,2070],{"class":118}," +",[65,2072,1202],{"class":404},[65,2074,642],{"class":118},[65,2076,778],{"class":122},[65,2078,1534],{"class":806},[65,2080,1537],{"class":122},[65,2082,81],{"class":118},[65,2084,2085],{"class":404},"length\n",[65,2087,2089],{"class":114,"line":2088},28,[65,2090,445],{"emptyLinePlaceholder":42},[65,2092,2094,2096,2099,2101,2103,2105,2107,2109,2111,2113,2115],{"class":114,"line":2093},29,[65,2095,1904],{"class":138},[65,2097,2098],{"class":404}," newImgTag",[65,2100,757],{"class":118},[65,2102,1202],{"class":404},[65,2104,642],{"class":118},[65,2106,778],{"class":122},[65,2108,1534],{"class":806},[65,2110,1537],{"class":122},[65,2112,81],{"class":118},[65,2114,1542],{"class":456},[65,2116,1545],{"class":122},[65,2118,2120,2123,2125,2127,2129,2131],{"class":114,"line":2119},30,[65,2121,2122],{"class":118},"            \u002F",[65,2124,1553],{"class":148},[65,2126,459],{"class":118},[65,2128,1558],{"class":148},[65,2130,1561],{"class":118},[65,2132,484],{"class":118},[65,2134,2136,2139,2141,2143,2145,2147,2149,2151,2153,2155,2157,2159,2161,2163,2165,2167,2169,2171,2173,2175],{"class":114,"line":2135},31,[65,2137,2138],{"class":118},"            `",[65,2140,1571],{"class":148},[65,2142,1574],{"class":118},[65,2144,1577],{"class":404},[65,2146,81],{"class":118},[65,2148,1464],{"class":404},[65,2150,548],{"class":118},[65,2152,1586],{"class":148},[65,2154,1574],{"class":118},[65,2156,1577],{"class":404},[65,2158,81],{"class":118},[65,2160,1473],{"class":404},[65,2162,548],{"class":118},[65,2164,1599],{"class":148},[65,2166,1574],{"class":118},[65,2168,1604],{"class":456},[65,2170,1607],{"class":404},[65,2172,548],{"class":118},[65,2174,1612],{"class":148},[65,2176,1615],{"class":118},[65,2178,2180],{"class":114,"line":2179},32,[65,2181,2182],{"class":122},"          )\n",[65,2184,2186],{"class":114,"line":2185},33,[65,2187,445],{"emptyLinePlaceholder":42},[65,2189,2191,2194,2196,2199,2201,2204,2206,2208,2210,2212],{"class":114,"line":2190},34,[65,2192,2193],{"class":404},"          s",[65,2195,81],{"class":118},[65,2197,2198],{"class":456},"overwrite",[65,2200,459],{"class":122},[65,2202,2203],{"class":404},"imgTagStart",[65,2205,621],{"class":118},[65,2207,2063],{"class":404},[65,2209,621],{"class":118},[65,2211,2098],{"class":404},[65,2213,551],{"class":122},[65,2215,2217],{"class":114,"line":2216},35,[65,2218,445],{"emptyLinePlaceholder":42},[65,2220,2222,2225,2227,2229,2231,2233,2235,2237],{"class":114,"line":2221},36,[65,2223,2224],{"class":404},"          match",[65,2226,757],{"class":118},[65,2228,754],{"class":404},[65,2230,81],{"class":118},[65,2232,1164],{"class":456},[65,2234,459],{"class":122},[65,2236,110],{"class":404},[65,2238,551],{"class":122},[65,2240,2242,2245,2247,2249,2252,2255,2257],{"class":114,"line":2241},37,[65,2243,2244],{"class":118},"        }",[65,2246,1240],{"class":400},[65,2248,639],{"class":122},[65,2250,2251],{"class":404},"match",[65,2253,2254],{"class":118}," !==",[65,2256,1249],{"class":118},[65,2258,551],{"class":122},[65,2260,2262],{"class":114,"line":2261},38,[65,2263,445],{"emptyLinePlaceholder":42},[65,2265,2267,2270],{"class":114,"line":2266},39,[65,2268,2269],{"class":400},"        return",[65,2271,497],{"class":118},[65,2273,2275,2278,2280,2282,2284,2287,2289],{"class":114,"line":2274},40,[65,2276,2277],{"class":122},"          code",[65,2279,470],{"class":118},[65,2281,1845],{"class":404},[65,2283,81],{"class":118},[65,2285,2286],{"class":456},"toString",[65,2288,481],{"class":122},[65,2290,484],{"class":118},[65,2292,2294,2297,2299,2301,2303,2306,2308,2311,2314,2316,2320,2322],{"class":114,"line":2293},41,[65,2295,2296],{"class":122},"          map",[65,2298,470],{"class":118},[65,2300,1845],{"class":404},[65,2302,81],{"class":118},[65,2304,2305],{"class":456},"generateMap",[65,2307,459],{"class":122},[65,2309,2310],{"class":118},"{",[65,2312,2313],{"class":122}," hires",[65,2315,470],{"class":118},[65,2317,2319],{"class":2318},"sfNiH"," true",[65,2321,430],{"class":118},[65,2323,551],{"class":122},[65,2325,2327],{"class":114,"line":2326},42,[65,2328,2329],{"class":118},"        }\n",[65,2331,2333],{"class":114,"line":2332},43,[65,2334,525],{"class":118},[65,2336,2338],{"class":114,"line":2337},44,[65,2339,2340],{"class":118},"    },\n",[65,2342,2344,2347,2349,2352,2354],{"class":114,"line":2343},45,[65,2345,2346],{"class":122},"    configResolved",[65,2348,459],{"class":118},[65,2350,2351],{"class":618},"config",[65,2353,627],{"class":118},[65,2355,497],{"class":118},[65,2357,2359,2362,2364],{"class":114,"line":2358},46,[65,2360,2361],{"class":404},"      resolvedConfig",[65,2363,757],{"class":118},[65,2365,2366],{"class":404}," config\n",[65,2368,2370],{"class":114,"line":2369},47,[65,2371,714],{"class":118},[65,2373,2375],{"class":114,"line":2374},48,[65,2376,720],{"class":118},[65,2378,2380,2382],{"class":114,"line":2379},49,[65,2381,548],{"class":118},[65,2383,534],{"class":404},[60,2385,2386],{},[10,2387,2388,2390,2391,2394,2395,2397],{},[65,2389,67],{},"\nLe hook ",[110,2392,2393],{},"configResolved"," est utilisé pour obtenir la configuration Vite résolue, dont nous avons besoin pour accéder au ",[110,2396,1332],{}," où les images sont stockées.",[10,2399,2400,2401],{},"Le code source complet est également disponible sur ",[2402,2403,81],"git-hub-link",{"repo":2404},"Barbapapazes\u002Fvite-vue-unpic",[10,2406,2407],{},"Avec ce plugin, nous offrons maintenant une meilleure expérience utilisateur sans ajouter de travail manuel à notre processus de création de contenu.",[36,2409,38,2410,38,2415],{},[40,2411,43,2412,38],{"autoPlay":42,"loop":42,"muted":42,"playsInline":42},[45,2413],{"src":2414,"type":48},"https:\u002F\u002Fimages.soubiran.dev\u002Fposts\u002Ferase-cls-by-automatically-optimizing-images-with-vite\u002Foptimized-image.mp4",[50,2416,2417],{},"Exemple d'image optimisée avec espace réservé et chargement différé.",[10,2419,2420,2421,2423,2424,2426,2427,2430],{},"Lorsque la page se charge, l'espace de l'image est déjà réservé grâce aux attributs ",[110,2422,1464],{}," et ",[110,2425,1473],{},", et un placeholder flouté est affiché en utilisant le style ",[110,2428,2429],{},"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.",[83,2432,2434],{"id":2433},"observer-le-pipeline","Observer le pipeline",[10,2436,2437,2438,81],{},"Nous pouvons observer comment notre plugin interagit avec le pipeline Vite en utilisant ",[70,2439,2442],{"href":2440,"rel":2441},"https:\u002F\u002Fgithub.com\u002Fantfu-collective\u002Fvite-plugin-inspect",[79],"vite-plugin-inspect",[103,2444,2448],{"className":2445,"code":2446,"language":2447,"meta":108,"style":108},"language-sh shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","pnpm add -D vite-plugin-inspect\n","sh",[110,2449,2450],{"__ignoreMap":108},[65,2451,2452,2454,2457,2459],{"class":114,"line":115},[65,2453,1110],{"class":1109},[65,2455,2456],{"class":148}," add",[65,2458,1116],{"class":148},[65,2460,2461],{"class":148}," vite-plugin-inspect\n",[10,2463,2464,2465,2467],{},"Ensuite, ajoutez-le au fichier ",[110,2466,387],{}," :",[103,2469,2471],{"className":391,"code":2470,"filename":387,"language":393,"meta":108,"style":108},"import { defineConfig } from 'vite'\nimport inspect from 'vite-plugin-inspect'\n\nexport default defineConfig({\n  plugins: [\n    inspect()\n\n    \u002F\u002F ...\n  ]\n})\n",[110,2472,2473,2491,2506,2510,2522,2530,2538,2542,2547,2552],{"__ignoreMap":108},[65,2474,2475,2477,2479,2481,2483,2485,2487,2489],{"class":114,"line":115},[65,2476,401],{"class":400},[65,2478,424],{"class":118},[65,2480,427],{"class":404},[65,2482,430],{"class":118},[65,2484,433],{"class":400},[65,2486,411],{"class":118},[65,2488,438],{"class":148},[65,2490,417],{"class":118},[65,2492,2493,2495,2498,2500,2502,2504],{"class":114,"line":129},[65,2494,401],{"class":400},[65,2496,2497],{"class":404}," inspect ",[65,2499,408],{"class":400},[65,2501,411],{"class":118},[65,2503,2442],{"class":148},[65,2505,417],{"class":118},[65,2507,2508],{"class":114,"line":168},[65,2509,445],{"emptyLinePlaceholder":42},[65,2511,2512,2514,2516,2518,2520],{"class":114,"line":216},[65,2513,450],{"class":400},[65,2515,453],{"class":400},[65,2517,427],{"class":456},[65,2519,459],{"class":404},[65,2521,462],{"class":118},[65,2523,2524,2526,2528],{"class":114,"line":230},[65,2525,467],{"class":122},[65,2527,470],{"class":118},[65,2529,473],{"class":404},[65,2531,2532,2535],{"class":114,"line":245},[65,2533,2534],{"class":456},"    inspect",[65,2536,2537],{"class":404},"()\n",[65,2539,2540],{"class":114,"line":260},[65,2541,445],{"emptyLinePlaceholder":42},[65,2543,2544],{"class":114,"line":275},[65,2545,2546],{"class":676},"    \u002F\u002F ...\n",[65,2548,2549],{"class":114,"line":290},[65,2550,2551],{"class":404},"  ]\n",[65,2553,2554,2556],{"class":114,"line":296},[65,2555,548],{"class":118},[65,2557,551],{"class":404},[10,2559,2560],{},"Ensuite, lancez le serveur de développement :",[103,2562,2564],{"className":2445,"code":2563,"language":2447,"meta":108,"style":108},"pnpm dev\n",[110,2565,2566],{"__ignoreMap":108},[65,2567,2568,2570],{"class":114,"line":115},[65,2569,1110],{"class":1109},[65,2571,2572],{"class":148}," dev\n",[10,2574,2575,2576,81],{},"Et ouvrez le panneau d'inspection à ",[110,2577,2578],{},"http:\u002F\u002Flocalhost:5173\u002F__inspect\u002F",[10,2580,2581,2582,2585],{},"Vous pouvez voir tous les fichiers traités par Vite. En cliquant sur ",[110,2583,2584],{},"App.vue",", vous pouvez voir comment notre plugin a transformé le code étape par étape.",[10,2587,2588],{},[135,2589],{"alt":2590,"src":2591},"Vite Plugin Inspect - Étapes de transformation d'image","\u002Fposts\u002Ferase-cls-by-automatically-optimizing-images-with-vite\u002Fvite-plugin-inspect.webp",[83,2593,2595],{"id":2594},"aller-plus-loin","Aller plus loin",[10,2597,2598],{},"À travers cet exemple relativement simple mais utile, j'espère que vous avez appris la puissance des plugins Vite.",[10,2600,2601],{},"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.",[10,2603,2604,2605,2610,2611,2423,2616,2621,2622,2625],{},"Pour en savoir plus sur les plugins, je recommande vivement de lire le code source de plugins Vite populaires comme ",[70,2606,2609],{"href":2607,"rel":2608},"https:\u002F\u002Fgithub.com\u002Fvitejs\u002Fvite-plugin-vue",[79],"vite-plugin-vue"," ou ",[70,2612,2615],{"href":2613,"rel":2614},"https:\u002F\u002Fgithub.com\u002Funplugin\u002Funplugin-icons",[79],"unplugin-icons",[70,2617,2620],{"href":2618,"rel":2619},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fvitepress\u002Ftree\u002Fmain\u002Fsrc\u002Fnode\u002Fplugins",[79],"ceux de VitePress",". ",[70,2623,2442],{"href":2440,"rel":2624},[79]," aide vraiment à comprendre comment les plugins interagissent entre eux et modifient le code.",[10,2627,2628],{},"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.",[10,2630,2631,2632,2634],{},"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 ",[110,2633,335],{}," 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.",[24,2636],{},[10,2638,2639],{},"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 !",[2641,2642,2643],"style",{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":108,"searchDepth":129,"depth":129,"links":2645},[2646,2647,2648,2649,2650],{"id":85,"depth":129,"text":86},{"id":321,"depth":129,"text":322},{"id":1054,"depth":129,"text":1055},{"id":2433,"depth":129,"text":2434},{"id":2594,"depth":129,"text":2595},"a505274a-14a6-471e-95a2-a3dbc64cd170","2025-11-11","Le Cumulative Layout Shift, lorsqu'il est lié aux images, peut être difficile à gérer. Découvrez comment supprimer le CLS automatiquement.","md",{},"\u002Ffr\u002Fposts\u002Ferase-cls-by-automatically-optimizing-images-with-vite","---\ncontentId: a505274a-14a6-471e-95a2-a3dbc64cd170\ntitle: Éliminer le CLS en optimisant les images avec Vite\ndescription: Le Cumulative Layout Shift, lorsqu'il est lié aux images, peut être difficile à gérer. Découvrez comment supprimer le CLS automatiquement.\ndate: 2025-11-11\n---\n\nJ'adore Vite, non seulement parce qu'il est rapide, mais aussi pour son incroyable système de plugins.\n\nÀ 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.\n\nUne fois que vous avez compris cela, tout devient possible. _Tout en tant que plugin Vite !_\n\n---\n\nDepuis 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.\n\nCependant, 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.\n\nAu-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.\n\n\u003Cfigure>\n  \u003Cvideo autoplay loop muted playsinline>\n    \u003Csource src=\"https:\u002F\u002Fimages.soubiran.dev\u002Fposts\u002Ferase-cls-by-automatically-optimizing-images-with-vite\u002Fcls-issue.mp4\" type=\"video\u002Fmp4\">\n  \u003C\u002Fvideo>\n  \u003Cfigcaption>Exemple de problème de Cumulative Layout Shift (CLS) lors du chargement différé d'une image sans espace réservé.\u003C\u002Ffigcaption>\n\u003C\u002Ffigure>\n\nC'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é !_\n\n> [!NOTE]\n> Cet article est dédié au contenu HTML. [Un autre article](.\u002Fmarkdown-exit-breaks-the-rules-with-async-rendering.md) couvre la gestion du contenu Markdown en utilisant [Markdown Exit](https:\u002F\u002Fmarkdown-exit.pages.dev\u002F).\n\n## Vite à la rescousse\n\nLa première question qui vient à l'esprit est : Comment savoir si un plugin Vite est le bon outil pour le travail ?\n\nC'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._\n\nAnalysons le problème en regardant ses entrées et ses sorties.\n\nL'entrée est probablement un extrait HTML brut comme celui-ci :\n\n```html\n\u003Cdiv>\n  \u003Cimg src=\"\u002Fpath\u002Fto\u002Fimage.jpg\" alt=\"An image\">\n\u003C\u002Fdiv>\n```\n\nLa sortie devrait être un extrait HTML optimisé comme celui-ci :\n\n```html\n\u003Cdiv>\n  \u003Cimg\n    src=\"https:\u002F\u002Fcdn.example.com\u002Fpath\u002Fto\u002Fimage-optimized.jpg\"\n    alt=\"An image\"\n    width=\"600\"\n    height=\"400\"\n    loading=\"lazy\"\n    style=\"background-image: url('data:image\u002Fsvg+xml;base64,...'); background-size: cover;\"\n  >\n\u003C\u002Fdiv>\n```\n\nVite 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.\n\nNotre 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, ..._\n\n> [!NOTE]\n> 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.\n\n## Construire le plugin\n\nQuel est le plan ? C'est bien d'avoir une idée de ce que nous voulons accomplir, mais comment y parvenir ?\n\nNous devons nous greffer sur le pipeline de Vite et rechercher les templates Vue. Ensuite, nous devons trouver toutes les balises `\u003Cimg>`, extraire leurs attributs `src`, charger les images, obtenir leurs dimensions, générer le placeholder flouté, et enfin remplacer les balises `\u003Cimg>` originales par des versions optimisées.\n\n> [!NOTE]\n> J'utilise Vue pour tous mes projets, mais la même approche peut être appliquée à n'importe quel framework frontend supporté par Vite.\n\nCela semble plus facile que ça ne l'est, surtout quand on ne sait pas par où commencer et quel outil utiliser.\n\nPour gérer tout le traitement d'image, j'utilise [unpic](https:\u002F\u002Funpic.pics\u002F). 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](https:\u002F\u002Fgithub.com\u002FRich-Harris\u002Fmagic-string).\n\nC'est tout ce dont nous avons besoin !\n\nMaintenant, nous pouvons créer un plugin Vite qui fait exactement ce que nous voulons.\n\n> [!NOTE]\n> Vous pouvez utiliser un projet Vite + Vue comme point de départ. Le code source du plugin final est disponible sur [GitHub](https:\u002F\u002Fgithub.com\u002FBarbapapazes\u002Fvite-vue-unpic).\n\nDans le fichier `vite.config.ts` de notre projet, créons d'abord un plugin :\n\n```ts [vite.config.ts]\nimport vue from '@vitejs\u002Fplugin-vue'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: [\n    vue(),\n    (() => {\n      return {\n        name: 'unpic',\n      }\n    })()\n  ],\n})\n```\n\nAu 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.\n\nEnsuite, nous devons nous greffer sur la bonne partie du pipeline. Il existe [beaucoup de hooks](https:\u002F\u002Frollupjs.org\u002Fplugin-development\u002F#build-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.\n\n```ts [vite.config.ts]\n(() => {\n  return {\n    name: 'unpic',\n    async transform(code, id) {\n      if (!id.endsWith('.vue')) \u002F\u002F [!code highlight]\n        return\n\n      \u002F\u002F Some magic will happen here\n\n      return {\n        code,\n        map: null,\n      }\n    }\n  }\n})()\n```\n\nMais 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.\n\n```ts [vite.config.ts]\n(() => {\n  const imgTagRegex = \u002F\u003Cimg\\s[^>]*src=[\"']([^\"']+)[\"'][^>]*>\u002Fg \u002F\u002F [!code highlight]\n\n  return {\n    name: 'unpic',\n    enforce: 'pre',\n    transform: {\n      filter: {\n        id: \u002F\\.vue$\u002F, \u002F\u002F [!code highlight]\n        code: imgTagRegex, \u002F\u002F [!code highlight]\n      },\n      async handler(code) {},\n    }\n  }\n})()\n```\n\nCe `filter` garantit que notre plugin ne s'exécute que sur les fichiers Vue contenant au moins une balise `\u003Cimg>`. Tellement mieux !\n\n\u003CCalloutReadMore text=\"Filtres de hook de plugin\" href=\"https:\u002F\u002Frolldown.rs\u002Fapis\u002Fplugin-hook-filters\" \u002F>\n\nUne 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 :\n\n1. Placer notre plugin avant le plugin Vue dans le tableau `plugins`.\n2. Utiliser l'option `enforce: 'pre'` dans la définition de notre plugin.\n\n```ts [vite.config.ts]\n(() => {\n  return {\n    name: 'unpic',\n    enforce: 'pre', \u002F\u002F [!code highlight]\n    transform: {\n      \u002F\u002F ...\n    }\n  }\n})()\n```\n\nMaintenant, nous pouvons commencer à traiter les fichiers Vue pour améliorer les balises `\u003Cimg>`.\n\n## Transformer le code\n\nDans le hook `transform`, voici ce qui doit être fait :\n\n1. Analyser le code pour trouver toutes les balises `\u003Cimg>`.\n2. Pour chaque balise `\u003Cimg>`, extraire l'attribut `src`.\n3. Lire le fichier depuis le système de fichiers.\n4. Traiter l'image avec `unpic` pour obtenir ses dimensions et un placeholder flouté.\n5. Générer et remplacer la balise `\u003Cimg>` originale par la balise `\u003Cimg>` optimisée avec tous les attributs nécessaires.\n\nAvant de commencer, nous devons installer les dépendances :\n\n```bash\npnpm install -D @unpic\u002Fpixels @unpic\u002Fplaceholder blurhash magic-string\n```\n\nImplémentons cela étape par étape.\n\n1. Analyser le code pour trouver toutes les balises `\u003Cimg>`.\n\n  ```ts [vite.config.ts]\n  \u002F\u002F eslint-disable-next-line prefer-const\n  let match = imgTagRegex.exec(code) \u002F\u002F On réutilise la regex définie plus tôt\n  ```\n\n2. Pour chaque balise `\u003Cimg>`, extraire l'attribut `src`.\n\n  ```ts [vite.config.ts]\n  do {\n    const srcValue = match[1]\n\n    match = imgTagRegex.exec(code)\n  } while (match !== null)\n  ```\n\n3. Lire le fichier depuis le système de fichiers.\n\n  ```ts [vite.config.ts]\n  import { readFile } from 'node:fs\u002Fpromises'\n  import { join } from 'node:path'\n\n  const img = await readFile(join(resolvedConfig.publicDir, srcValue))\n  ```\n\n4. Traiter l'image avec `unpic` pour obtenir ses dimensions et un placeholder flouté.\n\n  ```ts [vite.config.ts]\n  import { getPixels } from '@unpic\u002Fpixels'\n  import { blurhashToDataUri } from '@unpic\u002Fplaceholder'\n  import { encode } from 'blurhash'\n\n  const data = await getPixels(img)\n  const blurhash = encode(Uint8ClampedArray.from(data.data), data.width, data.height, 4, 4)\n  ```\n\n5. Générer et remplacer la balise `\u003Cimg>` originale par la balise `\u003Cimg>` optimisée avec tous les attributs nécessaires.\n\n  ```ts [vite.config.ts]\n  import { blurhashToDataUri } from '@unpic\u002Fplaceholder'\n\n  const newImgTag = match[0].replace(\n    \u002F\u003Cimg(\\s+)\u002F,\n    `\u003Cimg$1width=\"${data.width}\" height=\"${data.height}\" style=\"background-size: cover; background-image: url(${blurhashToDataUri(blurhash)});\" loading=\"lazy\" `\n  )\n  ```\n\nAprès avoir implémenté toutes ces étapes, notre plugin final ressemble à ceci :\n\n```ts [vite.config.ts]\n(() => {\n  let resolvedConfig: ResolvedConfig\n\n  const imgTagRegex = \u002F\u003Cimg\\s[^>]*src=[\"']([^\"']+)[\"'][^>]*>\u002Fg\n\n  return {\n    name: 'unpic',\n    enforce: 'pre',\n    transform: {\n      filter: {\n        id: \u002F\\.vue$\u002F,\n        code: \u002F\u003Cimg\\s[^>]*src=[\"']([^\"']+)[\"'][^>]*>\u002Fg,\n      },\n      async handler(code, id) {\n        const s = new MagicString(code)\n\n        let match = imgTagRegex.exec(code)\n\n        do {\n          const srcValue = match![1]\n\n          const img = await readFile(join(resolvedConfig.publicDir, srcValue))\n          const data = await getPixels(img)\n          const blurhash = encode(Uint8ClampedArray.from(data.data), data.width, data.height, 4, 4)\n\n          const imgTagStart = match!.index\n          const imgTagEnd = imgTagStart + match![0].length\n\n          const newImgTag = match![0].replace(\n            \u002F\u003Cimg(\\s+)\u002F,\n            `\u003Cimg$1width=\"${data.width}\" height=\"${data.height}\" style=\"background-size: cover; background-image: url(${blurhashToDataUri(blurhash)});\" loading=\"lazy\" `\n          )\n\n          s.overwrite(imgTagStart, imgTagEnd, newImgTag)\n\n          match = imgTagRegex.exec(code)\n        } while (match !== null)\n\n        return {\n          code: s.toString(),\n          map: s.generateMap({ hires: true })\n        }\n      }\n    },\n    configResolved(config) {\n      resolvedConfig = config\n    }\n  }\n})()\n```\n\n> [!NOTE]\n> 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.\n\nLe code source complet est également disponible sur \u003CGitHubLink repo=\"Barbapapazes\u002Fvite-vue-unpic\" \u002F>.\n\nAvec ce plugin, nous offrons maintenant une meilleure expérience utilisateur sans ajouter de travail manuel à notre processus de création de contenu.\n\n\u003Cfigure>\n  \u003Cvideo autoplay loop muted playsinline>\n    \u003Csource src=\"https:\u002F\u002Fimages.soubiran.dev\u002Fposts\u002Ferase-cls-by-automatically-optimizing-images-with-vite\u002Foptimized-image.mp4\" type=\"video\u002Fmp4\">\n  \u003C\u002Fvideo>\n  \u003Cfigcaption>Exemple d'image optimisée avec espace réservé et chargement différé.\u003C\u002Ffigcaption>\n\u003C\u002Ffigure>\n\nLorsque 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.\n\n## Observer le pipeline\n\nNous pouvons observer comment notre plugin interagit avec le pipeline Vite en utilisant [vite-plugin-inspect](https:\u002F\u002Fgithub.com\u002Fantfu-collective\u002Fvite-plugin-inspect).\n\n```sh\npnpm add -D vite-plugin-inspect\n```\n\nEnsuite, ajoutez-le au fichier `vite.config.ts` :\n\n```ts [vite.config.ts]\nimport { defineConfig } from 'vite'\nimport inspect from 'vite-plugin-inspect'\n\nexport default defineConfig({\n  plugins: [\n    inspect()\n\n    \u002F\u002F ...\n  ]\n})\n```\n\nEnsuite, lancez le serveur de développement :\n\n```sh\npnpm dev\n```\n\nEt ouvrez le panneau d'inspection à `http:\u002F\u002Flocalhost:5173\u002F__inspect\u002F`.\n\nVous 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.\n\n![Vite Plugin Inspect - Étapes de transformation d'image](\u002Fposts\u002Ferase-cls-by-automatically-optimizing-images-with-vite\u002Fvite-plugin-inspect.webp)\n\n## Aller plus loin\n\nÀ travers cet exemple relativement simple mais utile, j'espère que vous avez appris la puissance des plugins Vite.\n\nUne 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.\n\nPour en savoir plus sur les plugins, je recommande vivement de lire le code source de plugins Vite populaires comme [vite-plugin-vue](https:\u002F\u002Fgithub.com\u002Fvitejs\u002Fvite-plugin-vue) ou [unplugin-icons](https:\u002F\u002Fgithub.com\u002Funplugin\u002Funplugin-icons) et [ceux de VitePress](https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fvitepress\u002Ftree\u002Fmain\u002Fsrc\u002Fnode\u002Fplugins). [vite-plugin-inspect](https:\u002F\u002Fgithub.com\u002Fantfu-collective\u002Fvite-plugin-inspect) aide vraiment à comprendre comment les plugins interagissent entre eux et modifient le code.\n\nDe 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.\n\nEn 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.\n\n---\n\nJ'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 !\n",null,{"title":5,"description":2653},{"loc":2656,"images":2661},[2662],{"loc":2591},"fr\u002Fposts\u002Ferase-cls-by-automatically-optimizing-images-with-vite","jsajhQBNt6EJEOo1Q1PMAxNYTF9QVsDDRERmz53u9BM",1790086903228]