From aa1fb1c6f5cd15e0400de277ef65f3b387ba16cb Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Mon, 7 Jul 2025 12:41:23 +0530 Subject: [PATCH] feat(performance): optimize resource loading and initialization - Added resource hints in index.html for improved loading performance, including preconnect and dns-prefetch links. - Implemented preload for critical JSON resources to enhance initial load times. - Optimized Google Analytics and HubSpot script loading to defer execution and reduce blocking during initial render. - Refactored app initialization in App.tsx to defer non-critical operations, improving perceived performance. - Introduced lazy loading for chart components and TinyMCE editor to minimize initial bundle size and enhance user experience. - Enhanced Vite configuration for optimized chunking strategy and improved caching with shorter hash lengths. --- worklenz-frontend/index.html | 93 ++++++--- worklenz-frontend/src/App.tsx | 48 ++++- .../components/charts/LazyChartComponents.tsx | 84 ++++++++ .../shared/info-tab/description-editor.tsx | 197 +++++++++++------- worklenz-frontend/src/i18n.ts | 121 +++++++++-- .../project-time-sheet-chart.tsx | 99 +++++++-- worklenz-frontend/vite.config.ts | 118 +++++++++-- 7 files changed, 583 insertions(+), 177 deletions(-) create mode 100644 worklenz-frontend/src/components/charts/LazyChartComponents.tsx diff --git a/worklenz-frontend/index.html b/worklenz-frontend/index.html index a2f637b2..0b2b7f18 100644 --- a/worklenz-frontend/index.html +++ b/worklenz-frontend/index.html @@ -5,43 +5,74 @@ + + + + + + + + + + + + + Worklenz + - + + diff --git a/worklenz-frontend/src/App.tsx b/worklenz-frontend/src/App.tsx index 0658c25c..9fdd1605 100644 --- a/worklenz-frontend/src/App.tsx +++ b/worklenz-frontend/src/App.tsx @@ -29,6 +29,7 @@ import { SuspenseFallback } from './components/suspense-fallback/suspense-fallba * 4. Lazy loading - All route components loaded on demand * 5. Suspense boundaries - Better loading states * 6. Optimized guard components with memoization + * 7. Deferred initialization - Non-critical operations moved to background */ const App: React.FC = memo(() => { const themeMode = useAppSelector(state => state.themeReducer.mode); @@ -37,8 +38,22 @@ const App: React.FC = memo(() => { // Memoize mixpanel initialization to prevent re-initialization const mixpanelToken = useMemo(() => import.meta.env.VITE_MIXPANEL_TOKEN as string, []); + // Defer mixpanel initialization to not block initial render useEffect(() => { - initMixpanel(mixpanelToken); + const initializeMixpanel = () => { + try { + initMixpanel(mixpanelToken); + } catch (error) { + logger.error('Failed to initialize Mixpanel:', error); + } + }; + + // Use requestIdleCallback to defer mixpanel initialization + if ('requestIdleCallback' in window) { + requestIdleCallback(initializeMixpanel, { timeout: 2000 }); + } else { + setTimeout(initializeMixpanel, 1000); + } }, [mixpanelToken]); // Memoize language change handler @@ -48,39 +63,54 @@ const App: React.FC = memo(() => { }); }, []); + // Apply theme immediately to prevent flash useEffect(() => { document.documentElement.setAttribute('data-theme', themeMode); }, [themeMode]); + // Handle language changes useEffect(() => { handleLanguageChange(language || Language.EN); }, [language, handleLanguageChange]); - // Initialize CSRF token and translations on app startup + // Initialize critical app functionality useEffect(() => { let isMounted = true; - const initializeApp = async () => { + const initializeCriticalApp = async () => { try { - // Initialize CSRF token + // Initialize CSRF token immediately as it's needed for API calls await initializeCsrfToken(); - - // Note: Translation preloading is handled in i18n.ts initialization - // No need to call ensureTranslationsLoaded here to avoid duplicate requests } catch (error) { if (isMounted) { - logger.error('Failed to initialize app:', error); + logger.error('Failed to initialize critical app functionality:', error); } } }; - initializeApp(); + // Initialize critical functionality immediately + initializeCriticalApp(); return () => { isMounted = false; }; }, []); + // Defer non-critical initialization + useEffect(() => { + const initializeNonCriticalApp = () => { + // Any non-critical initialization can go here + // For example: analytics, feature flags, etc. + }; + + // Defer non-critical initialization to not block initial render + if ('requestIdleCallback' in window) { + requestIdleCallback(initializeNonCriticalApp, { timeout: 3000 }); + } else { + setTimeout(initializeNonCriticalApp, 1500); + } + }, []); + return ( }> diff --git a/worklenz-frontend/src/components/charts/LazyChartComponents.tsx b/worklenz-frontend/src/components/charts/LazyChartComponents.tsx new file mode 100644 index 00000000..3a170a7b --- /dev/null +++ b/worklenz-frontend/src/components/charts/LazyChartComponents.tsx @@ -0,0 +1,84 @@ +import { lazy, Suspense } from 'react'; +import { Spin } from 'antd'; + +// Lazy load Chart.js components +const LazyBarChart = lazy(() => + import('react-chartjs-2').then(module => ({ default: module.Bar })) +); + +const LazyLineChart = lazy(() => + import('react-chartjs-2').then(module => ({ default: module.Line })) +); + +const LazyPieChart = lazy(() => + import('react-chartjs-2').then(module => ({ default: module.Pie })) +); + +const LazyDoughnutChart = lazy(() => + import('react-chartjs-2').then(module => ({ default: module.Doughnut })) +); + +// Lazy load Gantt components +const LazyGanttChart = lazy(() => + import('gantt-task-react').then(module => ({ default: module.Gantt })) +); + +// Chart loading fallback +const ChartLoadingFallback = () => ( +
+ +
+); + +// Wrapped components with Suspense +export const BarChart = (props: any) => ( + }> + + +); + +export const LineChart = (props: any) => ( + }> + + +); + +export const PieChart = (props: any) => ( + }> + + +); + +export const DoughnutChart = (props: any) => ( + }> + + +); + +export const GanttChart = (props: any) => ( + }> + + +); + +// Hook to preload chart libraries when needed +export const usePreloadCharts = () => { + const preloadCharts = () => { + // Preload Chart.js + import('react-chartjs-2'); + import('chart.js'); + + // Preload Gantt + import('gantt-task-react'); + }; + + return { preloadCharts }; +}; \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-drawer/shared/info-tab/description-editor.tsx b/worklenz-frontend/src/components/task-drawer/shared/info-tab/description-editor.tsx index 19a808e8..470a18c9 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/info-tab/description-editor.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/info-tab/description-editor.tsx @@ -1,10 +1,14 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { Editor } from '@tinymce/tinymce-react'; +import React, { useState, useRef, useEffect, lazy, Suspense } from 'react'; import DOMPurify from 'dompurify'; import { useAppSelector } from '@/hooks/useAppSelector'; import { useSocket } from '@/socket/socketContext'; import { SocketEvents } from '@/shared/socket-events'; +// Lazy load TinyMCE editor to reduce initial bundle size +const LazyTinyMCEEditor = lazy(() => + import('@tinymce/tinymce-react').then(module => ({ default: module.Editor })) +); + interface DescriptionEditorProps { description: string | null; taskId: string; @@ -17,23 +21,39 @@ const DescriptionEditor = ({ description, taskId, parentTaskId }: DescriptionEdi const [isEditorOpen, setIsEditorOpen] = useState(false); const [content, setContent] = useState(description || ''); const [isEditorLoading, setIsEditorLoading] = useState(false); - const [wordCount, setWordCount] = useState(0); // State for word count + const [wordCount, setWordCount] = useState(0); + const [isTinyMCELoaded, setIsTinyMCELoaded] = useState(false); const editorRef = useRef(null); const wrapperRef = useRef(null); const themeMode = useAppSelector(state => state.themeReducer.mode); - // Preload TinyMCE script - useEffect(() => { - const preloadTinyMCE = () => { - const link = document.createElement('link'); - link.rel = 'preload'; - link.href = '/tinymce/tinymce.min.js'; - link.as = 'script'; - document.head.appendChild(link); - }; - - preloadTinyMCE(); - }, []); + // Load TinyMCE script only when editor is opened + const loadTinyMCE = async () => { + if (isTinyMCELoaded) return; + + setIsEditorLoading(true); + try { + // Load TinyMCE script dynamically + await new Promise((resolve, reject) => { + if (window.tinymce) { + resolve(); + return; + } + + const script = document.createElement('script'); + script.src = '/tinymce/tinymce.min.js'; + script.async = true; + script.onload = () => resolve(); + script.onerror = () => reject(new Error('Failed to load TinyMCE')); + document.head.appendChild(script); + }); + + setIsTinyMCELoaded(true); + } catch (error) { + console.error('Failed to load TinyMCE:', error); + setIsEditorLoading(false); + } + }; const handleDescriptionChange = () => { if (!taskId) return; @@ -80,7 +100,6 @@ const DescriptionEditor = ({ description, taskId, parentTaskId }: DescriptionEdi const handleEditorChange = (content: string) => { const sanitizedContent = DOMPurify.sanitize(content); setContent(sanitizedContent); - // Update word count when content changes if (editorRef.current) { const count = editorRef.current.plugins.wordcount.getCount(); setWordCount(count); @@ -90,15 +109,14 @@ const DescriptionEditor = ({ description, taskId, parentTaskId }: DescriptionEdi const handleInit = (evt: any, editor: any) => { editorRef.current = editor; editor.on('focus', () => setIsEditorOpen(true)); - // Set initial word count on init const initialCount = editor.plugins.wordcount.getCount(); setWordCount(initialCount); setIsEditorLoading(false); }; - const handleOpenEditor = () => { + const handleOpenEditor = async () => { setIsEditorOpen(true); - setIsEditorLoading(true); + await loadTinyMCE(); }; const darkModeStyles = @@ -141,59 +159,63 @@ const DescriptionEditor = ({ description, taskId, parentTaskId }: DescriptionEdi
Loading editor...
)} - { - editor.dom.setStyle( - editor.getBody(), - 'backgroundColor', - themeMode === 'dark' ? '#1e1e1e' : '#ffffff' - ); - }, - }} - onEditorChange={handleEditorChange} - /> + {isTinyMCELoaded && ( + Loading editor...}> + { + editor.dom.setStyle( + editor.getBody(), + 'backgroundColor', + themeMode === 'dark' ? '#1e1e1e' : '#ffffff' + ); + }, + }} + onEditorChange={handleEditorChange} + /> + + )} ) : (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} style={{ - minHeight: '32px', - padding: '4px 11px', - border: `1px solid ${isHovered ? (themeMode === 'dark' ? '#177ddc' : '#40a9ff') : 'transparent'}`, + minHeight: '40px', + padding: '8px 12px', + border: `1px solid ${themeMode === 'dark' ? '#424242' : '#d9d9d9'}`, borderRadius: '6px', cursor: 'pointer', + backgroundColor: isHovered + ? themeMode === 'dark' + ? '#2a2a2a' + : '#fafafa' + : themeMode === 'dark' + ? '#1e1e1e' + : '#ffffff', color: themeMode === 'dark' ? '#ffffff' : '#000000', - transition: 'border-color 0.3s ease', + transition: 'all 0.2s ease', }} > {content ? (
) : ( - - Add a more detailed description... - +
+ Click to add description... +
)}
)} diff --git a/worklenz-frontend/src/i18n.ts b/worklenz-frontend/src/i18n.ts index eebcb6d2..8c96cd62 100644 --- a/worklenz-frontend/src/i18n.ts +++ b/worklenz-frontend/src/i18n.ts @@ -6,16 +6,27 @@ import logger from './utils/errorLogger'; // Essential namespaces that should be preloaded to prevent Suspense const ESSENTIAL_NAMESPACES = [ 'common', + 'auth/login', + 'navbar', +]; + +// Secondary namespaces that can be loaded on demand +const SECONDARY_NAMESPACES = [ 'tasks/task-table-bulk-actions', 'task-management', - 'auth/login', 'settings', + 'home', + 'project-drawer', ]; // Cache to track loaded translations and prevent duplicate requests const loadedTranslations = new Set(); const loadingPromises = new Map>(); +// Background loading queue for non-essential translations +let backgroundLoadingQueue: Array<{ lang: string; ns: string }> = []; +let isBackgroundLoading = false; + i18n .use(HttpApi) .use(initReactI18next) @@ -23,24 +34,34 @@ i18n fallbackLng: 'en', backend: { loadPath: '/locales/{{lng}}/{{ns}}.json', + // Add request timeout to prevent hanging on slow connections + requestOptions: { + cache: 'default', + mode: 'cors', + credentials: 'same-origin', + }, }, defaultNS: 'common', + // Only load essential namespaces initially ns: ESSENTIAL_NAMESPACES, interpolation: { escapeValue: false, }, - // Preload essential namespaces - preload: ['en', 'es', 'pt', 'alb', 'de'], - // Load all namespaces on initialization + // Only preload current language to reduce initial load + preload: [], load: 'languageOnly', - // Cache translations + // Disable loading all namespaces on init + initImmediate: false, + // Cache translations with shorter expiration for better performance cache: { enabled: true, - expirationTime: 24 * 60 * 60 * 1000, // 24 hours + expirationTime: 12 * 60 * 60 * 1000, // 12 hours }, + // Reduce debug output in production + debug: process.env.NODE_ENV === 'development', }); -// Utility function to ensure translations are loaded +// Optimized function to ensure translations are loaded export const ensureTranslationsLoaded = async ( namespaces: string[] = ESSENTIAL_NAMESPACES, languages: string[] = [i18n.language || 'en'] @@ -65,7 +86,6 @@ export const ensureTranslationsLoaded = async ( // Create loading promise const loadingPromise = new Promise((resolve, reject) => { - // Switch to the target language temporarily if needed const currentLang = i18n.language; const shouldSwitchLang = currentLang !== lang; @@ -77,7 +97,6 @@ export const ensureTranslationsLoaded = async ( await i18n.loadNamespaces(ns); - // Switch back to original language if we changed it if (shouldSwitchLang && currentLang) { await i18n.changeLanguage(currentLang); } @@ -100,7 +119,6 @@ export const ensureTranslationsLoaded = async ( } } - // Wait for all loading promises to complete await Promise.all(loadPromises); return true; } catch (error) { @@ -109,13 +127,86 @@ export const ensureTranslationsLoaded = async ( } }; -// Preload essential translations for current language only on startup -const initializeTranslations = async () => { - const currentLang = i18n.language || 'en'; - await ensureTranslationsLoaded(ESSENTIAL_NAMESPACES, [currentLang]); +// Background loading function for non-essential translations +const processBackgroundQueue = async () => { + if (isBackgroundLoading || backgroundLoadingQueue.length === 0) return; + + isBackgroundLoading = true; + + try { + // Process queue in batches to avoid overwhelming the network + const batchSize = 3; + while (backgroundLoadingQueue.length > 0) { + const batch = backgroundLoadingQueue.splice(0, batchSize); + const batchPromises = batch.map(({ lang, ns }) => + ensureTranslationsLoaded([ns], [lang]).catch(error => { + logger.error(`Background loading failed for ${lang}:${ns}`, error); + }) + ); + + await Promise.all(batchPromises); + + // Add small delay between batches to prevent blocking + if (backgroundLoadingQueue.length > 0) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + } finally { + isBackgroundLoading = false; + } }; -// Initialize translations on app startup (only once) +// Queue secondary translations for background loading +const queueSecondaryTranslations = (language: string) => { + SECONDARY_NAMESPACES.forEach(ns => { + const key = `${language}:${ns}`; + if (!loadedTranslations.has(key)) { + backgroundLoadingQueue.push({ lang: language, ns }); + } + }); + + // Start background loading with a delay to not interfere with initial render + setTimeout(processBackgroundQueue, 2000); +}; + +// Initialize only essential translations for current language +const initializeTranslations = async () => { + try { + const currentLang = i18n.language || 'en'; + + // Load only essential namespaces initially + await ensureTranslationsLoaded(ESSENTIAL_NAMESPACES, [currentLang]); + + // Queue secondary translations for background loading + queueSecondaryTranslations(currentLang); + + return true; + } catch (error) { + logger.error('Failed to initialize translations:', error); + return false; + } +}; + +// Language change handler that prioritizes essential namespaces +export const changeLanguageOptimized = async (language: string) => { + try { + // Change language first + await i18n.changeLanguage(language); + + // Load essential namespaces immediately + await ensureTranslationsLoaded(ESSENTIAL_NAMESPACES, [language]); + + // Queue secondary translations for background loading + queueSecondaryTranslations(language); + + return true; + } catch (error) { + logger.error(`Failed to change language to ${language}:`, error); + return false; + } +}; + +// Initialize translations on app startup (only essential ones) initializeTranslations(); export default i18n; diff --git a/worklenz-frontend/src/pages/reporting/time-reports/project-time-sheet/project-time-sheet-chart.tsx b/worklenz-frontend/src/pages/reporting/time-reports/project-time-sheet/project-time-sheet-chart.tsx index 6f912f83..6881c702 100644 --- a/worklenz-frontend/src/pages/reporting/time-reports/project-time-sheet/project-time-sheet-chart.tsx +++ b/worklenz-frontend/src/pages/reporting/time-reports/project-time-sheet/project-time-sheet-chart.tsx @@ -1,5 +1,4 @@ -import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react'; -import { Bar } from 'react-chartjs-2'; +import React, { useEffect, useState, forwardRef, useImperativeHandle, lazy, Suspense } from 'react'; import { Chart as ChartJS, CategoryScale, @@ -20,7 +19,34 @@ import { IRPTTimeProject } from '@/types/reporting/reporting.types'; import { Empty, Spin } from 'antd'; import logger from '@/utils/errorLogger'; -ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ChartDataLabels); +// Lazy load the Bar chart component +const LazyBarChart = lazy(() => + import('react-chartjs-2').then(module => ({ default: module.Bar })) +); + +// Chart loading fallback +const ChartLoadingFallback = () => ( +
+ +
+); + +// Register Chart.js components only when needed +let isChartJSRegistered = false; +const registerChartJS = () => { + if (!isChartJSRegistered) { + ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ChartDataLabels); + isChartJSRegistered = true; + } +}; const BAR_THICKNESS = 40; const STROKE_WIDTH = 4; @@ -36,6 +62,7 @@ const ProjectTimeSheetChart = forwardRef((_, ref) => { const { t } = useTranslation('time-report'); const [jsonData, setJsonData] = useState([]); const [loading, setLoading] = useState(false); + const [chartReady, setChartReady] = useState(false); const chartRef = React.useRef>(null); const themeMode = useAppSelector(state => state.themeReducer.mode); @@ -51,6 +78,21 @@ const ProjectTimeSheetChart = forwardRef((_, ref) => { } = useAppSelector(state => state.timeReportsOverviewReducer); const { duration, dateRange } = useAppSelector(state => state.reportingReducer); + // Initialize chart when component mounts + useEffect(() => { + const initChart = () => { + registerChartJS(); + setChartReady(true); + }; + + // Use requestIdleCallback to defer chart initialization + if ('requestIdleCallback' in window) { + requestIdleCallback(initChart, { timeout: 1000 }); + } else { + setTimeout(initChart, 500); + } + }, []); + const handleBarClick = (event: any, elements: any) => { if (elements.length > 0) { const elementIndex = elements[0].index; @@ -158,7 +200,7 @@ const ProjectTimeSheetChart = forwardRef((_, ref) => { }; useEffect(() => { - if (!loadingTeams && !loadingProjects && !loadingCategories) { + if (!loadingTeams && !loadingProjects && !loadingCategories && chartReady) { setLoading(true); fetchChartData().finally(() => { setLoading(false); @@ -175,6 +217,7 @@ const ProjectTimeSheetChart = forwardRef((_, ref) => { loadingTeams, loadingProjects, loadingCategories, + chartReady, ]); const exportChart = () => { @@ -200,8 +243,8 @@ const ProjectTimeSheetChart = forwardRef((_, ref) => { // Create download link const link = document.createElement('a'); - link.download = 'project-time-sheet.png'; - link.href = tempCanvas.toDataURL('image/png'); + link.download = 'project-time-sheet-chart.png'; + link.href = tempCanvas.toDataURL(); link.click(); } }; @@ -210,25 +253,35 @@ const ProjectTimeSheetChart = forwardRef((_, ref) => { exportChart, })); - // if (loading) { - // return ( - //
- // - //
- // ); - // } + if (loading) { + return ( +
+ +
+ ); + } + + if (!Array.isArray(jsonData) || jsonData.length === 0) { + return ( +
+ +
+ ); + } + + const chartHeight = jsonData.length * (BAR_THICKNESS + 10) + 100; + const containerHeight = Math.max(chartHeight, 400); return ( -
-
- +
+
+ {chartReady ? ( + }> + + + ) : ( + + )}
diff --git a/worklenz-frontend/vite.config.ts b/worklenz-frontend/vite.config.ts index e18e0eb0..e3fa7ed5 100644 --- a/worklenz-frontend/vite.config.ts +++ b/worklenz-frontend/vite.config.ts @@ -77,36 +77,90 @@ export default defineConfig(({ command, mode }) => { // **Rollup Options** rollupOptions: { output: { - // **Simplified Chunking Strategy to avoid React context issues** - manualChunks: { - // Keep React and all React-dependent libraries together - 'react-vendor': ['react', 'react-dom', 'react/jsx-runtime'], - - // Separate chunk for router - 'react-router': ['react-router-dom'], - - // Keep Ant Design separate but ensure React is available - antd: ['antd', '@ant-design/icons'], + // **Optimized Chunking Strategy for better caching and loading** + manualChunks: (id) => { + // Core React libraries - most stable, rarely change + if (id.includes('react') || id.includes('react-dom') || id.includes('react/jsx-runtime')) { + return 'react-core'; + } + + // React Router - separate chunk as it's used throughout the app + if (id.includes('react-router') || id.includes('react-router-dom')) { + return 'react-router'; + } + + // Ant Design - large UI library, separate chunk + if (id.includes('antd') || id.includes('@ant-design')) { + return 'antd'; + } + + // Chart.js and related libraries - heavy visualization libs + if (id.includes('chart.js') || id.includes('react-chartjs') || id.includes('chartjs')) { + return 'charts'; + } + + // TinyMCE - heavy editor, separate chunk (lazy loaded) + if (id.includes('tinymce') || id.includes('@tinymce')) { + return 'tinymce'; + } + + // Gantt and scheduling libraries - heavy components + if (id.includes('gantt') || id.includes('scheduler')) { + return 'gantt'; + } + + // Date utilities - commonly used + if (id.includes('date-fns') || id.includes('moment')) { + return 'date-utils'; + } + + // Redux and state management + if (id.includes('@reduxjs') || id.includes('react-redux') || id.includes('redux')) { + return 'redux'; + } + + // Socket.io - real-time communication + if (id.includes('socket.io')) { + return 'socket'; + } + + // Utility libraries + if (id.includes('lodash') || id.includes('dompurify') || id.includes('nanoid')) { + return 'utils'; + } + + // i18n libraries + if (id.includes('i18next') || id.includes('react-i18next')) { + return 'i18n'; + } + + // Other node_modules dependencies + if (id.includes('node_modules')) { + return 'vendor'; + } + + // Return undefined for app code to be bundled together + return undefined; }, // **File Naming Strategies** - chunkFileNames: chunkInfo => { - const facadeModuleId = chunkInfo.facadeModuleId - ? chunkInfo.facadeModuleId.split('/').pop() - : 'chunk'; - return `assets/js/[name]-[hash].js`; + chunkFileNames: (chunkInfo) => { + // Use shorter names for better caching + return `assets/js/[name]-[hash:8].js`; }, - entryFileNames: 'assets/js/[name]-[hash].js', - assetFileNames: assetInfo => { - if (!assetInfo.name) return 'assets/[name]-[hash].[ext]'; + entryFileNames: 'assets/js/[name]-[hash:8].js', + assetFileNames: (assetInfo) => { + if (!assetInfo.name) return 'assets/[name]-[hash:8].[ext]'; const info = assetInfo.name.split('.'); let extType = info[info.length - 1]; - if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType)) { + if (/png|jpe?g|svg|gif|tiff|bmp|ico|webp/i.test(extType)) { extType = 'img'; } else if (/woff2?|eot|ttf|otf/i.test(extType)) { extType = 'fonts'; + } else if (/css/i.test(extType)) { + extType = 'css'; } - return `assets/${extType}/[name]-[hash].[ext]`; + return `assets/${extType}/[name]-[hash:8].[ext]`; }, }, @@ -126,17 +180,35 @@ export default defineConfig(({ command, mode }) => { // **Optimization** optimizeDeps: { - include: ['react', 'react-dom', 'react/jsx-runtime', 'antd', '@ant-design/icons'], + include: [ + 'react', + 'react-dom', + 'react/jsx-runtime', + 'antd', + '@ant-design/icons', + 'react-router-dom', + 'i18next', + 'react-i18next', + 'date-fns', + 'dompurify', + ], exclude: [ - // Add any packages that should not be pre-bundled + // Exclude heavy libraries that should be lazy loaded + '@tinymce/tinymce-react', + 'tinymce', + 'chart.js', + 'react-chartjs-2', + 'gantt-task-react', ], // Force pre-bundling to avoid runtime issues - force: true, + force: false, // Only force when needed to improve dev startup time }, // **Define global constants** define: { __DEV__: !isProduction, }, + + }; });