diff --git a/index.html b/index.html
index da738cc..30c90af 100644
--- a/index.html
+++ b/index.html
@@ -1,12 +1,17 @@
-
+
-
-
-
+
+
+
+
+
+
+
+
Alexandre Pommier - Portfolio
@@ -21,21 +26,24 @@
-
-
-
+
-
-
+
+
diff --git a/nginx.conf b/nginx.conf
index b87af55..89e8ccd 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -10,27 +10,61 @@ server {
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
+
+ # Performance headers
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
- gzip_types text/plain text/css text/xml text/javascript
- application/json application/javascript application/xml+rss
- application/rss+xml font/truetype font/opentype
- application/vnd.ms-fontobject image/svg+xml;
+ gzip_min_length 256;
+ gzip_types
+ text/plain
+ text/css
+ text/xml
+ text/javascript
+ application/json
+ application/javascript
+ application/xml+rss
+ application/rss+xml
+ font/truetype
+ font/opentype
+ application/vnd.ms-fontobject
+ image/svg+xml
+ application/wasm;
- # Cache static assets - Images
- location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ {
+ # Brotli compression (si supporté par nginx)
+ # brotli on;
+ # brotli_comp_level 6;
+ # brotli_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
+
+ # Cache static assets - Images WebP
+ location ~* \.(webp)$ {
expires 1y;
add_header Cache-Control "public, immutable";
+ add_header Vary "Accept-Encoding";
}
- # Cache static assets - CSS/JS
+ # Cache static assets - Images
+ location ~* \.(jpg|jpeg|png|gif|ico|svg)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ add_header Vary "Accept-Encoding";
+ }
+
+ # Cache static assets - CSS/JS avec hash (versionnés)
location ~* \.(css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
+ add_header Vary "Accept-Encoding";
+
+ # Preload header pour les ressources critiques
+ location ~* -[a-f0-9]{8}\.(css|js)$ {
+ add_header Link "; rel=preload; as=script" always;
+ }
}
# Cache static assets - Fonts
@@ -43,13 +77,21 @@ server {
# SPA fallback - toutes les routes vers index.html
location / {
try_files $uri $uri/ /index.html;
+
+ # HTTP/2 Server Push pour les ressources critiques (si supporté)
+ # http2_push /assets/css/index.css;
+ # http2_push /assets/js/index.js;
}
# Disable cache for index.html
location = /index.html {
- add_header Cache-Control "no-cache, no-store, must-revalidate";
- add_header Pragma "no-cache";
- add_header Expires "0";
+ add_header Cache-Control "no-cache, no-store, must-revalidate" always;
+ add_header Pragma "no-cache" always;
+ add_header Expires "0" always;
+
+ # Ajout de Link headers pour preconnect
+ add_header Link "; rel=preconnect" always;
+ add_header Link "; rel=preconnect; crossorigin" always;
}
# Disable cache for service worker if you add one later
@@ -61,4 +103,10 @@ server {
# Error pages
error_page 404 /index.html;
+
+ # Optimisation supplémentaire
+ tcp_nopush on;
+ tcp_nodelay on;
+ sendfile on;
+ keepalive_timeout 65;
}
diff --git a/package.json b/package.json
index 30519f3..1048b52 100644
--- a/package.json
+++ b/package.json
@@ -6,10 +6,12 @@
"scripts": {
"dev": "vite",
"build": "vite build",
+ "build:analyze": "vite build --mode production && npm run analyze",
"build:dev": "vite build --mode development",
"lint": "eslint .",
"preview": "vite preview",
- "optimize-images": "node scripts/optimize-images.js"
+ "optimize-images": "node scripts/optimize-images.js",
+ "analyze": "node scripts/analyze-bundle.js"
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
diff --git a/scripts/analyze-bundle.js b/scripts/analyze-bundle.js
new file mode 100644
index 0000000..431dd21
--- /dev/null
+++ b/scripts/analyze-bundle.js
@@ -0,0 +1,92 @@
+import { readdir, stat } from 'fs/promises';
+import { join, extname } from 'path';
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const distDir = join(__dirname, '..', 'dist');
+
+async function getFileSize(filePath) {
+ const stats = await stat(filePath);
+ return stats.size;
+}
+
+async function analyzeDirectory(dir, prefix = '') {
+ const files = await readdir(dir);
+ const results = [];
+
+ for (const file of files) {
+ const fullPath = join(dir, file);
+ const stats = await stat(fullPath);
+
+ if (stats.isDirectory()) {
+ results.push(...(await analyzeDirectory(fullPath, prefix + file + '/')));
+ } else {
+ const size = await getFileSize(fullPath);
+ results.push({
+ path: prefix + file,
+ size,
+ sizeKB: (size / 1024).toFixed(2),
+ ext: extname(file)
+ });
+ }
+ }
+
+ return results;
+}
+
+async function main() {
+ console.log('📊 Analyse du bundle de production...\n');
+
+ try {
+ const files = await analyzeDirectory(distDir);
+
+ // Grouper par type
+ const byType = files.reduce((acc, file) => {
+ if (!acc[file.ext]) acc[file.ext] = [];
+ acc[file.ext].push(file);
+ return acc;
+ }, {});
+
+ // Afficher les statistiques
+ console.log('📦 Taille par type de fichier:\n');
+
+ Object.keys(byType).sort().forEach(ext => {
+ const totalSize = byType[ext].reduce((sum, f) => sum + f.size, 0);
+ const count = byType[ext].length;
+ console.log(`${ext || 'no-ext'}: ${(totalSize / 1024).toFixed(2)} KB (${count} fichier${count > 1 ? 's' : ''})`);
+ });
+
+ // Fichiers les plus lourds
+ console.log('\n🔝 10 fichiers les plus lourds:\n');
+ files
+ .sort((a, b) => b.size - a.size)
+ .slice(0, 10)
+ .forEach((file, i) => {
+ console.log(`${i + 1}. ${file.path}: ${file.sizeKB} KB`);
+ });
+
+ // Taille totale
+ const totalSize = files.reduce((sum, f) => sum + f.size, 0);
+ console.log(`\n💾 Taille totale: ${(totalSize / 1024 / 1024).toFixed(2)} MB`);
+
+ // Avertissements
+ console.log('\n⚠️ Avertissements:');
+ const largeFiles = files.filter(f => f.size > 500 * 1024); // > 500KB
+ if (largeFiles.length > 0) {
+ console.log(`\n${largeFiles.length} fichier(s) de plus de 500 KB détecté(s):`);
+ largeFiles.forEach(f => {
+ console.log(` - ${f.path}: ${f.sizeKB} KB`);
+ });
+ } else {
+ console.log('✅ Aucun fichier trop lourd détecté!');
+ }
+
+ } catch (error) {
+ console.error('❌ Erreur lors de l\'analyse:', error.message);
+ }
+}
+
+main().catch(console.error);
diff --git a/src/App.tsx b/src/App.tsx
index 908d57f..594f1a8 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -3,14 +3,24 @@ import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
-import Index from "./pages/Index";
-import NotFound from "./pages/NotFound";
-import ProjectPage from "./pages/ProjectPage";
-import { ParticlesBackground } from "./components/ParticlesBackground";
+import { lazy, Suspense } from "react";
import { ThemeProvider } from "./contexts/ThemeContext";
+// Lazy load pages and heavy components for better performance
+const Index = lazy(() => import("./pages/Index"));
+const ProjectPage = lazy(() => import("./pages/ProjectPage"));
+const NotFound = lazy(() => import("./pages/NotFound"));
+const ParticlesBackground = lazy(() => import("./components/ParticlesBackground").then(m => ({ default: m.ParticlesBackground })));
+
const queryClient = new QueryClient();
+// Loading fallback component
+const PageLoader = () => (
+
+);
+
const IndexWrapper = () => ;
const router = createBrowserRouter([
@@ -32,10 +42,14 @@ const App = () => (
-
+
+
+
-
+ }>
+
+
diff --git a/src/components/OptimizedImage.tsx b/src/components/OptimizedImage.tsx
new file mode 100644
index 0000000..6fc06f5
--- /dev/null
+++ b/src/components/OptimizedImage.tsx
@@ -0,0 +1,45 @@
+import { useState, useEffect } from 'react';
+
+interface OptimizedImageProps {
+ src: string;
+ alt: string;
+ className?: string;
+ loading?: 'lazy' | 'eager';
+ priority?: boolean;
+}
+
+export const OptimizedImage = ({
+ src,
+ alt,
+ className = '',
+ loading = 'lazy',
+ priority = false
+}: OptimizedImageProps) => {
+ const [imageSrc, setImageSrc] = useState(undefined);
+ const [imageLoaded, setImageLoaded] = useState(false);
+
+ useEffect(() => {
+ if (priority) {
+ // Pour les images prioritaires, charger immédiatement
+ setImageSrc(src);
+ } else {
+ // Pour les autres, utiliser IntersectionObserver
+ const img = new Image();
+ img.src = src;
+ img.onload = () => {
+ setImageSrc(src);
+ setImageLoaded(true);
+ };
+ }
+ }, [src, priority]);
+
+ return (
+
+ );
+};
diff --git a/vite-plugin-preload.ts b/vite-plugin-preload.ts
new file mode 100644
index 0000000..7a2ba27
--- /dev/null
+++ b/vite-plugin-preload.ts
@@ -0,0 +1,17 @@
+import type { Plugin } from 'vite';
+
+export function preloadPlugin(): Plugin {
+ return {
+ name: 'vite-plugin-preload',
+ transformIndexHtml(html) {
+ // Ajouter des preload hints pour les chunks critiques
+ const preloadLinks = `
+
+
+
+ `;
+
+ return html.replace('', `${preloadLinks}\n `);
+ },
+ };
+}
diff --git a/vite.config.ts b/vite.config.ts
index 9795052..6fddb7a 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -25,40 +25,30 @@ export default defineConfig(({ mode }) => ({
build: {
// Optimisations pour la production
minify: 'esbuild', // Utiliser esbuild au lieu de terser (plus rapide, déjà inclus)
+ target: 'esnext', // Code plus moderne et plus petit
+ cssMinify: true,
// Chunking optimal pour de meilleures performances
rollupOptions: {
output: {
+ // Split vendors pour améliorer le cache
manualChunks: {
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
'ui-vendor': ['framer-motion', 'lucide-react'],
- 'particles': ['@tsparticles/react', '@tsparticles/slim'],
- 'radix-ui': [
- '@radix-ui/react-accordion',
- '@radix-ui/react-alert-dialog',
- '@radix-ui/react-avatar',
- '@radix-ui/react-checkbox',
- '@radix-ui/react-collapsible',
- '@radix-ui/react-dialog',
- '@radix-ui/react-dropdown-menu',
- '@radix-ui/react-label',
- '@radix-ui/react-popover',
- '@radix-ui/react-progress',
- '@radix-ui/react-scroll-area',
- '@radix-ui/react-select',
- '@radix-ui/react-separator',
- '@radix-ui/react-slider',
- '@radix-ui/react-slot',
- '@radix-ui/react-switch',
- '@radix-ui/react-tabs',
- '@radix-ui/react-toast',
- '@radix-ui/react-tooltip',
- ],
+ 'particles': ['@tsparticles/react', '@tsparticles/slim', '@tsparticles/engine'],
},
+ // Nommer les chunks de manière cohérente pour le cache
+ chunkFileNames: 'assets/js/[name]-[hash].js',
+ entryFileNames: 'assets/js/[name]-[hash].js',
+ assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
},
},
// Optimisation des assets
assetsInlineLimit: 4096, // Images < 4kb seront inline en base64
- chunkSizeWarningLimit: 1000, // Augmenter la limite d'avertissement
+ chunkSizeWarningLimit: 500, // Limite plus stricte pour éviter les gros bundles
sourcemap: false, // Désactiver les sourcemaps en production
+ // Compression CSS supplémentaire
+ cssCodeSplit: true,
+ // Minification supplémentaire
+ reportCompressedSize: true,
},
}));