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.
This commit is contained in:
@@ -5,43 +5,74 @@
|
|||||||
<link rel="icon" href="./favicon.ico" />
|
<link rel="icon" href="./favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#2b2b2b" />
|
<meta name="theme-color" content="#2b2b2b" />
|
||||||
|
|
||||||
|
<!-- Resource hints for better loading performance -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link rel="dns-prefetch" href="https://www.googletagmanager.com" />
|
||||||
|
<link rel="dns-prefetch" href="https://js.hs-scripts.com" />
|
||||||
|
|
||||||
|
<!-- Preload critical resources -->
|
||||||
|
<link rel="preload" href="/locales/en/common.json" as="fetch" type="application/json" crossorigin />
|
||||||
|
<link rel="preload" href="/locales/en/auth/login.json" as="fetch" type="application/json" crossorigin />
|
||||||
|
<link rel="preload" href="/locales/en/navbar.json" as="fetch" type="application/json" crossorigin />
|
||||||
|
|
||||||
|
<!-- Optimized font loading with font-display: swap -->
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
|
media="print"
|
||||||
|
onload="this.media='all'"
|
||||||
/>
|
/>
|
||||||
|
<noscript>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
<title>Worklenz</title>
|
<title>Worklenz</title>
|
||||||
|
|
||||||
<!-- Environment configuration -->
|
<!-- Environment configuration -->
|
||||||
<script src="/env-config.js"></script>
|
<script src="/env-config.js"></script>
|
||||||
<!-- Google Analytics -->
|
|
||||||
|
<!-- Optimized Google Analytics with reduced blocking -->
|
||||||
<script>
|
<script>
|
||||||
// Function to initialize Google Analytics
|
// Function to initialize Google Analytics asynchronously
|
||||||
function initGoogleAnalytics() {
|
function initGoogleAnalytics() {
|
||||||
// Load the Google Analytics script
|
// Use requestIdleCallback to defer analytics loading
|
||||||
const script = document.createElement('script');
|
const loadAnalytics = () => {
|
||||||
script.async = true;
|
// Determine which tracking ID to use based on the environment
|
||||||
|
const isProduction =
|
||||||
|
window.location.hostname === 'worklenz.com' ||
|
||||||
|
window.location.hostname === 'app.worklenz.com';
|
||||||
|
|
||||||
// Determine which tracking ID to use based on the environment
|
const trackingId = isProduction ? 'G-XXXXXXXXXX' : 'G-3LM2HGWEXG'; // Open source tracking ID
|
||||||
const isProduction =
|
|
||||||
window.location.hostname === 'worklenz.com' ||
|
|
||||||
window.location.hostname === 'app.worklenz.com';
|
|
||||||
|
|
||||||
const trackingId = isProduction ? 'G-XXXXXXXXXX' : 'G-3LM2HGWEXG'; // Open source tracking ID
|
// Load the Google Analytics script
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.async = true;
|
||||||
|
script.src = `https://www.googletagmanager.com/gtag/js?id=${trackingId}`;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
|
||||||
script.src = `https://www.googletagmanager.com/gtag/js?id=${trackingId}`;
|
// Initialize Google Analytics
|
||||||
document.head.appendChild(script);
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag() {
|
||||||
|
dataLayer.push(arguments);
|
||||||
|
}
|
||||||
|
gtag('js', new Date());
|
||||||
|
gtag('config', trackingId);
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize Google Analytics
|
// Use requestIdleCallback if available, otherwise setTimeout
|
||||||
window.dataLayer = window.dataLayer || [];
|
if ('requestIdleCallback' in window) {
|
||||||
function gtag() {
|
requestIdleCallback(loadAnalytics, { timeout: 2000 });
|
||||||
dataLayer.push(arguments);
|
} else {
|
||||||
|
setTimeout(loadAnalytics, 1000);
|
||||||
}
|
}
|
||||||
gtag('js', new Date());
|
|
||||||
gtag('config', trackingId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize analytics
|
// Initialize analytics after a delay to not block initial render
|
||||||
initGoogleAnalytics();
|
initGoogleAnalytics();
|
||||||
|
|
||||||
// Function to show privacy notice
|
// Function to show privacy notice
|
||||||
@@ -98,14 +129,24 @@
|
|||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="./src/index.tsx"></script>
|
<script type="module" src="./src/index.tsx"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
// Load HubSpot script asynchronously and only for production
|
||||||
if (window.location.hostname === 'app.worklenz.com') {
|
if (window.location.hostname === 'app.worklenz.com') {
|
||||||
var hs = document.createElement('script');
|
// Use requestIdleCallback to defer HubSpot loading
|
||||||
hs.type = 'text/javascript';
|
const loadHubSpot = () => {
|
||||||
hs.id = 'hs-script-loader';
|
var hs = document.createElement('script');
|
||||||
hs.async = true;
|
hs.type = 'text/javascript';
|
||||||
hs.defer = true;
|
hs.id = 'hs-script-loader';
|
||||||
hs.src = '//js.hs-scripts.com/22348300.js';
|
hs.async = true;
|
||||||
document.body.appendChild(hs);
|
hs.defer = true;
|
||||||
|
hs.src = '//js.hs-scripts.com/22348300.js';
|
||||||
|
document.body.appendChild(hs);
|
||||||
|
};
|
||||||
|
|
||||||
|
if ('requestIdleCallback' in window) {
|
||||||
|
requestIdleCallback(loadHubSpot, { timeout: 3000 });
|
||||||
|
} else {
|
||||||
|
setTimeout(loadHubSpot, 2000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { SuspenseFallback } from './components/suspense-fallback/suspense-fallba
|
|||||||
* 4. Lazy loading - All route components loaded on demand
|
* 4. Lazy loading - All route components loaded on demand
|
||||||
* 5. Suspense boundaries - Better loading states
|
* 5. Suspense boundaries - Better loading states
|
||||||
* 6. Optimized guard components with memoization
|
* 6. Optimized guard components with memoization
|
||||||
|
* 7. Deferred initialization - Non-critical operations moved to background
|
||||||
*/
|
*/
|
||||||
const App: React.FC = memo(() => {
|
const App: React.FC = memo(() => {
|
||||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
@@ -37,8 +38,22 @@ const App: React.FC = memo(() => {
|
|||||||
// Memoize mixpanel initialization to prevent re-initialization
|
// Memoize mixpanel initialization to prevent re-initialization
|
||||||
const mixpanelToken = useMemo(() => import.meta.env.VITE_MIXPANEL_TOKEN as string, []);
|
const mixpanelToken = useMemo(() => import.meta.env.VITE_MIXPANEL_TOKEN as string, []);
|
||||||
|
|
||||||
|
// Defer mixpanel initialization to not block initial render
|
||||||
useEffect(() => {
|
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]);
|
}, [mixpanelToken]);
|
||||||
|
|
||||||
// Memoize language change handler
|
// Memoize language change handler
|
||||||
@@ -48,39 +63,54 @@ const App: React.FC = memo(() => {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Apply theme immediately to prevent flash
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.setAttribute('data-theme', themeMode);
|
document.documentElement.setAttribute('data-theme', themeMode);
|
||||||
}, [themeMode]);
|
}, [themeMode]);
|
||||||
|
|
||||||
|
// Handle language changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleLanguageChange(language || Language.EN);
|
handleLanguageChange(language || Language.EN);
|
||||||
}, [language, handleLanguageChange]);
|
}, [language, handleLanguageChange]);
|
||||||
|
|
||||||
// Initialize CSRF token and translations on app startup
|
// Initialize critical app functionality
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
|
|
||||||
const initializeApp = async () => {
|
const initializeCriticalApp = async () => {
|
||||||
try {
|
try {
|
||||||
// Initialize CSRF token
|
// Initialize CSRF token immediately as it's needed for API calls
|
||||||
await initializeCsrfToken();
|
await initializeCsrfToken();
|
||||||
|
|
||||||
// Note: Translation preloading is handled in i18n.ts initialization
|
|
||||||
// No need to call ensureTranslationsLoaded here to avoid duplicate requests
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isMounted) {
|
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 () => {
|
return () => {
|
||||||
isMounted = false;
|
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 (
|
return (
|
||||||
<Suspense fallback={<SuspenseFallback />}>
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
<ThemeWrapper>
|
<ThemeWrapper>
|
||||||
|
|||||||
@@ -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 = () => (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '300px',
|
||||||
|
background: '#fafafa',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #f0f0f0'
|
||||||
|
}}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wrapped components with Suspense
|
||||||
|
export const BarChart = (props: any) => (
|
||||||
|
<Suspense fallback={<ChartLoadingFallback />}>
|
||||||
|
<LazyBarChart {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const LineChart = (props: any) => (
|
||||||
|
<Suspense fallback={<ChartLoadingFallback />}>
|
||||||
|
<LazyLineChart {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const PieChart = (props: any) => (
|
||||||
|
<Suspense fallback={<ChartLoadingFallback />}>
|
||||||
|
<LazyPieChart {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DoughnutChart = (props: any) => (
|
||||||
|
<Suspense fallback={<ChartLoadingFallback />}>
|
||||||
|
<LazyDoughnutChart {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const GanttChart = (props: any) => (
|
||||||
|
<Suspense fallback={<ChartLoadingFallback />}>
|
||||||
|
<LazyGanttChart {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 };
|
||||||
|
};
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect, lazy, Suspense } from 'react';
|
||||||
import { Editor } from '@tinymce/tinymce-react';
|
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import { useSocket } from '@/socket/socketContext';
|
import { useSocket } from '@/socket/socketContext';
|
||||||
import { SocketEvents } from '@/shared/socket-events';
|
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 {
|
interface DescriptionEditorProps {
|
||||||
description: string | null;
|
description: string | null;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
@@ -17,23 +21,39 @@ const DescriptionEditor = ({ description, taskId, parentTaskId }: DescriptionEdi
|
|||||||
const [isEditorOpen, setIsEditorOpen] = useState<boolean>(false);
|
const [isEditorOpen, setIsEditorOpen] = useState<boolean>(false);
|
||||||
const [content, setContent] = useState<string>(description || '');
|
const [content, setContent] = useState<string>(description || '');
|
||||||
const [isEditorLoading, setIsEditorLoading] = useState<boolean>(false);
|
const [isEditorLoading, setIsEditorLoading] = useState<boolean>(false);
|
||||||
const [wordCount, setWordCount] = useState<number>(0); // State for word count
|
const [wordCount, setWordCount] = useState<number>(0);
|
||||||
|
const [isTinyMCELoaded, setIsTinyMCELoaded] = useState<boolean>(false);
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
|
|
||||||
// Preload TinyMCE script
|
// Load TinyMCE script only when editor is opened
|
||||||
useEffect(() => {
|
const loadTinyMCE = async () => {
|
||||||
const preloadTinyMCE = () => {
|
if (isTinyMCELoaded) return;
|
||||||
const link = document.createElement('link');
|
|
||||||
link.rel = 'preload';
|
setIsEditorLoading(true);
|
||||||
link.href = '/tinymce/tinymce.min.js';
|
try {
|
||||||
link.as = 'script';
|
// Load TinyMCE script dynamically
|
||||||
document.head.appendChild(link);
|
await new Promise<void>((resolve, reject) => {
|
||||||
};
|
if (window.tinymce) {
|
||||||
|
resolve();
|
||||||
preloadTinyMCE();
|
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 = () => {
|
const handleDescriptionChange = () => {
|
||||||
if (!taskId) return;
|
if (!taskId) return;
|
||||||
@@ -80,7 +100,6 @@ const DescriptionEditor = ({ description, taskId, parentTaskId }: DescriptionEdi
|
|||||||
const handleEditorChange = (content: string) => {
|
const handleEditorChange = (content: string) => {
|
||||||
const sanitizedContent = DOMPurify.sanitize(content);
|
const sanitizedContent = DOMPurify.sanitize(content);
|
||||||
setContent(sanitizedContent);
|
setContent(sanitizedContent);
|
||||||
// Update word count when content changes
|
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
const count = editorRef.current.plugins.wordcount.getCount();
|
const count = editorRef.current.plugins.wordcount.getCount();
|
||||||
setWordCount(count);
|
setWordCount(count);
|
||||||
@@ -90,15 +109,14 @@ const DescriptionEditor = ({ description, taskId, parentTaskId }: DescriptionEdi
|
|||||||
const handleInit = (evt: any, editor: any) => {
|
const handleInit = (evt: any, editor: any) => {
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
editor.on('focus', () => setIsEditorOpen(true));
|
editor.on('focus', () => setIsEditorOpen(true));
|
||||||
// Set initial word count on init
|
|
||||||
const initialCount = editor.plugins.wordcount.getCount();
|
const initialCount = editor.plugins.wordcount.getCount();
|
||||||
setWordCount(initialCount);
|
setWordCount(initialCount);
|
||||||
setIsEditorLoading(false);
|
setIsEditorLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenEditor = () => {
|
const handleOpenEditor = async () => {
|
||||||
setIsEditorOpen(true);
|
setIsEditorOpen(true);
|
||||||
setIsEditorLoading(true);
|
await loadTinyMCE();
|
||||||
};
|
};
|
||||||
|
|
||||||
const darkModeStyles =
|
const darkModeStyles =
|
||||||
@@ -141,59 +159,63 @@ const DescriptionEditor = ({ description, taskId, parentTaskId }: DescriptionEdi
|
|||||||
<div>Loading editor...</div>
|
<div>Loading editor...</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Editor
|
{isTinyMCELoaded && (
|
||||||
tinymceScriptSrc="/tinymce/tinymce.min.js"
|
<Suspense fallback={<div>Loading editor...</div>}>
|
||||||
value={content}
|
<LazyTinyMCEEditor
|
||||||
onInit={handleInit}
|
tinymceScriptSrc="/tinymce/tinymce.min.js"
|
||||||
licenseKey="gpl"
|
value={content}
|
||||||
init={{
|
onInit={handleInit}
|
||||||
height: 200,
|
licenseKey="gpl"
|
||||||
menubar: false,
|
init={{
|
||||||
branding: false,
|
height: 200,
|
||||||
plugins: [
|
menubar: false,
|
||||||
'advlist',
|
branding: false,
|
||||||
'autolink',
|
plugins: [
|
||||||
'lists',
|
'advlist',
|
||||||
'link',
|
'autolink',
|
||||||
'charmap',
|
'lists',
|
||||||
'preview',
|
'link',
|
||||||
'anchor',
|
'charmap',
|
||||||
'searchreplace',
|
'preview',
|
||||||
'visualblocks',
|
'anchor',
|
||||||
'code',
|
'searchreplace',
|
||||||
'fullscreen',
|
'visualblocks',
|
||||||
'insertdatetime',
|
'code',
|
||||||
'media',
|
'fullscreen',
|
||||||
'table',
|
'insertdatetime',
|
||||||
'code',
|
'media',
|
||||||
'wordcount', // Added wordcount
|
'table',
|
||||||
],
|
'code',
|
||||||
toolbar:
|
'wordcount',
|
||||||
'blocks |' +
|
],
|
||||||
'bold italic underline strikethrough | ' +
|
toolbar:
|
||||||
'bullist numlist | link | removeformat | help',
|
'blocks |' +
|
||||||
content_style: `
|
'bold italic underline strikethrough | ' +
|
||||||
body {
|
'bullist numlist | link | removeformat | help',
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
content_style: `
|
||||||
font-size: 14px;
|
body {
|
||||||
}
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
${darkModeStyles}
|
font-size: 14px;
|
||||||
`,
|
}
|
||||||
skin: themeMode === 'dark' ? 'oxide-dark' : 'oxide',
|
${darkModeStyles}
|
||||||
content_css: themeMode === 'dark' ? 'dark' : 'default',
|
`,
|
||||||
skin_url: `/tinymce/skins/ui/${themeMode === 'dark' ? 'oxide-dark' : 'oxide'}`,
|
skin: themeMode === 'dark' ? 'oxide-dark' : 'oxide',
|
||||||
content_css_cors: true,
|
content_css: themeMode === 'dark' ? 'dark' : 'default',
|
||||||
auto_focus: true,
|
skin_url: `/tinymce/skins/ui/${themeMode === 'dark' ? 'oxide-dark' : 'oxide'}`,
|
||||||
init_instance_callback: editor => {
|
content_css_cors: true,
|
||||||
editor.dom.setStyle(
|
auto_focus: true,
|
||||||
editor.getBody(),
|
init_instance_callback: editor => {
|
||||||
'backgroundColor',
|
editor.dom.setStyle(
|
||||||
themeMode === 'dark' ? '#1e1e1e' : '#ffffff'
|
editor.getBody(),
|
||||||
);
|
'backgroundColor',
|
||||||
},
|
themeMode === 'dark' ? '#1e1e1e' : '#ffffff'
|
||||||
}}
|
);
|
||||||
onEditorChange={handleEditorChange}
|
},
|
||||||
/>
|
}}
|
||||||
|
onEditorChange={handleEditorChange}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
@@ -201,24 +223,37 @@ const DescriptionEditor = ({ description, taskId, parentTaskId }: DescriptionEdi
|
|||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
style={{
|
style={{
|
||||||
minHeight: '32px',
|
minHeight: '40px',
|
||||||
padding: '4px 11px',
|
padding: '8px 12px',
|
||||||
border: `1px solid ${isHovered ? (themeMode === 'dark' ? '#177ddc' : '#40a9ff') : 'transparent'}`,
|
border: `1px solid ${themeMode === 'dark' ? '#424242' : '#d9d9d9'}`,
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
backgroundColor: isHovered
|
||||||
|
? themeMode === 'dark'
|
||||||
|
? '#2a2a2a'
|
||||||
|
: '#fafafa'
|
||||||
|
: themeMode === 'dark'
|
||||||
|
? '#1e1e1e'
|
||||||
|
: '#ffffff',
|
||||||
color: themeMode === 'dark' ? '#ffffff' : '#000000',
|
color: themeMode === 'dark' ? '#ffffff' : '#000000',
|
||||||
transition: 'border-color 0.3s ease',
|
transition: 'all 0.2s ease',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{content ? (
|
{content ? (
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}
|
dangerouslySetInnerHTML={{
|
||||||
style={{ color: 'inherit' }}
|
__html: DOMPurify.sanitize(content),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span style={{ color: themeMode === 'dark' ? '#666666' : '#bfbfbf' }}>
|
<div
|
||||||
Add a more detailed description...
|
style={{
|
||||||
</span>
|
color: themeMode === 'dark' ? '#888888' : '#999999',
|
||||||
|
fontStyle: 'italic',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Click to add description...
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -6,16 +6,27 @@ import logger from './utils/errorLogger';
|
|||||||
// Essential namespaces that should be preloaded to prevent Suspense
|
// Essential namespaces that should be preloaded to prevent Suspense
|
||||||
const ESSENTIAL_NAMESPACES = [
|
const ESSENTIAL_NAMESPACES = [
|
||||||
'common',
|
'common',
|
||||||
|
'auth/login',
|
||||||
|
'navbar',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Secondary namespaces that can be loaded on demand
|
||||||
|
const SECONDARY_NAMESPACES = [
|
||||||
'tasks/task-table-bulk-actions',
|
'tasks/task-table-bulk-actions',
|
||||||
'task-management',
|
'task-management',
|
||||||
'auth/login',
|
|
||||||
'settings',
|
'settings',
|
||||||
|
'home',
|
||||||
|
'project-drawer',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Cache to track loaded translations and prevent duplicate requests
|
// Cache to track loaded translations and prevent duplicate requests
|
||||||
const loadedTranslations = new Set<string>();
|
const loadedTranslations = new Set<string>();
|
||||||
const loadingPromises = new Map<string, Promise<any>>();
|
const loadingPromises = new Map<string, Promise<any>>();
|
||||||
|
|
||||||
|
// Background loading queue for non-essential translations
|
||||||
|
let backgroundLoadingQueue: Array<{ lang: string; ns: string }> = [];
|
||||||
|
let isBackgroundLoading = false;
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
.use(HttpApi)
|
.use(HttpApi)
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
@@ -23,24 +34,34 @@ i18n
|
|||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
backend: {
|
backend: {
|
||||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||||
|
// Add request timeout to prevent hanging on slow connections
|
||||||
|
requestOptions: {
|
||||||
|
cache: 'default',
|
||||||
|
mode: 'cors',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
defaultNS: 'common',
|
defaultNS: 'common',
|
||||||
|
// Only load essential namespaces initially
|
||||||
ns: ESSENTIAL_NAMESPACES,
|
ns: ESSENTIAL_NAMESPACES,
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
escapeValue: false,
|
||||||
},
|
},
|
||||||
// Preload essential namespaces
|
// Only preload current language to reduce initial load
|
||||||
preload: ['en', 'es', 'pt', 'alb', 'de'],
|
preload: [],
|
||||||
// Load all namespaces on initialization
|
|
||||||
load: 'languageOnly',
|
load: 'languageOnly',
|
||||||
// Cache translations
|
// Disable loading all namespaces on init
|
||||||
|
initImmediate: false,
|
||||||
|
// Cache translations with shorter expiration for better performance
|
||||||
cache: {
|
cache: {
|
||||||
enabled: true,
|
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 (
|
export const ensureTranslationsLoaded = async (
|
||||||
namespaces: string[] = ESSENTIAL_NAMESPACES,
|
namespaces: string[] = ESSENTIAL_NAMESPACES,
|
||||||
languages: string[] = [i18n.language || 'en']
|
languages: string[] = [i18n.language || 'en']
|
||||||
@@ -65,7 +86,6 @@ export const ensureTranslationsLoaded = async (
|
|||||||
|
|
||||||
// Create loading promise
|
// Create loading promise
|
||||||
const loadingPromise = new Promise<void>((resolve, reject) => {
|
const loadingPromise = new Promise<void>((resolve, reject) => {
|
||||||
// Switch to the target language temporarily if needed
|
|
||||||
const currentLang = i18n.language;
|
const currentLang = i18n.language;
|
||||||
const shouldSwitchLang = currentLang !== lang;
|
const shouldSwitchLang = currentLang !== lang;
|
||||||
|
|
||||||
@@ -77,7 +97,6 @@ export const ensureTranslationsLoaded = async (
|
|||||||
|
|
||||||
await i18n.loadNamespaces(ns);
|
await i18n.loadNamespaces(ns);
|
||||||
|
|
||||||
// Switch back to original language if we changed it
|
|
||||||
if (shouldSwitchLang && currentLang) {
|
if (shouldSwitchLang && currentLang) {
|
||||||
await i18n.changeLanguage(currentLang);
|
await i18n.changeLanguage(currentLang);
|
||||||
}
|
}
|
||||||
@@ -100,7 +119,6 @@ export const ensureTranslationsLoaded = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all loading promises to complete
|
|
||||||
await Promise.all(loadPromises);
|
await Promise.all(loadPromises);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -109,13 +127,86 @@ export const ensureTranslationsLoaded = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Preload essential translations for current language only on startup
|
// Background loading function for non-essential translations
|
||||||
const initializeTranslations = async () => {
|
const processBackgroundQueue = async () => {
|
||||||
const currentLang = i18n.language || 'en';
|
if (isBackgroundLoading || backgroundLoadingQueue.length === 0) return;
|
||||||
await ensureTranslationsLoaded(ESSENTIAL_NAMESPACES, [currentLang]);
|
|
||||||
|
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();
|
initializeTranslations();
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
|
import React, { useEffect, useState, forwardRef, useImperativeHandle, lazy, Suspense } from 'react';
|
||||||
import { Bar } from 'react-chartjs-2';
|
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
@@ -20,7 +19,34 @@ import { IRPTTimeProject } from '@/types/reporting/reporting.types';
|
|||||||
import { Empty, Spin } from 'antd';
|
import { Empty, Spin } from 'antd';
|
||||||
import logger from '@/utils/errorLogger';
|
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 = () => (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '400px',
|
||||||
|
background: '#fafafa',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #f0f0f0'
|
||||||
|
}}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 BAR_THICKNESS = 40;
|
||||||
const STROKE_WIDTH = 4;
|
const STROKE_WIDTH = 4;
|
||||||
@@ -36,6 +62,7 @@ const ProjectTimeSheetChart = forwardRef<ProjectTimeSheetChartRef>((_, ref) => {
|
|||||||
const { t } = useTranslation('time-report');
|
const { t } = useTranslation('time-report');
|
||||||
const [jsonData, setJsonData] = useState<IRPTTimeProject[]>([]);
|
const [jsonData, setJsonData] = useState<IRPTTimeProject[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [chartReady, setChartReady] = useState(false);
|
||||||
const chartRef = React.useRef<ChartJS<'bar', string[], unknown>>(null);
|
const chartRef = React.useRef<ChartJS<'bar', string[], unknown>>(null);
|
||||||
|
|
||||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
@@ -51,6 +78,21 @@ const ProjectTimeSheetChart = forwardRef<ProjectTimeSheetChartRef>((_, ref) => {
|
|||||||
} = useAppSelector(state => state.timeReportsOverviewReducer);
|
} = useAppSelector(state => state.timeReportsOverviewReducer);
|
||||||
const { duration, dateRange } = useAppSelector(state => state.reportingReducer);
|
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) => {
|
const handleBarClick = (event: any, elements: any) => {
|
||||||
if (elements.length > 0) {
|
if (elements.length > 0) {
|
||||||
const elementIndex = elements[0].index;
|
const elementIndex = elements[0].index;
|
||||||
@@ -158,7 +200,7 @@ const ProjectTimeSheetChart = forwardRef<ProjectTimeSheetChartRef>((_, ref) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loadingTeams && !loadingProjects && !loadingCategories) {
|
if (!loadingTeams && !loadingProjects && !loadingCategories && chartReady) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
fetchChartData().finally(() => {
|
fetchChartData().finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -175,6 +217,7 @@ const ProjectTimeSheetChart = forwardRef<ProjectTimeSheetChartRef>((_, ref) => {
|
|||||||
loadingTeams,
|
loadingTeams,
|
||||||
loadingProjects,
|
loadingProjects,
|
||||||
loadingCategories,
|
loadingCategories,
|
||||||
|
chartReady,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const exportChart = () => {
|
const exportChart = () => {
|
||||||
@@ -200,8 +243,8 @@ const ProjectTimeSheetChart = forwardRef<ProjectTimeSheetChartRef>((_, ref) => {
|
|||||||
|
|
||||||
// Create download link
|
// Create download link
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.download = 'project-time-sheet.png';
|
link.download = 'project-time-sheet-chart.png';
|
||||||
link.href = tempCanvas.toDataURL('image/png');
|
link.href = tempCanvas.toDataURL();
|
||||||
link.click();
|
link.click();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -210,25 +253,35 @@ const ProjectTimeSheetChart = forwardRef<ProjectTimeSheetChartRef>((_, ref) => {
|
|||||||
exportChart,
|
exportChart,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// if (loading) {
|
if (loading) {
|
||||||
// return (
|
return (
|
||||||
// <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
<div style={{ minHeight: MIN_HEIGHT }}>
|
||||||
// <Spin />
|
<Spin size="large" style={{ display: 'block', margin: '100px auto' }} />
|
||||||
// </div>
|
</div>
|
||||||
// );
|
);
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(jsonData) || jsonData.length === 0) {
|
||||||
|
return (
|
||||||
|
<div style={{ minHeight: MIN_HEIGHT }}>
|
||||||
|
<Empty description={t('noDataAvailable')} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartHeight = jsonData.length * (BAR_THICKNESS + 10) + 100;
|
||||||
|
const containerHeight = Math.max(chartHeight, 400);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{ minHeight: MIN_HEIGHT }}>
|
||||||
<div
|
<div style={{ height: `${containerHeight}px`, width: '100%' }}>
|
||||||
style={{
|
{chartReady ? (
|
||||||
maxWidth: `calc(100vw - ${SIDEBAR_WIDTH}px)`,
|
<Suspense fallback={<ChartLoadingFallback />}>
|
||||||
minWidth: 'calc(100vw - 260px)',
|
<LazyBarChart data={data} options={options} ref={chartRef} />
|
||||||
minHeight: MIN_HEIGHT,
|
</Suspense>
|
||||||
height: `${60 * data.labels.length}px`,
|
) : (
|
||||||
}}
|
<ChartLoadingFallback />
|
||||||
>
|
)}
|
||||||
<Bar data={data} options={options} ref={chartRef} />
|
|
||||||
</div>
|
</div>
|
||||||
<ProjectTimeLogDrawer />
|
<ProjectTimeLogDrawer />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -77,36 +77,90 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
// **Rollup Options**
|
// **Rollup Options**
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
// **Simplified Chunking Strategy to avoid React context issues**
|
// **Optimized Chunking Strategy for better caching and loading**
|
||||||
manualChunks: {
|
manualChunks: (id) => {
|
||||||
// Keep React and all React-dependent libraries together
|
// Core React libraries - most stable, rarely change
|
||||||
'react-vendor': ['react', 'react-dom', 'react/jsx-runtime'],
|
if (id.includes('react') || id.includes('react-dom') || id.includes('react/jsx-runtime')) {
|
||||||
|
return 'react-core';
|
||||||
// Separate chunk for router
|
}
|
||||||
'react-router': ['react-router-dom'],
|
|
||||||
|
// React Router - separate chunk as it's used throughout the app
|
||||||
// Keep Ant Design separate but ensure React is available
|
if (id.includes('react-router') || id.includes('react-router-dom')) {
|
||||||
antd: ['antd', '@ant-design/icons'],
|
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**
|
// **File Naming Strategies**
|
||||||
chunkFileNames: chunkInfo => {
|
chunkFileNames: (chunkInfo) => {
|
||||||
const facadeModuleId = chunkInfo.facadeModuleId
|
// Use shorter names for better caching
|
||||||
? chunkInfo.facadeModuleId.split('/').pop()
|
return `assets/js/[name]-[hash:8].js`;
|
||||||
: 'chunk';
|
|
||||||
return `assets/js/[name]-[hash].js`;
|
|
||||||
},
|
},
|
||||||
entryFileNames: 'assets/js/[name]-[hash].js',
|
entryFileNames: 'assets/js/[name]-[hash:8].js',
|
||||||
assetFileNames: assetInfo => {
|
assetFileNames: (assetInfo) => {
|
||||||
if (!assetInfo.name) return 'assets/[name]-[hash].[ext]';
|
if (!assetInfo.name) return 'assets/[name]-[hash:8].[ext]';
|
||||||
const info = assetInfo.name.split('.');
|
const info = assetInfo.name.split('.');
|
||||||
let extType = info[info.length - 1];
|
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';
|
extType = 'img';
|
||||||
} else if (/woff2?|eot|ttf|otf/i.test(extType)) {
|
} else if (/woff2?|eot|ttf|otf/i.test(extType)) {
|
||||||
extType = 'fonts';
|
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**
|
// **Optimization**
|
||||||
optimizeDeps: {
|
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: [
|
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 pre-bundling to avoid runtime issues
|
||||||
force: true,
|
force: false, // Only force when needed to improve dev startup time
|
||||||
},
|
},
|
||||||
|
|
||||||
// **Define global constants**
|
// **Define global constants**
|
||||||
define: {
|
define: {
|
||||||
__DEV__: !isProduction,
|
__DEV__: !isProduction,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user