optimisation critical css

This commit is contained in:
kinou-p 2025-10-02 19:17:11 +02:00
parent 1d0870d0f9
commit f85e637e8b
8 changed files with 4 additions and 322 deletions

View File

@ -1,32 +1,6 @@
# 🌟 Portfolio d'Alexandre Pommier # 🌟 Portfolio d'Alexandre Pommier
Un portfolio moderne et interactif développé avec React, TypeScript et shadcn/ui, présentant m## 🛡️ Sécurité Un portfolio moderne et interactif développé avec React, TypeScript et shadcn/ui, présentant mes projets et compétences techniques.
Ce portfolio implémente des pratiques de sécurité avancées pour protéger contre les vulnérabilités web courantes :
- **Content Security Policy (CSP)** - Protection contre XSS
- **HSTS** - Forçage HTTPS avec preload
- **COOP/CORP/COEP** - Isolation cross-origin
- **X-Frame-Options** - Protection contre le clickjacking
- **Permissions Policy** - Contrôle des fonctionnalités du navigateur
Pour plus de détails, consultez [SECURITY.md](./SECURITY.md).
## ⚡ Performance
Optimisations avancées pour des Core Web Vitals excellents :
- **CSS Critique Inliné** - Élimination du blocage de rendu
- **Lazy Loading** - Chargement à la demande des composants et images
- **Code Splitting** - Bundles optimisés et chunking intelligent
- **Resource Hints** - Preconnect et DNS prefetch
- **Image Optimization** - WebP avec thumbnails générés automatiquement
**Scores Lighthouse cibles** : Performance 95+, Accessibilité 95+, Best Practices 95+, SEO 100
Pour plus de détails, consultez [PERFORMANCE.md](./PERFORMANCE.md).
## 📱 Responsive Design compétences techniques.
## 🚀 Aperçu ## 🚀 Aperçu

View File

@ -26,16 +26,7 @@
<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" />
<!-- Critical CSS inliné pour éviter le blocage du rendu --> <!-- Google Fonts - Optimisé avec display=swap -->
<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>

View File

@ -68,10 +68,6 @@ server {
expires 1y; expires 1y;
add_header Cache-Control "public, immutable"; add_header Cache-Control "public, immutable";
add_header Vary "Accept-Encoding"; add_header Vary "Accept-Encoding";
access_log off;
# Préchargement HTTP/2 Server Push (optionnel)
# http2_push_preload on;
} }
# Cache static assets - Images # Cache static assets - Images
@ -79,7 +75,6 @@ server {
expires 1y; expires 1y;
add_header Cache-Control "public, immutable"; add_header Cache-Control "public, immutable";
add_header Vary "Accept-Encoding"; add_header Vary "Accept-Encoding";
access_log off;
} }
# Cache static assets - CSS/JS avec hash (versionnés) # Cache static assets - CSS/JS avec hash (versionnés)
@ -87,10 +82,6 @@ server {
expires 1y; expires 1y;
add_header Cache-Control "public, immutable"; add_header Cache-Control "public, immutable";
add_header Vary "Accept-Encoding"; add_header Vary "Accept-Encoding";
access_log off;
# Précharger les ressources critiques
add_header Link "</assets/fonts/inter.woff2>; rel=preload; as=font; type=font/woff2; crossorigin" always;
} }
# Cache static assets - Fonts # Cache static assets - Fonts
@ -98,7 +89,6 @@ server {
expires 1y; expires 1y;
add_header Cache-Control "public, immutable"; add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin "*"; add_header Access-Control-Allow-Origin "*";
access_log off;
} }
# SPA fallback - toutes les routes vers index.html # SPA fallback - toutes les routes vers index.html

View File

@ -12,8 +12,7 @@
"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",

View File

@ -1,83 +0,0 @@
/**
* 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 };

View File

@ -1,63 +0,0 @@
/* 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;
}
}

View File

@ -1,121 +0,0 @@
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;
}
}
};
}

View File

@ -2,7 +2,6 @@ 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 }) => ({
@ -17,11 +16,7 @@ export default defineConfig(({ mode }) => ({
"0.0.0.0" "0.0.0.0"
] ]
}, },
plugins: [ plugins: [react(), mode === "development" && componentTagger()].filter(Boolean),
react(),
mode === "development" && componentTagger(),
mode === "production" && preloadCriticalAssets()
].filter(Boolean),
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),