diff --git a/index.html b/index.html
index 30c90af..a0cff56 100644
--- a/index.html
+++ b/index.html
@@ -33,16 +33,36 @@
-
+
diff --git a/public/sw.js b/public/sw.js
new file mode 100644
index 0000000..29a62f6
--- /dev/null
+++ b/public/sw.js
@@ -0,0 +1,76 @@
+// Service Worker pour mettre en cache les ressources statiques
+const CACHE_NAME = 'portfolio-v1';
+const STATIC_CACHE = 'portfolio-static-v1';
+
+// Ressources à mettre en cache immédiatement
+const STATIC_ASSETS = [
+ '/',
+ '/favicon.ico',
+ '/favicon.svg',
+ '/robots.txt',
+ // GTM sera mis en cache lors de la première visite
+];
+
+// Installer le service worker
+self.addEventListener('install', (event) => {
+ event.waitUntil(
+ caches.open(STATIC_CACHE).then((cache) => {
+ return cache.addAll(STATIC_ASSETS);
+ })
+ );
+ // Forcer l'activation immédiate
+ self.skipWaiting();
+});
+
+// Activer le service worker
+self.addEventListener('activate', (event) => {
+ event.waitUntil(
+ caches.keys().then((cacheNames) => {
+ return Promise.all(
+ cacheNames.map((cacheName) => {
+ if (cacheName !== STATIC_CACHE && cacheName !== CACHE_NAME) {
+ return caches.delete(cacheName);
+ }
+ })
+ );
+ })
+ );
+ self.clients.claim();
+});
+
+// Intercepter les requêtes
+self.addEventListener('fetch', (event) => {
+ const url = new URL(event.request.url);
+
+ // Stratégie Cache First pour les ressources statiques
+ if (event.request.method === 'GET' &&
+ (url.pathname.match(/\.(css|js|png|jpg|jpeg|webp|svg|woff|woff2|ttf|eot)$/i) ||
+ url.hostname === 'www.googletagmanager.com' ||
+ url.hostname === 'fonts.googleapis.com' ||
+ url.hostname === 'fonts.gstatic.com')) {
+
+ event.respondWith(
+ caches.match(event.request).then((cachedResponse) => {
+ if (cachedResponse) {
+ return cachedResponse;
+ }
+
+ return fetch(event.request).then((response) => {
+ // Ne mettre en cache que les réponses réussies
+ if (response.status === 200 && response.type === 'basic') {
+ const responseClone = response.clone();
+ caches.open(CACHE_NAME).then((cache) => {
+ cache.put(event.request, responseClone);
+ });
+ }
+ return response;
+ }).catch(() => {
+ // Fallback pour les ressources critiques
+ if (url.pathname.includes('gtm.js')) {
+ return new Response('', { status: 404 });
+ }
+ });
+ })
+ );
+ }
+});
diff --git a/src/App.tsx b/src/App.tsx
index 594f1a8..09fc00c 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { lazy, Suspense } from "react";
import { ThemeProvider } from "./contexts/ThemeContext";
+import { useServiceWorker } from "./hooks/useServiceWorker";
// Lazy load pages and heavy components for better performance
const Index = lazy(() => import("./pages/Index"));
@@ -38,21 +39,26 @@ const router = createBrowserRouter([
},
]);
-const App = () => (
-
-
-
-
-
-
-
-
- }>
-
-
-
-
-
-);
+const App = () => {
+ // Enregistrer le service worker pour la mise en cache
+ useServiceWorker();
+
+ return (
+
+
+
+
+
+
+
+
+ }>
+
+
+
+
+
+ );
+};
export default App;
diff --git a/src/hooks/useServiceWorker.ts b/src/hooks/useServiceWorker.ts
new file mode 100644
index 0000000..47b8bfd
--- /dev/null
+++ b/src/hooks/useServiceWorker.ts
@@ -0,0 +1,40 @@
+import { useEffect } from 'react';
+
+export const useServiceWorker = () => {
+ useEffect(() => {
+ if ('serviceWorker' in navigator && process.env.NODE_ENV === 'production') {
+ // Enregistrer le service worker après un petit délai pour ne pas bloquer le rendu
+ const registerSW = async () => {
+ try {
+ const registration = await navigator.serviceWorker.register('/sw.js', {
+ scope: '/'
+ });
+
+ registration.addEventListener('updatefound', () => {
+ const newWorker = registration.installing;
+ if (newWorker) {
+ newWorker.addEventListener('statechange', () => {
+ if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
+ // Nouvelle version disponible
+ console.log('New service worker available, consider refreshing the page');
+ }
+ });
+ }
+ });
+
+ console.log('Service Worker registered successfully');
+ } catch (error) {
+ console.log('Service Worker registration failed:', error);
+ }
+ };
+
+ // Attendre que la page soit interactive avant d'enregistrer le SW
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', registerSW);
+ } else {
+ // Petit délai pour ne pas bloquer le rendu initial
+ setTimeout(registerSW, 100);
+ }
+ }
+ }, []);
+};
diff --git a/vite.config.ts b/vite.config.ts
index 6fddb7a..1da9935 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -24,17 +24,30 @@ export default defineConfig(({ mode }) => ({
},
build: {
// Optimisations pour la production
- minify: 'esbuild', // Utiliser esbuild au lieu de terser (plus rapide, déjà inclus)
+ minify: 'terser', // Utiliser Terser pour une meilleure compression que esbuild
target: 'esnext', // Code plus moderne et plus petit
- cssMinify: true,
+ cssMinify: 'esbuild', // Garder esbuild pour CSS (plus rapide)
// 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', '@tsparticles/engine'],
+ // Split vendors pour améliorer le cache et réduire les tailles
+ manualChunks: (id) => {
+ // React et core
+ if (id.includes('react') || id.includes('react-dom') || id.includes('react-router')) {
+ return 'react-vendor';
+ }
+ // UI libraries
+ if (id.includes('framer-motion') || id.includes('lucide-react') || id.includes('@radix-ui')) {
+ return 'ui-vendor';
+ }
+ // Particles (lazy loaded anyway)
+ if (id.includes('@tsparticles') || id.includes('particles')) {
+ return 'particles';
+ }
+ // Autres node_modules dans un chunk séparé
+ if (id.includes('node_modules')) {
+ return 'vendor';
+ }
},
// Nommer les chunks de manière cohérente pour le cache
chunkFileNames: 'assets/js/[name]-[hash].js',
@@ -43,12 +56,23 @@ export default defineConfig(({ mode }) => ({
},
},
// Optimisation des assets
- assetsInlineLimit: 4096, // Images < 4kb seront inline en base64
- chunkSizeWarningLimit: 500, // Limite plus stricte pour éviter les gros bundles
+ assetsInlineLimit: 2048, // Réduire pour inliner moins d'assets
+ chunkSizeWarningLimit: 300, // Limite encore plus stricte
sourcemap: false, // Désactiver les sourcemaps en production
// Compression CSS supplémentaire
cssCodeSplit: true,
- // Minification supplémentaire
+ // Minification supplémentaire avec Terser
+ terserOptions: {
+ compress: {
+ drop_console: true, // Supprimer les console.log en production
+ drop_debugger: true,
+ pure_funcs: ['console.log', 'console.info', 'console.debug'],
+ },
+ mangle: {
+ safari10: true,
+ },
+ },
+ // Optimisations supplémentaires
reportCompressedSize: true,
},
}));