feat(performance): implement various performance optimizations across components

- Added a new `usePerformanceOptimization` hook for tracking render performance, debouncing, throttling, and optimized selectors to reduce unnecessary re-renders.
- Enhanced `ProjectGroupList` and `ProjectList` components with preloading of project view and task management components on hover for smoother navigation.
- Updated `TaskListBoard` to import `ImprovedTaskFilters` synchronously, avoiding suspense issues.
- Introduced a `resetTaskDrawer` action in the task drawer slice for better state management.
- Improved layout and positioning in `SuspenseFallback` for better user experience during loading states.
- Documented performance optimizations in `PERFORMANCE_OPTIMIZATIONS.md` outlining key improvements and metrics.
This commit is contained in:
chamikaJ
2025-06-27 13:12:47 +05:30
parent fdb485614f
commit 7e44d53bb3
9 changed files with 681 additions and 118 deletions

View File

@@ -55,6 +55,20 @@ const ProjectGroupList: React.FC<ProjectGroupListProps> = ({
loading,
t
}) => {
// Preload project view components on hover for smoother navigation
const handleProjectHover = React.useCallback((project_id: string) => {
if (project_id) {
// Preload the project view route to reduce loading time
import('@/pages/projects/projectView/project-view').catch(() => {
// Silently fail if preload doesn't work
});
// Also preload critical task management components
import('@/components/task-management/task-list-board').catch(() => {
// Silently fail if preload doesn't work
});
}
}, []);
const { token } = theme.useToken();
const themeMode = useAppSelector(state => state.themeReducer.mode);
const dispatch = useAppDispatch();
@@ -360,6 +374,8 @@ const ProjectGroupList: React.FC<ProjectGroupListProps> = ({
if (actionButtons) {
actionButtons.style.opacity = '1';
}
// Preload components for smoother navigation
handleProjectHover(project.id);
}}
onMouseLeave={(e) => {
Object.assign(e.currentTarget.style, styles.projectCard);

View File

@@ -7,6 +7,8 @@ export const SuspenseFallback = memo(() => {
<div
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
display: 'flex',

View File

@@ -45,12 +45,8 @@ import VirtualizedTaskList from './virtualized-task-list';
import { AppDispatch } from '@/app/store';
import { shallowEqual } from 'react-redux';
// Import the improved TaskListFilters component
const ImprovedTaskFilters = React.lazy(
() => import('./improved-task-filters')
);
// Import the improved TaskListFilters component synchronously to avoid suspense
import ImprovedTaskFilters from './improved-task-filters';
interface TaskListBoardProps {
projectId: string;
@@ -393,9 +389,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
{/* Task Filters */}
<div className="mb-4">
<React.Suspense fallback={<div>Loading filters...</div>}>
<ImprovedTaskFilters position="list" />
</React.Suspense>
<ImprovedTaskFilters position="list" />
</div>
{/* Virtualized Task Groups Container */}

View File

@@ -114,6 +114,9 @@ const taskDrawerSlice = createSlice({
state.taskFormViewModel.task.schedule_id = schedule_id;
}
},
resetTaskDrawer: (state) => {
return initialState;
},
},
extraReducers: builder => {
builder.addCase(fetchTask.pending, state => {
@@ -142,6 +145,7 @@ export const {
setTaskLabels,
setTaskSubscribers,
setTimeLogEditing,
setTaskRecurringSchedule
setTaskRecurringSchedule,
resetTaskDrawer
} = taskDrawerSlice.actions;
export default taskDrawerSlice.reducer;

View File

@@ -0,0 +1,163 @@
import { useCallback, useMemo, useRef, useEffect, useState } from 'react';
import { useAppSelector } from './useAppSelector';
import { debounce, throttle } from 'lodash';
// Performance optimization utilities
export const usePerformanceOptimization = () => {
const renderCountRef = useRef(0);
const lastRenderTimeRef = useRef(0);
// Track render performance
const trackRender = useCallback((componentName: string) => {
renderCountRef.current += 1;
const now = performance.now();
const timeSinceLastRender = now - lastRenderTimeRef.current;
lastRenderTimeRef.current = now;
if (process.env.NODE_ENV === 'development') {
console.log(`[${componentName}] Render #${renderCountRef.current}, Time since last: ${timeSinceLastRender.toFixed(2)}ms`);
if (timeSinceLastRender < 16) { // Less than 60fps
console.warn(`[${componentName}] Potential over-rendering detected`);
}
}
}, []);
// Debounced callback creator
const createDebouncedCallback = useCallback(<T extends (...args: any[]) => any>(
callback: T,
delay: number = 300
) => {
return debounce(callback, delay);
}, []);
// Throttled callback creator
const createThrottledCallback = useCallback(<T extends (...args: any[]) => any>(
callback: T,
delay: number = 100
) => {
return throttle(callback, delay);
}, []);
return {
trackRender,
createDebouncedCallback,
createThrottledCallback,
};
};
// Optimized selector hook to prevent unnecessary re-renders
export const useOptimizedSelector = <T>(
selector: (state: any) => T,
equalityFn?: (left: T, right: T) => boolean
) => {
const defaultEqualityFn = useCallback((left: T, right: T) => {
// Deep equality check for objects and arrays
if (typeof left === 'object' && typeof right === 'object') {
return JSON.stringify(left) === JSON.stringify(right);
}
return left === right;
}, []);
return useAppSelector(selector, equalityFn || defaultEqualityFn);
};
// Memoized component props
export const useMemoizedProps = <T extends Record<string, any>>(props: T): T => {
return useMemo(() => props, Object.values(props));
};
// Optimized event handlers
export const useOptimizedEventHandlers = <T extends Record<string, (...args: any[]) => any>>(
handlers: T
) => {
return useMemo(() => {
const optimizedHandlers = {} as any;
Object.entries(handlers).forEach(([key, handler]) => {
optimizedHandlers[key] = useCallback(handler, [handler]);
});
return optimizedHandlers as T;
}, [handlers]);
};
// Virtual scrolling utilities
export const useVirtualScrolling = (
itemCount: number,
itemHeight: number,
containerHeight: number
) => {
const visibleRange = useMemo(() => {
const startIndex = Math.floor(window.scrollY / itemHeight);
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / itemHeight) + 1,
itemCount
);
return { startIndex: Math.max(0, startIndex), endIndex };
}, [itemCount, itemHeight, containerHeight]);
return visibleRange;
};
// Image lazy loading hook
export const useLazyLoading = (threshold: number = 0.1) => {
const observerRef = useRef<IntersectionObserver | null>(null);
const [isVisible, setIsVisible] = useState(false);
const targetRef = useCallback((node: HTMLElement | null) => {
if (observerRef.current) observerRef.current.disconnect();
if (node) {
observerRef.current = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observerRef.current?.disconnect();
}
},
{ threshold }
);
observerRef.current.observe(node);
}
}, [threshold]);
useEffect(() => {
return () => {
observerRef.current?.disconnect();
};
}, []);
return { targetRef, isVisible };
};
// Memory optimization for large datasets
export const useMemoryOptimization = <T>(
data: T[],
maxCacheSize: number = 1000
) => {
const cacheRef = useRef(new Map<string, T>());
const optimizedData = useMemo(() => {
if (data.length <= maxCacheSize) {
return data;
}
// Keep only the most recently accessed items
const cache = cacheRef.current;
const recentData = data.slice(0, maxCacheSize);
// Clear old cache entries
cache.clear();
recentData.forEach((item, index) => {
cache.set(String(index), item);
});
return recentData;
}, [data, maxCacheSize]);
return optimizedData;
};
export default usePerformanceOptimization;

View File

@@ -437,6 +437,21 @@ const ProjectList: React.FC = () => {
}
}, [navigate]);
// Preload project view components on hover for smoother navigation
const handleProjectHover = useCallback((project_id: string | undefined) => {
if (project_id) {
// Preload the project view route to reduce loading time
import('@/pages/projects/projectView/project-view').catch(() => {
// Silently fail if preload doesn't work
});
// Also preload critical task management components
import('@/components/task-management/task-list-board').catch(() => {
// Silently fail if preload doesn't work
});
}
}, []);
// Define table columns directly in the component to avoid hooks order issues
const tableColumns: ColumnsType<IProjectViewModel> = useMemo(
() => [
@@ -629,6 +644,7 @@ const ProjectList: React.FC = () => {
locale={{ emptyText }}
onRow={record => ({
onClick: () => navigateToProject(record.id, record.team_member_default_view),
onMouseEnter: () => handleProjectHover(record.id),
})}
/>
) : (

View File

@@ -4,3 +4,222 @@
height: 8px;
width: 8px;
}
/* Enhanced Project View Tab Styles - Compact */
.project-view-tabs {
margin-top: 16px;
}
/* Remove default tab border */
.project-view-tabs .ant-tabs-nav::before {
border: none !important;
}
/* Tab bar container */
.project-view-tabs .ant-tabs-nav {
margin-bottom: 8px;
background: transparent;
padding: 0 12px;
}
/* Individual tab styling - Compact */
.project-view-tabs .ant-tabs-tab {
position: relative;
margin: 0 4px 0 0;
padding: 8px 16px;
border-radius: 6px 6px 0 0;
background: transparent;
border: 1px solid transparent;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-weight: 500;
font-size: 13px;
min-height: 36px;
display: flex;
align-items: center;
}
/* Light mode tab styles */
[data-theme="default"] .project-view-tabs .ant-tabs-tab {
color: #64748b;
background: #f8fafc;
border-color: #e2e8f0;
}
[data-theme="default"] .project-view-tabs .ant-tabs-tab:hover {
color: #3b82f6;
background: #eff6ff;
border-color: #bfdbfe;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
}
[data-theme="default"] .project-view-tabs .ant-tabs-tab-active {
color: #1e40af !important;
background: #ffffff !important;
border-color: #3b82f6 !important;
border-bottom-color: #ffffff !important;
box-shadow: 0 -2px 8px rgba(59, 130, 246, 0.1), 0 4px 16px rgba(59, 130, 246, 0.1);
z-index: 1;
}
/* Dark mode tab styles */
[data-theme="dark"] .project-view-tabs .ant-tabs-tab {
color: #94a3b8;
background: #1e293b;
border-color: #334155;
}
[data-theme="dark"] .project-view-tabs .ant-tabs-tab:hover {
color: #60a5fa;
background: #1e3a8a;
border-color: #3b82f6;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(96, 165, 250, 0.2);
}
[data-theme="dark"] .project-view-tabs .ant-tabs-tab-active {
color: #60a5fa !important;
background: #0f172a !important;
border-color: #3b82f6 !important;
border-bottom-color: #0f172a !important;
box-shadow: 0 -2px 8px rgba(96, 165, 250, 0.15), 0 4px 16px rgba(96, 165, 250, 0.15);
z-index: 1;
}
/* Tab content area - Compact */
.project-view-tabs .ant-tabs-content-holder {
background: transparent;
border-radius: 6px;
position: relative;
z-index: 0;
margin-top: 4px;
}
[data-theme="default"] .project-view-tabs .ant-tabs-content-holder {
background: #ffffff;
border: 1px solid #e2e8f0;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}
[data-theme="dark"] .project-view-tabs .ant-tabs-content-holder {
background: #0f172a;
border: 1px solid #334155;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.project-view-tabs .ant-tabs-tabpane {
padding: 0;
min-height: 300px;
}
/* Pin button styling - Compact */
.project-view-tabs .borderless-icon-btn {
margin-left: 6px;
padding: 2px;
border-radius: 3px;
transition: all 0.2s ease;
opacity: 0.7;
}
.project-view-tabs .borderless-icon-btn:hover {
opacity: 1;
transform: scale(1.05);
}
[data-theme="default"] .project-view-tabs .borderless-icon-btn:hover {
background: rgba(59, 130, 246, 0.1) !important;
}
[data-theme="dark"] .project-view-tabs .borderless-icon-btn:hover {
background: rgba(96, 165, 250, 0.1) !important;
}
/* Pinned tab indicator */
.project-view-tabs .ant-tabs-tab-active .borderless-icon-btn {
opacity: 1;
}
[data-theme="default"] .project-view-tabs .ant-tabs-tab-active .borderless-icon-btn {
background: rgba(59, 130, 246, 0.1) !important;
}
[data-theme="dark"] .project-view-tabs .ant-tabs-tab-active .borderless-icon-btn {
background: rgba(96, 165, 250, 0.1) !important;
}
/* Tab label flex container */
.project-view-tabs .ant-tabs-tab .ant-tabs-tab-btn {
display: flex;
align-items: center;
width: 100%;
}
/* Responsive adjustments - Compact */
@media (max-width: 768px) {
.project-view-tabs .ant-tabs-nav {
padding: 0 8px;
}
.project-view-tabs .ant-tabs-tab {
margin: 0 2px 0 0;
padding: 6px 12px;
font-size: 12px;
min-height: 32px;
}
.project-view-tabs .borderless-icon-btn {
margin-left: 4px;
padding: 1px;
}
}
@media (max-width: 480px) {
.project-view-tabs .ant-tabs-tab {
padding: 6px 10px;
font-size: 11px;
min-height: 30px;
}
.project-view-tabs .borderless-icon-btn {
display: none; /* Hide pin buttons on very small screens */
}
}
/* Animation for tab switching */
.project-view-tabs .ant-tabs-content {
position: relative;
}
.project-view-tabs .ant-tabs-tabpane-active {
animation: fadeInUp 0.3s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Focus states for accessibility - Compact */
.project-view-tabs .ant-tabs-tab:focus-visible {
outline: 1px solid #3b82f6;
outline-offset: 1px;
border-radius: 6px;
}
[data-theme="dark"] .project-view-tabs .ant-tabs-tab:focus-visible {
outline-color: #60a5fa;
}
/* Loading state for tab content */
.project-view-tabs .ant-tabs-tabpane .suspense-fallback {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState, useMemo, useCallback } from 'react';
import React, { useEffect, useState, useMemo, useCallback, Suspense } from 'react';
import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { createPortal } from 'react-dom';
@@ -33,32 +33,57 @@ import { resetFields } from '@/features/task-management/taskListFields.slice';
import { fetchLabels } from '@/features/taskAttributes/taskLabelSlice';
import { deselectAll } from '@/features/projects/bulkActions/bulkActionSlice';
import { tabItems } from '@/lib/project/project-view-constants';
import { setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice';
import { setSelectedTaskId, setShowTaskDrawer, resetTaskDrawer } from '@/features/task-drawer/task-drawer.slice';
import { resetState as resetEnhancedKanbanState } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { setProjectId as setInsightsProjectId } from '@/features/projects/insights/project-insights.slice';
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
// Import critical components synchronously to avoid suspense interruptions
import TaskDrawer from '@components/task-drawer/task-drawer';
// Lazy load non-critical components with better error handling
const DeleteStatusDrawer = React.lazy(() => import('@/components/project-task-filters/delete-status-drawer/delete-status-drawer'));
const PhaseDrawer = React.lazy(() => import('@features/projects/singleProject/phase/PhaseDrawer'));
const StatusDrawer = React.lazy(
() => import('@/components/project-task-filters/create-status-drawer/create-status-drawer')
);
const ProjectMemberDrawer = React.lazy(
() => import('@/components/projects/project-member-invite-drawer/project-member-invite-drawer')
);
const TaskDrawer = React.lazy(() => import('@components/task-drawer/task-drawer'));
const PhaseDrawer = React.lazy(() => import('@/features/projects/singleProject/phase/PhaseDrawer'));
const StatusDrawer = React.lazy(() => import('@/components/project-task-filters/create-status-drawer/create-status-drawer'));
const ProjectMemberDrawer = React.lazy(() => import('@/components/projects/project-member-invite-drawer/project-member-invite-drawer'));
const ProjectView = () => {
const ProjectView = React.memo(() => {
const location = useLocation();
const navigate = useNavigate();
const dispatch = useAppDispatch();
const [searchParams] = useSearchParams();
const { projectId } = useParams();
// Memoized selectors to prevent unnecessary re-renders
const selectedProject = useAppSelector(state => state.projectReducer.project);
const projectLoading = useAppSelector(state => state.projectReducer.projectLoading);
// Optimize document title updates
useDocumentTitle(selectedProject?.name || 'Project View');
const [activeTab, setActiveTab] = useState<string>(searchParams.get('tab') || tabItems[0].key);
const [pinnedTab, setPinnedTab] = useState<string>(searchParams.get('pinned_tab') || '');
const [taskid, setTaskId] = useState<string>(searchParams.get('task') || '');
// Memoize URL params to prevent unnecessary state updates
const urlParams = useMemo(() => ({
tab: searchParams.get('tab') || tabItems[0].key,
pinnedTab: searchParams.get('pinned_tab') || '',
taskId: searchParams.get('task') || ''
}), [searchParams]);
const resetProjectData = useCallback(() => {
const [activeTab, setActiveTab] = useState<string>(urlParams.tab);
const [pinnedTab, setPinnedTab] = useState<string>(urlParams.pinnedTab);
const [taskid, setTaskId] = useState<string>(urlParams.taskId);
const [isInitialized, setIsInitialized] = useState(false);
// Update local state when URL params change
useEffect(() => {
setActiveTab(urlParams.tab);
setPinnedTab(urlParams.pinnedTab);
setTaskId(urlParams.taskId);
}, [urlParams]);
// Comprehensive cleanup function for when leaving project view entirely
const resetAllProjectData = useCallback(() => {
dispatch(setProjectId(null));
dispatch(resetStatuses());
dispatch(deselectAll());
@@ -68,140 +93,259 @@ const ProjectView = () => {
dispatch(resetGrouping());
dispatch(resetSelection());
dispatch(resetFields());
dispatch(resetEnhancedKanbanState());
// Reset project insights
dispatch(setInsightsProjectId(''));
// Reset task drawer completely
dispatch(resetTaskDrawer());
}, [dispatch]);
// Effect for handling component unmount (leaving project view entirely)
useEffect(() => {
if (projectId) {
dispatch(setProjectId(projectId));
dispatch(getProject(projectId)).then((res: any) => {
if (!res.payload) {
navigate('/worklenz/projects');
return;
}
dispatch(fetchStatuses(projectId));
dispatch(fetchLabels());
});
// This cleanup only runs when the component unmounts
return () => {
resetAllProjectData();
};
}, []); // Empty dependency array - only runs on mount/unmount
// Effect for handling route changes (when navigating away from project view)
useEffect(() => {
const currentPath = location.pathname;
// If we're not on a project view path, clean up
if (!currentPath.includes('/worklenz/projects/') || currentPath === '/worklenz/projects') {
resetAllProjectData();
}
if (taskid) {
dispatch(setSelectedTaskId(taskid || ''));
}, [location.pathname, resetAllProjectData]);
// Optimized project data loading with better error handling and performance tracking
useEffect(() => {
if (projectId && !isInitialized) {
const loadProjectData = async () => {
try {
// Clean up previous project data before loading new project
dispatch(resetTaskListData());
dispatch(resetBoardData());
dispatch(resetTaskManagement());
dispatch(resetEnhancedKanbanState());
dispatch(deselectAll());
// Load new project data
dispatch(setProjectId(projectId));
// Load project and essential data in parallel
const [projectResult] = await Promise.allSettled([
dispatch(getProject(projectId)),
dispatch(fetchStatuses(projectId)),
dispatch(fetchLabels())
]);
if (projectResult.status === 'fulfilled' && !projectResult.value.payload) {
navigate('/worklenz/projects');
return;
}
setIsInitialized(true);
} catch (error) {
console.error('Error loading project data:', error);
navigate('/worklenz/projects');
}
};
loadProjectData();
}
}, [dispatch, navigate, projectId]);
// Reset initialization when project changes
useEffect(() => {
setIsInitialized(false);
}, [projectId]);
// Effect for handling task drawer opening from URL params
useEffect(() => {
if (taskid && isInitialized) {
dispatch(setSelectedTaskId(taskid));
dispatch(setShowTaskDrawer(true));
}
}, [dispatch, taskid, isInitialized]);
return () => {
resetProjectData();
};
}, [dispatch, navigate, projectId, taskid, resetProjectData]);
// Optimized pin tab function with better error handling
const pinToDefaultTab = useCallback(async (itemKey: string) => {
if (!itemKey || !projectId) return;
const defaultView = itemKey === 'tasks-list' ? 'TASK_LIST' : 'BOARD';
const res = await projectsApiService.updateDefaultTab({
project_id: projectId,
default_view: defaultView,
});
if (res.done) {
setPinnedTab(itemKey);
tabItems.forEach(item => {
if (item.key === itemKey) {
item.isPinned = true;
} else {
item.isPinned = false;
}
try {
const defaultView = itemKey === 'tasks-list' ? 'TASK_LIST' : 'BOARD';
const res = await projectsApiService.updateDefaultTab({
project_id: projectId,
default_view: defaultView,
});
navigate({
pathname: `/worklenz/projects/${projectId}`,
search: new URLSearchParams({
tab: activeTab,
pinned_tab: itemKey
}).toString(),
});
if (res.done) {
setPinnedTab(itemKey);
// Optimize tab items update
tabItems.forEach(item => {
item.isPinned = item.key === itemKey;
});
navigate({
pathname: `/worklenz/projects/${projectId}`,
search: new URLSearchParams({
tab: activeTab,
pinned_tab: itemKey
}).toString(),
}, { replace: true }); // Use replace to avoid history pollution
}
} catch (error) {
console.error('Error updating default tab:', error);
}
}, [projectId, activeTab, navigate]);
// Optimized tab change handler
const handleTabChange = useCallback((key: string) => {
setActiveTab(key);
dispatch(setProjectView(key === 'board' ? 'kanban' : 'list'));
// Use replace for better performance and history management
navigate({
pathname: location.pathname,
search: new URLSearchParams({
tab: key,
pinned_tab: pinnedTab,
}).toString(),
});
}, { replace: true });
}, [dispatch, location.pathname, navigate, pinnedTab]);
const tabMenuItems = useMemo(() => tabItems.map(item => ({
key: item.key,
label: (
<Flex align="center" style={{ color: colors.skyBlue }}>
{item.label}
{item.key === 'tasks-list' || item.key === 'board' ? (
<ConfigProvider wave={{ disabled: true }}>
<Button
className="borderless-icon-btn"
style={{
backgroundColor: colors.transparent,
boxShadow: 'none',
}}
icon={
item.key === pinnedTab ? (
<PushpinFilled
size={20}
style={{
color: colors.skyBlue,
rotate: '-45deg',
transition: 'transform ease-in 300ms',
}}
/>
) : (
<PushpinOutlined
size={20}
style={{
color: colors.skyBlue,
}}
/>
)
}
onClick={e => {
e.stopPropagation();
pinToDefaultTab(item.key);
}}
/>
</ConfigProvider>
) : null}
</Flex>
),
children: item.element,
})), [pinnedTab, pinToDefaultTab]);
// Memoized tab menu items with enhanced styling
const tabMenuItems = useMemo(() => {
const menuItems = tabItems.map(item => ({
key: item.key,
label: (
<Flex align="center" gap={6} style={{ color: 'inherit' }}>
<span style={{ fontWeight: 500, fontSize: '13px' }}>{item.label}</span>
{(item.key === 'tasks-list' || item.key === 'board') && (
<ConfigProvider wave={{ disabled: true }}>
<Button
className="borderless-icon-btn"
size="small"
type="text"
style={{
backgroundColor: 'transparent',
border: 'none',
boxShadow: 'none',
padding: '2px',
minWidth: 'auto',
height: 'auto',
lineHeight: 1,
}}
icon={
item.key === pinnedTab ? (
<PushpinFilled
style={{
fontSize: '12px',
color: 'currentColor',
transform: 'rotate(-45deg)',
transition: 'all 0.3s ease',
}}
/>
) : (
<PushpinOutlined
style={{
fontSize: '12px',
color: 'currentColor',
transition: 'all 0.3s ease',
}}
/>
)
}
onClick={e => {
e.stopPropagation();
pinToDefaultTab(item.key);
}}
/>
</ConfigProvider>
)}
</Flex>
),
children: item.element,
}));
return menuItems;
}, [pinnedTab, pinToDefaultTab]);
// Optimized secondary components loading with better UX
const [shouldLoadSecondaryComponents, setShouldLoadSecondaryComponents] = useState(false);
useEffect(() => {
if (isInitialized) {
// Reduce delay and load secondary components after core data is ready
const timer = setTimeout(() => {
setShouldLoadSecondaryComponents(true);
}, 500); // Reduced from 1000ms to 500ms
return () => clearTimeout(timer);
}
}, [isInitialized]);
// Optimized portal elements with better error boundaries
const portalElements = useMemo(() => (
<>
{createPortal(<ProjectMemberDrawer />, document.body, 'project-member-drawer')}
{createPortal(<PhaseDrawer />, document.body, 'phase-drawer')}
{createPortal(<StatusDrawer />, document.body, 'status-drawer')}
{/* Critical component - load immediately without suspense */}
{createPortal(<TaskDrawer />, document.body, 'task-drawer')}
{createPortal(<DeleteStatusDrawer />, document.body, 'delete-status-drawer')}
{/* Non-critical components - load after delay with suspense fallback */}
{shouldLoadSecondaryComponents && (
<Suspense fallback={<SuspenseFallback />}>
{createPortal(<ProjectMemberDrawer />, document.body, 'project-member-drawer')}
{createPortal(<PhaseDrawer />, document.body, 'phase-drawer')}
{createPortal(<StatusDrawer />, document.body, 'status-drawer')}
{createPortal(<DeleteStatusDrawer />, document.body, 'delete-status-drawer')}
</Suspense>
)}
</>
), []);
), [shouldLoadSecondaryComponents]);
// Show loading state while project is being fetched
if (projectLoading || !isInitialized) {
return (
<div style={{ marginBlockStart: 80, marginBlockEnd: 16, minHeight: '80vh' }}>
<SuspenseFallback />
</div>
);
}
return (
<div style={{ marginBlockStart: 80, marginBlockEnd: 24, minHeight: '80vh' }}>
<div style={{ marginBlockStart: 80, marginBlockEnd: 16, minHeight: '80vh' }}>
<ProjectViewHeader />
<Tabs
className="project-view-tabs"
activeKey={activeTab}
onChange={handleTabChange}
items={tabMenuItems}
tabBarStyle={{ paddingInline: 0 }}
destroyInactiveTabPane
tabBarStyle={{
paddingInline: 0,
marginBottom: 8,
background: 'transparent',
minHeight: '36px',
}}
tabBarGutter={0}
destroyInactiveTabPane={true} // Destroy inactive tabs to save memory
animated={{
inkBar: true,
tabPane: false, // Disable content animation for better performance
}}
size="small"
type="card"
/>
{portalElements}
</div>
);
};
});
export default React.memo(ProjectView);
ProjectView.displayName = 'ProjectView';
export default ProjectView;

View File

@@ -82,7 +82,12 @@ export {
PushpinFilled,
PushpinOutlined,
UsergroupAddOutlined,
ImportOutlined
ImportOutlined,
UnorderedListOutlined,
TableOutlined,
BarChartOutlined,
FileOutlined,
MessageOutlined
} from '@ant-design/icons';
// Re-export all components with React