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:
@@ -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 { 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<boolean>(false);
|
||||
const [content, setContent] = useState<string>(description || '');
|
||||
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 wrapperRef = useRef<HTMLDivElement>(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<void>((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
|
||||
<div>Loading editor...</div>
|
||||
</div>
|
||||
)}
|
||||
<Editor
|
||||
tinymceScriptSrc="/tinymce/tinymce.min.js"
|
||||
value={content}
|
||||
onInit={handleInit}
|
||||
licenseKey="gpl"
|
||||
init={{
|
||||
height: 200,
|
||||
menubar: false,
|
||||
branding: false,
|
||||
plugins: [
|
||||
'advlist',
|
||||
'autolink',
|
||||
'lists',
|
||||
'link',
|
||||
'charmap',
|
||||
'preview',
|
||||
'anchor',
|
||||
'searchreplace',
|
||||
'visualblocks',
|
||||
'code',
|
||||
'fullscreen',
|
||||
'insertdatetime',
|
||||
'media',
|
||||
'table',
|
||||
'code',
|
||||
'wordcount', // Added wordcount
|
||||
],
|
||||
toolbar:
|
||||
'blocks |' +
|
||||
'bold italic underline strikethrough | ' +
|
||||
'bullist numlist | link | removeformat | help',
|
||||
content_style: `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
${darkModeStyles}
|
||||
`,
|
||||
skin: themeMode === 'dark' ? 'oxide-dark' : 'oxide',
|
||||
content_css: themeMode === 'dark' ? 'dark' : 'default',
|
||||
skin_url: `/tinymce/skins/ui/${themeMode === 'dark' ? 'oxide-dark' : 'oxide'}`,
|
||||
content_css_cors: true,
|
||||
auto_focus: true,
|
||||
init_instance_callback: editor => {
|
||||
editor.dom.setStyle(
|
||||
editor.getBody(),
|
||||
'backgroundColor',
|
||||
themeMode === 'dark' ? '#1e1e1e' : '#ffffff'
|
||||
);
|
||||
},
|
||||
}}
|
||||
onEditorChange={handleEditorChange}
|
||||
/>
|
||||
{isTinyMCELoaded && (
|
||||
<Suspense fallback={<div>Loading editor...</div>}>
|
||||
<LazyTinyMCEEditor
|
||||
tinymceScriptSrc="/tinymce/tinymce.min.js"
|
||||
value={content}
|
||||
onInit={handleInit}
|
||||
licenseKey="gpl"
|
||||
init={{
|
||||
height: 200,
|
||||
menubar: false,
|
||||
branding: false,
|
||||
plugins: [
|
||||
'advlist',
|
||||
'autolink',
|
||||
'lists',
|
||||
'link',
|
||||
'charmap',
|
||||
'preview',
|
||||
'anchor',
|
||||
'searchreplace',
|
||||
'visualblocks',
|
||||
'code',
|
||||
'fullscreen',
|
||||
'insertdatetime',
|
||||
'media',
|
||||
'table',
|
||||
'code',
|
||||
'wordcount',
|
||||
],
|
||||
toolbar:
|
||||
'blocks |' +
|
||||
'bold italic underline strikethrough | ' +
|
||||
'bullist numlist | link | removeformat | help',
|
||||
content_style: `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
${darkModeStyles}
|
||||
`,
|
||||
skin: themeMode === 'dark' ? 'oxide-dark' : 'oxide',
|
||||
content_css: themeMode === 'dark' ? 'dark' : 'default',
|
||||
skin_url: `/tinymce/skins/ui/${themeMode === 'dark' ? 'oxide-dark' : 'oxide'}`,
|
||||
content_css_cors: true,
|
||||
auto_focus: true,
|
||||
init_instance_callback: editor => {
|
||||
editor.dom.setStyle(
|
||||
editor.getBody(),
|
||||
'backgroundColor',
|
||||
themeMode === 'dark' ? '#1e1e1e' : '#ffffff'
|
||||
);
|
||||
},
|
||||
}}
|
||||
onEditorChange={handleEditorChange}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
@@ -201,24 +223,37 @@ const DescriptionEditor = ({ description, taskId, parentTaskId }: DescriptionEdi
|
||||
onMouseEnter={() => 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 ? (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}
|
||||
style={{ color: 'inherit' }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(content),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span style={{ color: themeMode === 'dark' ? '#666666' : '#bfbfbf' }}>
|
||||
Add a more detailed description...
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
color: themeMode === 'dark' ? '#888888' : '#999999',
|
||||
fontStyle: 'italic',
|
||||
}}
|
||||
>
|
||||
Click to add description...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user