optimisation critical css
This commit is contained in:
parent
fc5bd0fa6d
commit
471f1c54b8
11
index.html
11
index.html
@ -26,7 +26,16 @@
|
|||||||
<meta name="twitter:site" content="@lovable_dev" />
|
<meta name="twitter:site" content="@lovable_dev" />
|
||||||
<meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
<meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
||||||
|
|
||||||
<!-- Google Fonts - Optimisé avec display=swap -->
|
<!-- Critical CSS inliné pour éviter le blocage du rendu -->
|
||||||
|
<style>
|
||||||
|
:root{--background:0 0% 100%;--foreground:222 47% 11%;--primary:217 91% 60%;--border:214 32% 91%}.dark{--background:0 0% 4%;--foreground:0 0% 98%;--primary:217 91% 60%;--border:0 0% 15%}*{margin:0;padding:0;box-sizing:border-box}html{scroll-behavior:smooth}body{background-color:hsl(var(--background));color:hsl(var(--foreground));font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Roboto',sans-serif;line-height:1.6;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#root{min-height:100vh}.wf-loading body{visibility:hidden}.loading-skeleton{background:linear-gradient(90deg,hsl(var(--background)) 25%,hsl(var(--border)) 50%,hsl(var(--background)) 75%);background-size:200% 100%;animation:loading 1.5s ease-in-out infinite}@keyframes loading{0%{background-position:200% 0}100%{background-position:-200% 0}}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- Preload critical fonts -->
|
||||||
|
<link rel="preload" as="font" type="font/woff2" href="https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hjp-Ek-_EeA.woff2" crossorigin>
|
||||||
|
<link rel="preload" as="font" type="font/woff2" href="https://fonts.gstatic.com/s/montserrat/v26/JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2" crossorigin>
|
||||||
|
|
||||||
|
<!-- Google Fonts - Chargement asynchrone pour ne pas bloquer le rendu -->
|
||||||
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@400;500;600;700&display=swap">
|
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@400;500;600;700&display=swap">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'">
|
||||||
<noscript>
|
<noscript>
|
||||||
|
|||||||
@ -12,7 +12,8 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"optimize-images": "node scripts/optimize-images.js",
|
"optimize-images": "node scripts/optimize-images.js",
|
||||||
"create-thumbnails": "node scripts/create-thumbnails.js",
|
"create-thumbnails": "node scripts/create-thumbnails.js",
|
||||||
"analyze": "node scripts/analyze-bundle.js"
|
"analyze": "node scripts/analyze-bundle.js",
|
||||||
|
"extract-critical-css": "npm run build && node scripts/extract-critical-css.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "^3.10.0",
|
||||||
|
|||||||
83
scripts/extract-critical-css.js
Normal file
83
scripts/extract-critical-css.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Script pour extraire le CSS critique
|
||||||
|
* Utilise Puppeteer pour analyser la page et extraire uniquement le CSS nécessaire au rendu initial
|
||||||
|
*
|
||||||
|
* Installation requise:
|
||||||
|
* npm install --save-dev puppeteer critical
|
||||||
|
*
|
||||||
|
* Utilisation:
|
||||||
|
* node scripts/extract-critical-css.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { generate } from 'critical';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
async function extractCriticalCSS() {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Extraction du CSS critique...');
|
||||||
|
|
||||||
|
const { css, html } = await generate({
|
||||||
|
// Chemin vers le fichier HTML de build
|
||||||
|
base: path.join(__dirname, '../dist/'),
|
||||||
|
src: 'index.html',
|
||||||
|
|
||||||
|
// Dimensions pour le CSS critique (above the fold)
|
||||||
|
dimensions: [
|
||||||
|
{
|
||||||
|
width: 375,
|
||||||
|
height: 667
|
||||||
|
}, // Mobile
|
||||||
|
{
|
||||||
|
width: 1920,
|
||||||
|
height: 1080
|
||||||
|
}, // Desktop
|
||||||
|
],
|
||||||
|
|
||||||
|
// Options d'extraction
|
||||||
|
inline: false, // Ne pas inliner automatiquement
|
||||||
|
extract: true,
|
||||||
|
|
||||||
|
// Ignorer les erreurs de ressources externes
|
||||||
|
ignore: {
|
||||||
|
atrule: ['@font-face', '@import'],
|
||||||
|
rule: [/^\.tsparticles/],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Pourcentage de couverture CSS
|
||||||
|
penthouse: {
|
||||||
|
timeout: 60000,
|
||||||
|
maxEmbeddedBase64Length: 1000,
|
||||||
|
renderWaitTime: 500,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ CSS critique extrait avec succès!');
|
||||||
|
console.log(`📊 Taille: ${(css.length / 1024).toFixed(2)} KB`);
|
||||||
|
|
||||||
|
// Sauvegarder le CSS critique
|
||||||
|
const fs = await import('fs');
|
||||||
|
const criticalPath = path.join(__dirname, '../src/critical.css');
|
||||||
|
|
||||||
|
// Ajouter un commentaire en haut du fichier
|
||||||
|
const header = `/* Critical CSS - Généré automatiquement le ${new Date().toISOString()} */\n/* Ne pas modifier manuellement - Utiliser npm run extract-critical-css */\n\n`;
|
||||||
|
|
||||||
|
fs.writeFileSync(criticalPath, header + css);
|
||||||
|
console.log(`💾 Sauvegardé dans: ${criticalPath}`);
|
||||||
|
|
||||||
|
return css;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur lors de l\'extraction du CSS critique:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exécuter si appelé directement
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
extractCriticalCSS();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { extractCriticalCSS };
|
||||||
63
src/critical.css
Normal file
63
src/critical.css
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/* Critical CSS - Above the fold content */
|
||||||
|
/* This CSS should be inlined in the HTML head to prevent render-blocking */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222 47% 11%;
|
||||||
|
--primary: 217 91% 60%;
|
||||||
|
--border: 214 32% 91%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 0 0% 4%;
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
--primary: 217 91% 60%;
|
||||||
|
--border: 0 0% 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: hsl(var(--background));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide content until fonts load to prevent FOUT */
|
||||||
|
.wf-loading body {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skeleton loader for initial render */
|
||||||
|
.loading-skeleton {
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
hsl(var(--background)) 25%,
|
||||||
|
hsl(var(--border)) 50%,
|
||||||
|
hsl(var(--background)) 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: loading 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
121
vite-plugin-critical-css.ts
Normal file
121
vite-plugin-critical-css.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { Plugin } from 'vite';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin Vite pour inliner le CSS critique dans le HTML
|
||||||
|
* Cela réduit le temps de First Contentful Paint (FCP) et Largest Contentful Paint (LCP)
|
||||||
|
*/
|
||||||
|
export function inlineCriticalCSS(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'inline-critical-css',
|
||||||
|
apply: 'build', // Appliqué uniquement en mode build
|
||||||
|
|
||||||
|
transformIndexHtml: {
|
||||||
|
order: 'post',
|
||||||
|
handler(html) {
|
||||||
|
// Lire le CSS critique
|
||||||
|
const criticalCSSPath = path.resolve(__dirname, 'src/critical.css');
|
||||||
|
|
||||||
|
if (!fs.existsSync(criticalCSSPath)) {
|
||||||
|
console.warn('⚠️ Fichier critical.css introuvable. Ignoré.');
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
const criticalCSS = fs.readFileSync(criticalCSSPath, 'utf-8');
|
||||||
|
|
||||||
|
// Minifier le CSS critique (simple)
|
||||||
|
const minifiedCSS = criticalCSS
|
||||||
|
.replace(/\/\*[\s\S]*?\*\//g, '') // Supprimer les commentaires
|
||||||
|
.replace(/\s+/g, ' ') // Réduire les espaces multiples
|
||||||
|
.replace(/\s*([{}:;,>+~])\s*/g, '$1') // Supprimer les espaces autour des symboles
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// Injecter le CSS critique dans le <head>
|
||||||
|
const criticalStyle = `<style>${minifiedCSS}</style>`;
|
||||||
|
|
||||||
|
// Insérer juste après la balise <head> ou avant la première <link>
|
||||||
|
const headEndIndex = html.indexOf('</head>');
|
||||||
|
if (headEndIndex !== -1) {
|
||||||
|
html = html.slice(0, headEndIndex) + criticalStyle + html.slice(headEndIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin pour précharger les ressources critiques
|
||||||
|
*/
|
||||||
|
export function preloadCriticalAssets(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'preload-critical-assets',
|
||||||
|
apply: 'build',
|
||||||
|
|
||||||
|
transformIndexHtml: {
|
||||||
|
order: 'post',
|
||||||
|
handler(html, ctx) {
|
||||||
|
const preloads: string[] = [];
|
||||||
|
|
||||||
|
// Trouver les chunks CSS et JS critiques
|
||||||
|
if (ctx.bundle) {
|
||||||
|
for (const [fileName, chunk] of Object.entries(ctx.bundle)) {
|
||||||
|
if (chunk.type === 'chunk' && chunk.isEntry) {
|
||||||
|
// Précharger le JS principal
|
||||||
|
preloads.push(`<link rel="preload" href="/${fileName}" as="script" crossorigin>`);
|
||||||
|
} else if (chunk.type === 'asset' && fileName.endsWith('.css')) {
|
||||||
|
// Précharger le CSS principal
|
||||||
|
preloads.push(`<link rel="preload" href="/${fileName}" as="style">`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insérer les preloads dans le <head>
|
||||||
|
if (preloads.length > 0) {
|
||||||
|
const headEndIndex = html.indexOf('</head>');
|
||||||
|
if (headEndIndex !== -1) {
|
||||||
|
html = html.slice(0, headEndIndex) + preloads.join('') + html.slice(headEndIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin pour charger les CSS non critiques de manière asynchrone
|
||||||
|
*/
|
||||||
|
export function asyncNonCriticalCSS(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'async-non-critical-css',
|
||||||
|
apply: 'build',
|
||||||
|
|
||||||
|
transformIndexHtml: {
|
||||||
|
order: 'post',
|
||||||
|
handler(html) {
|
||||||
|
// Convertir les <link rel="stylesheet"> en chargement asynchrone
|
||||||
|
html = html.replace(
|
||||||
|
/<link([^>]*rel=["']stylesheet["'][^>]*)>/gi,
|
||||||
|
(match, attrs) => {
|
||||||
|
// Ne pas modifier les liens externes (Google Fonts, etc.)
|
||||||
|
if (attrs.includes('fonts.googleapis.com')) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter media="print" onload="this.media='all'" pour chargement async
|
||||||
|
if (!attrs.includes('media=')) {
|
||||||
|
return `<link${attrs} media="print" onload="this.media='all'"><noscript><link${attrs}></noscript>`;
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import { defineConfig } from "vite";
|
|||||||
import react from "@vitejs/plugin-react-swc";
|
import react from "@vitejs/plugin-react-swc";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { componentTagger } from "lovable-tagger";
|
import { componentTagger } from "lovable-tagger";
|
||||||
|
import { preloadCriticalAssets } from "./vite-plugin-critical-css";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
@ -16,7 +17,11 @@ export default defineConfig(({ mode }) => ({
|
|||||||
"0.0.0.0"
|
"0.0.0.0"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
plugins: [react(), mode === "development" && componentTagger()].filter(Boolean),
|
plugins: [
|
||||||
|
react(),
|
||||||
|
mode === "development" && componentTagger(),
|
||||||
|
mode === "production" && preloadCriticalAssets()
|
||||||
|
].filter(Boolean),
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user