From 8b9191d0963e652975964c21b4e2fac38195d06b Mon Sep 17 00:00:00 2001 From: kinou-p Date: Thu, 2 Oct 2025 18:57:03 +0200 Subject: [PATCH] perf: optimize JavaScript bundles and GTM loading - Switch to Terser minification for better compression - Add service worker for caching static assets including GTM - Optimize GTM loading with timeout and DOM ready check - Better chunk splitting with vendor separation - Remove console logs in production --- index.html | 38 +++++++++++++----- public/sw.js | 76 +++++++++++++++++++++++++++++++++++ src/App.tsx | 38 ++++++++++-------- src/hooks/useServiceWorker.ts | 40 ++++++++++++++++++ vite.config.ts | 44 +++++++++++++++----- 5 files changed, 201 insertions(+), 35 deletions(-) create mode 100644 public/sw.js create mode 100644 src/hooks/useServiceWorker.ts 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, }, }));