optimisation critical css
This commit is contained in:
parent
1d0870d0f9
commit
f85e637e8b
28
README.md
28
README.md
@ -1,32 +1,6 @@
|
||||
# 🌟 Portfolio d'Alexandre Pommier
|
||||
|
||||
Un portfolio moderne et interactif développé avec React, TypeScript et shadcn/ui, présentant m## 🛡️ Sécurité
|
||||
|
||||
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.
|
||||
Un portfolio moderne et interactif développé avec React, TypeScript et shadcn/ui, présentant mes projets et compétences techniques.
|
||||
|
||||
## 🚀 Aperçu
|
||||
|
||||
|
||||
11
index.html
11
index.html
@ -26,16 +26,7 @@
|
||||
<meta name="twitter:site" content="@lovable_dev" />
|
||||
<meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
||||
|
||||
<!-- 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 -->
|
||||
<!-- Google Fonts - Optimisé avec 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'">
|
||||
<noscript>
|
||||
|
||||
10
nginx.conf
10
nginx.conf
@ -68,10 +68,6 @@ server {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header Vary "Accept-Encoding";
|
||||
access_log off;
|
||||
|
||||
# Préchargement HTTP/2 Server Push (optionnel)
|
||||
# http2_push_preload on;
|
||||
}
|
||||
|
||||
# Cache static assets - Images
|
||||
@ -79,7 +75,6 @@ server {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header Vary "Accept-Encoding";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Cache static assets - CSS/JS avec hash (versionnés)
|
||||
@ -87,10 +82,6 @@ server {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
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
|
||||
@ -98,7 +89,6 @@ server {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# SPA fallback - toutes les routes vers index.html
|
||||
|
||||
@ -12,8 +12,7 @@
|
||||
"preview": "vite preview",
|
||||
"optimize-images": "node scripts/optimize-images.js",
|
||||
"create-thumbnails": "node scripts/create-thumbnails.js",
|
||||
"analyze": "node scripts/analyze-bundle.js",
|
||||
"extract-critical-css": "npm run build && node scripts/extract-critical-css.js"
|
||||
"analyze": "node scripts/analyze-bundle.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
|
||||
@ -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 };
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -2,7 +2,6 @@ import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import path from "path";
|
||||
import { componentTagger } from "lovable-tagger";
|
||||
import { preloadCriticalAssets } from "./vite-plugin-critical-css";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => ({
|
||||
@ -17,11 +16,7 @@ export default defineConfig(({ mode }) => ({
|
||||
"0.0.0.0"
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
mode === "development" && componentTagger(),
|
||||
mode === "production" && preloadCriticalAssets()
|
||||
].filter(Boolean),
|
||||
plugins: [react(), mode === "development" && componentTagger()].filter(Boolean),
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user