Avec Astro et Svelte, les styles d’un composant peuvent disparaître du build sans la moindre erreur : sur une PWA de signalements, quatre écrans s’affichaient avec les styles par défaut du navigateur. Le compilateur Svelte produisait bien leur CSS ; c’est le découpage du CSS par chunk, l’option Vite build.cssCodeSplit, qui le perdait en route. Le correctif : cssCodeSplit: false, plus un script qui fait échouer le build si un composant a perdu tous ses styles. Versions déclarées : astro ^6.1.3, @astrojs/svelte ^8.0.4, svelte ^5.55.1. C’est la même application que celle où un îlot imbriqué dans un autre cassait l’affichage en dev.
Le symptôme : calendrier sur une colonne, boutons gris, coins carrés
En août 2026, les écrans Calendrier, Détail d’un signalement, Correction et Témoins avaient perdu leur mise en forme : un calendrier sur une seule colonne au lieu d’une grille, des boutons gris, des coins carrés. La police, elle, était la bonne. Les styles globaux arrivaient, pas ceux des composants.
Le build, lui, n’avait signalé aucune erreur.
Le CSS était produit, mais dans aucune feuille émise
Compilé isolément, CalendarView émet ses 2,3 ko de styles, display: grid compris. Ces règles n’atterrissaient pourtant dans aucune feuille du dossier dist : ni dans un fichier .css, ni injectées dans le JS.
build.cssCodeSplit est l’option de Vite qui extrait le CSS importé par chaque chunk JS dans un fichier à part, chargé avec ce chunk. Elle est active par défaut (doc Vite). Quelque part dans ce découpage, une partie du CSS des composants Svelte se perdait. Je n’ai pas isolé le mécanisme exact dans Vite : la mesure suffisait à trancher.
Mesure sur le build avant correctif : quatre composants sans aucun de leurs styles (CalendarView, ProfileMenu, ManagerNav, BrandMark) et une douzaine d’autres amputés, dont ReportDetail et WitnessView. Après correctif : 46 composants vérifiés, aucun perdu.
Un build vert ne prouve pas que le CSS est arrivé : le CSS d’un composant Svelte peut disparaître du bundle sans qu’aucun outil ne proteste.
Deux remèdes essayés, un retenu
| Option | Corrige le défaut ? | Coût |
|---|---|---|
compilerOptions.css: "injected" (Svelte) | oui | plus de feuille de styles : le rendu serveur des îlots s’affiche nu jusqu’à l’exécution du JS |
build.cssCodeSplit: false (Vite) | oui | une feuille commune au lieu d’un découpage, quelques dizaines de kilo-octets, l’essentiel se compresse |
Avec css: "injected", chaque composant injecte son CSS depuis le JS. Pour une app à îlots rendue côté serveur, c’est perdre l’intérêt du HTML pré-rendu : la page arriverait sans style. J’ai donc retenu cssCodeSplit: false :
// astro.config.mjs
export default defineConfig({
integrations: [svelte()],
vite: {
build: {
// Le découpage du CSS par chunk perdait en silence les styles
// de certains composants. Une feuille commune coûte quelques
// dizaines de kilo-octets, contre quatre écrans inutilisables.
cssCodeSplit: false,
},
},
});
Un contrôle qui fait échouer le build
Le défaut étant silencieux, il fallait un contrôle qui ne le soit pas. scripts/check-component-styles.mjs tourne après chaque build :
"build": "astro build && node scripts/check-component-styles.mjs"
Il recompile chaque composant avec svelte/compiler, extrait les classes de son CSS, et cherche si au moins une apparaît dans les feuilles de dist/_astro :
const emis = compile(source, { filename }).css?.code ?? "";
const classes = [
...new Set([...emis.matchAll(/\.([a-z][a-z0-9_-]{3,})/gi)].map((m) => m[1])),
].filter((c) => !c.startsWith("svelte-") && !c.startsWith("s-"));
if (!classes.some((c) => css.includes("." + c))) {
perdus.push(`${fichier} (${classes.length} classes)`);
}
Il ne juge que le cas non ambigu : un composant dont aucune classe n’est présente. Un composant partiellement amputé ne déclenche rien, car ses classes génériques (.actions, .muted) se retrouvent chez les autres et fausseraient le verdict. J’ai vérifié le contrôle dans les deux sens : il passe avec le correctif, il échoue sans.
Le CSS émis est-il bien servi ?
Le lendemain, deux écrans montés après une interaction ont été signalés sans style. Le défaut ne se reproduisait pas sur le build : les 29 pages à îlot liaient bien les deux feuilles émises. Mais le premier contrôle n’aurait rien vu. Il vérifie que le CSS existe quelque part dans dist, pas que la page qui monte le composant lie la feuille où il vit.
Le second contrôle fige l’invariant qui découle de cssCodeSplit: false : toute page qui embarque un îlot lie toutes les feuilles émises. Les pages sans élément astro-island sont ignorées.
for (const page of pagesHtml(distRacine)) {
const html = readFileSync(page, "utf8");
if (!html.includes("astro-island")) continue;
const manquantes = feuilles.filter((f) => !html.includes(f));
if (manquantes.length > 0) incompletes.push(`${page} → manque ${manquantes.join(", ")}`);
}
Mis en défaut volontairement, en retirant un lien d’une page : le second contrôle nomme la page et la feuille manquante, pendant que le premier reste vert.
⚠️ Le revers d’une feuille commune : un bloc <style is:global> écrit pour une seule page s’applique désormais partout.
Lessons learned
- Le CSS d’un composant Svelte peut disparaître du build Astro sans erreur. Seul un contrôle explicite après build le détecte.
cssCodeSplit: falsea corrigé la perte des styles de composants. Le coût : quelques dizaines de kilo-octets, compressibles.css: "injected"corrige aussi, mais vide le rendu serveur de ses styles. Pour des îlots pré-rendus, ce n’est pas un échange acceptable.- Vérifier que le CSS existe ne prouve pas qu’il est servi. Il faut aussi contrôler que chaque page à îlot lie les feuilles qui le contiennent.
- Un garde-fou se valide en le faisant échouer. Chaque contrôle a été mis en défaut volontairement avant d’être gardé.