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:
chamikaJ
2025-07-07 12:41:23 +05:30
parent 26b47aac53
commit aa1fb1c6f5
7 changed files with 583 additions and 177 deletions

View File

@@ -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 };
};

View File

@@ -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>
)}