From 7b326e8ff0b53b35062dd70fa41b4564a743ecbb Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Wed, 25 Jun 2025 13:05:38 +0530 Subject: [PATCH] feat(project-view): optimize component loading and enhance configuration - Introduced lazy loading for project view components and chart components to reduce initial bundle size. - Centralized Ant Design imports in a new `antd-imports.ts` file for better tree-shaking and maintainability. - Updated project view header and task list components to utilize centralized imports, improving consistency and performance. - Enhanced project view constants to streamline component rendering and improve user experience. --- .../src/components/charts/chart-loader.tsx | 25 +++ .../src/lib/project/project-view-constants.ts | 50 ++--- .../projectView/project-view-header.tsx | 13 +- .../projects/projectView/project-view.tsx | 15 +- .../taskList/project-view-task-list.tsx | 2 +- worklenz-frontend/src/shared/antd-imports.ts | 198 ++++++++++++++++++ worklenz-frontend/vite.config.ts | 165 ++++++++++++--- 7 files changed, 389 insertions(+), 79 deletions(-) create mode 100644 worklenz-frontend/src/components/charts/chart-loader.tsx create mode 100644 worklenz-frontend/src/shared/antd-imports.ts diff --git a/worklenz-frontend/src/components/charts/chart-loader.tsx b/worklenz-frontend/src/components/charts/chart-loader.tsx new file mode 100644 index 00000000..e4790cde --- /dev/null +++ b/worklenz-frontend/src/components/charts/chart-loader.tsx @@ -0,0 +1,25 @@ +import React, { Suspense } from 'react'; +import { Spin } from 'antd'; + +// Lazy load chart components to reduce initial bundle size +const LazyBar = React.lazy(() => import('react-chartjs-2').then(module => ({ default: module.Bar }))); +const LazyDoughnut = React.lazy(() => import('react-chartjs-2').then(module => ({ default: module.Doughnut }))); + +interface ChartLoaderProps { + type: 'bar' | 'doughnut'; + data: any; + options?: any; + [key: string]: any; +} + +const ChartLoader: React.FC = ({ type, ...props }) => { + const ChartComponent = type === 'bar' ? LazyBar : LazyDoughnut; + + return ( + }> + + + ); +}; + +export default ChartLoader; \ No newline at end of file diff --git a/worklenz-frontend/src/lib/project/project-view-constants.ts b/worklenz-frontend/src/lib/project/project-view-constants.ts index 63416132..4b90676f 100644 --- a/worklenz-frontend/src/lib/project/project-view-constants.ts +++ b/worklenz-frontend/src/lib/project/project-view-constants.ts @@ -1,12 +1,12 @@ import React, { ReactNode } from 'react'; -import ProjectViewInsights from '@/pages/projects/projectView/insights/project-view-insights'; -import ProjectViewFiles from '@/pages/projects/projectView/files/project-view-files'; -import ProjectViewMembers from '@/pages/projects/projectView/members/project-view-members'; -import ProjectViewUpdates from '@/pages/projects/project-view-1/updates/project-view-updates'; -import ProjectViewTaskList from '@/pages/projects/projectView/taskList/project-view-task-list'; -import ProjectViewBoard from '@/pages/projects/projectView/board/project-view-board'; -import ProjectViewEnhancedTasks from '@/pages/projects/projectView/enhancedTasks/project-view-enhanced-tasks'; -import ProjectViewEnhancedBoard from '@/pages/projects/projectView/enhancedBoard/project-view-enhanced-board'; + +// Lazy load all project view components to reduce initial bundle size +const ProjectViewInsights = React.lazy(() => import('@/pages/projects/projectView/insights/project-view-insights')); +const ProjectViewFiles = React.lazy(() => import('@/pages/projects/projectView/files/project-view-files')); +const ProjectViewMembers = React.lazy(() => import('@/pages/projects/projectView/members/project-view-members')); +const ProjectViewUpdates = React.lazy(() => import('@/pages/projects/project-view-1/updates/project-view-updates')); +const ProjectViewEnhancedTasks = React.lazy(() => import('@/pages/projects/projectView/enhancedTasks/project-view-enhanced-tasks')); +const ProjectViewEnhancedBoard = React.lazy(() => import('@/pages/projects/projectView/enhancedBoard/project-view-enhanced-board')); // type of a tab items type TabItems = { @@ -28,57 +28,31 @@ export const tabItems: TabItems[] = [ }, { index: 1, - key: 'task-list-v1', - label: 'Task List v1', - isPinned: true, - element: React.createElement(ProjectViewTaskList), - }, - { - index: 2, key: 'board', label: 'Board', isPinned: true, element: React.createElement(ProjectViewEnhancedBoard), }, { - index: 3, - key: 'board-v1', - label: 'Board v1', - isPinned: true, - element: React.createElement(ProjectViewBoard), - }, - // { - // index: 3, - // key: 'workload', - // label: 'Workload', - // element: React.createElement(ProjectViewWorkload), - // }, - // { - // index: 4, - // key: 'roadmap', - // label: 'Roadmap', - // element: React.createElement(ProjectViewRoadmap), - // }, - { - index: 5, + index: 2, key: 'project-insights-member-overview', label: 'Insights', element: React.createElement(ProjectViewInsights), }, { - index: 6, + index: 3, key: 'all-attachments', label: 'Files', element: React.createElement(ProjectViewFiles), }, { - index: 7, + index: 4, key: 'members', label: 'Members', element: React.createElement(ProjectViewMembers), }, { - index: 8, + index: 5, key: 'updates', label: 'Updates', element: React.createElement(ProjectViewUpdates), diff --git a/worklenz-frontend/src/pages/projects/projectView/project-view-header.tsx b/worklenz-frontend/src/pages/projects/projectView/project-view-header.tsx index b2a17504..bb518e86 100644 --- a/worklenz-frontend/src/pages/projects/projectView/project-view-header.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/project-view-header.tsx @@ -1,4 +1,10 @@ -import { +import { + Button, + Dropdown, + Flex, + Tag, + Tooltip, + Typography, ArrowLeftOutlined, BellFilled, BellOutlined, @@ -9,10 +15,9 @@ import { SaveOutlined, SettingOutlined, SyncOutlined, - UsergroupAddOutlined, -} from '@ant-design/icons'; + UsergroupAddOutlined +} from '@/shared/antd-imports'; import { PageHeader } from '@ant-design/pro-components'; -import { Button, Dropdown, Flex, Tag, Tooltip, Typography } from 'antd'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; diff --git a/worklenz-frontend/src/pages/projects/projectView/project-view.tsx b/worklenz-frontend/src/pages/projects/projectView/project-view.tsx index e5a82177..764089f6 100644 --- a/worklenz-frontend/src/pages/projects/projectView/project-view.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/project-view.tsx @@ -1,9 +1,20 @@ import React, { useEffect, useState, useMemo, useCallback } from 'react'; -import { PushpinFilled, PushpinOutlined, QuestionCircleOutlined } from '@ant-design/icons'; -import { Badge, Button, ConfigProvider, Flex, Tabs, TabsProps, Tooltip } from 'antd'; import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { createPortal } from 'react-dom'; +// Centralized Ant Design imports +import { + Button, + ConfigProvider, + Flex, + Tooltip, + Badge, + Tabs, + PushpinFilled, + PushpinOutlined, + type TabsProps +} from '@/shared/antd-imports'; + import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppSelector } from '@/hooks/useAppSelector'; import { getProject, setProjectId, setProjectView } from '@/features/project/project.slice'; diff --git a/worklenz-frontend/src/pages/projects/projectView/taskList/project-view-task-list.tsx b/worklenz-frontend/src/pages/projects/projectView/taskList/project-view-task-list.tsx index 45d169b8..753aeb98 100644 --- a/worklenz-frontend/src/pages/projects/projectView/taskList/project-view-task-list.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/taskList/project-view-task-list.tsx @@ -1,4 +1,5 @@ import { useEffect, useState, useMemo, lazy, Suspense } from 'react'; +import { Empty } from '@/shared/antd-imports'; import Flex from 'antd/es/flex'; import Skeleton from 'antd/es/skeleton'; import { useSearchParams } from 'react-router-dom'; @@ -10,7 +11,6 @@ import { useAppDispatch } from '@/hooks/useAppDispatch'; import { fetchTaskGroups, fetchTaskListColumns } from '@/features/tasks/tasks.slice'; import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice'; import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice'; -import { Empty } from 'antd'; import useTabSearchParam from '@/hooks/useTabSearchParam'; const ProjectViewTaskList = () => { diff --git a/worklenz-frontend/src/shared/antd-imports.ts b/worklenz-frontend/src/shared/antd-imports.ts new file mode 100644 index 00000000..00da2774 --- /dev/null +++ b/worklenz-frontend/src/shared/antd-imports.ts @@ -0,0 +1,198 @@ +/** + * Optimized Ant Design imports for maximum tree-shaking + * + * This file provides: + * - Granular imports from antd/es for better tree-shaking + * - Only commonly used components to reduce bundle size + * - Separate icon imports to avoid loading entire icon set + */ + +// Core Components - Import as default exports for better tree-shaking +import Button from 'antd/es/button'; +import Input from 'antd/es/input'; +import Select from 'antd/es/select'; +import Typography from 'antd/es/typography'; +import Card from 'antd/es/card'; +import Spin from 'antd/es/spin'; +import Empty from 'antd/es/empty'; +import Space from 'antd/es/space'; +import Tooltip from 'antd/es/tooltip'; +import Badge from 'antd/es/badge'; +import Popconfirm from 'antd/es/popconfirm'; +import Checkbox from 'antd/es/checkbox'; +import Dropdown from 'antd/es/dropdown'; +import Menu from 'antd/es/menu'; +import Modal from 'antd/es/modal'; +import Tag from 'antd/es/tag'; +import Avatar from 'antd/es/avatar'; +import List from 'antd/es/list'; +import Table from 'antd/es/table'; +import Flex from 'antd/es/flex'; +import Divider from 'antd/es/divider'; +import Progress from 'antd/es/progress'; +import Result from 'antd/es/result'; +import Skeleton from 'antd/es/skeleton'; +import Alert from 'antd/es/alert'; +import Tabs from 'antd/es/tabs'; +import ConfigProvider from 'antd/es/config-provider'; + +// Date & Time Components +import DatePicker from 'antd/es/date-picker'; +import TimePicker from 'antd/es/time-picker'; + +// Form Components +import Form from 'antd/es/form'; +import InputNumber from 'antd/es/input-number'; + +// Layout Components +import Row from 'antd/es/row'; +import Col from 'antd/es/col'; +import Layout from 'antd/es/layout'; +import Drawer from 'antd/es/drawer'; + +// Message and Notification - Import separately +import message from 'antd/es/message'; +import notification from 'antd/es/notification'; + +// Theme +import theme from 'antd/es/theme'; + +// Re-export all components +export { + Button, + Input, + Select, + Typography, + Card, + Spin, + Empty, + Space, + Tooltip, + Badge, + Popconfirm, + Checkbox, + Dropdown, + Menu, + Modal, + Tag, + Avatar, + List, + Table, + Flex, + Divider, + Progress, + Result, + Skeleton, + Alert, + Tabs, + ConfigProvider, + DatePicker, + TimePicker, + Form, + InputNumber, + Row, + Col, + Layout, + Drawer, + message, + notification, + theme +}; + +// Icons - Import only commonly used ones +export { + EditOutlined, + DeleteOutlined, + PlusOutlined, + MoreOutlined, + CheckOutlined, + CloseOutlined, + CalendarOutlined, + ClockCircleOutlined, + UserOutlined, + TeamOutlined, + TagOutlined, + BarsOutlined, + AppstoreOutlined, + FilterOutlined, + SearchOutlined, + SettingOutlined, + DownOutlined, + RightOutlined, + LeftOutlined, + UpOutlined, + ArrowLeftOutlined, + BellFilled, + BellOutlined, + SaveOutlined, + SyncOutlined, + PushpinFilled, + PushpinOutlined, + UsergroupAddOutlined, + ImportOutlined +} from '@ant-design/icons'; + +// TypeScript Types - Import only commonly used ones +export type { + ButtonProps, + InputProps, + InputRef, + SelectProps, + TypographyProps, + CardProps, + TooltipProps, + DropdownProps, + MenuProps, + DatePickerProps, + FormProps, + FormInstance, + FlexProps, + TabsProps, + TableProps, + TableColumnsType +} from 'antd'; + +// Dayjs +export { default as dayjs } from 'dayjs'; +export type { Dayjs } from 'dayjs'; + +// Optimized message utilities +export const appMessage = { + success: (content: string) => message.success(content), + error: (content: string) => message.error(content), + warning: (content: string) => message.warning(content), + info: (content: string) => message.info(content), + loading: (content: string) => message.loading(content), +}; + +export const appNotification = { + success: (config: any) => notification.success(config), + error: (config: any) => notification.error(config), + warning: (config: any) => notification.warning(config), + info: (config: any) => notification.info(config), +}; + +// Default configurations +export const antdConfig = { + datePickerDefaults: { + format: 'MMM DD, YYYY', + placeholder: 'Set Date', + size: 'small' as const, + }, + buttonDefaults: { + size: 'small' as const, + }, + inputDefaults: { + size: 'small' as const, + }, + selectDefaults: { + size: 'small' as const, + showSearch: true, + }, +}; + +export default { + config: antdConfig, + message: appMessage, + notification: appNotification, +}; \ No newline at end of file diff --git a/worklenz-frontend/vite.config.ts b/worklenz-frontend/vite.config.ts index a0e74df1..4dc7d087 100644 --- a/worklenz-frontend/vite.config.ts +++ b/worklenz-frontend/vite.config.ts @@ -20,16 +20,34 @@ export default defineConfig(({ command, mode }) => { { find: '@features', replacement: path.resolve(__dirname, './src/features') }, { find: '@assets', replacement: path.resolve(__dirname, './src/assets') }, { find: '@utils', replacement: path.resolve(__dirname, './src/utils') }, - { find: '@services', replacement: path.resolve(__dirname, './src/services') }, + { find: '@hooks', replacement: path.resolve(__dirname, './src/hooks') }, + { find: '@pages', replacement: path.resolve(__dirname, './src/pages') }, { find: '@api', replacement: path.resolve(__dirname, './src/api') }, + { find: '@types', replacement: path.resolve(__dirname, './src/types') }, + { find: '@shared', replacement: path.resolve(__dirname, './src/shared') }, + { find: '@layouts', replacement: path.resolve(__dirname, './src/layouts') }, + { find: '@services', replacement: path.resolve(__dirname, './src/services') }, ], // **Ensure single React instance** dedupe: ['react', 'react-dom'], }, + // **CSS Configuration** + css: { + preprocessorOptions: { + less: { + modifyVars: { + '@primary-color': '#1890ff', + }, + javascriptEnabled: true, + }, + }, + }, + // **Development Server** server: { port: 5173, + host: true, open: true, hmr: { overlay: false, @@ -47,43 +65,114 @@ export default defineConfig(({ command, mode }) => { cssCodeSplit: true, // **Sourcemaps** - sourcemap: !isProduction ? 'inline' : false, // Disable sourcemaps in production for smaller bundles + sourcemap: false, // **Minification** - minify: isProduction ? 'terser' : false, - terserOptions: isProduction ? { + minify: 'terser', + terserOptions: { compress: { - drop_console: true, - drop_debugger: true, - pure_funcs: ['console.log', 'console.info', 'console.debug'], - passes: 2, // Multiple passes for better compression + drop_console: isProduction, + drop_debugger: isProduction, }, - mangle: { - safari10: true, - }, - format: { - comments: false, - }, - } : undefined, + }, // **Chunk Size Warnings** chunkSizeWarningLimit: 1000, - // **Rollup Options** - rollupOptions: { - output: { - // **Simplified Chunking Strategy to avoid React context issues** - manualChunks: { - // Keep React and all React-dependent libraries together - 'react-vendor': ['react', 'react-dom', 'react/jsx-runtime'], + // **Rollup Options** + rollupOptions: { + output: { + // **Manual Chunking Strategy** + manualChunks: (id) => { + // Vendor libraries + if (id.includes('node_modules')) { + // Ant Design - Split into smaller chunks + if (id.includes('antd/es')) { + // Split antd by component types for better caching + if (id.includes('date-picker') || id.includes('time-picker') || id.includes('calendar')) { + return 'antd-date'; + } + if (id.includes('table') || id.includes('list') || id.includes('tree')) { + return 'antd-data'; + } + if (id.includes('form') || id.includes('input') || id.includes('select') || id.includes('checkbox')) { + return 'antd-form'; + } + if (id.includes('button') || id.includes('tooltip') || id.includes('dropdown') || id.includes('menu')) { + return 'antd-basic'; + } + if (id.includes('modal') || id.includes('drawer') || id.includes('popconfirm')) { + return 'antd-overlay'; + } + // Catch remaining antd components + return 'antd-misc'; + } - // Separate chunk for router - 'react-router': ['react-router-dom'], + // Icons + if (id.includes('@ant-design/icons')) { + return 'antd-icons'; + } - // Keep Ant Design separate but ensure React is available - 'antd': ['antd', '@ant-design/icons'], - }, - + // Chart.js and related + if (id.includes('chart.js') || id.includes('react-chartjs-2')) { + return 'charts'; + } + + // Redux and related state management + if (id.includes('@reduxjs/toolkit') || id.includes('react-redux')) { + return 'redux'; + } + + // React Router + if (id.includes('react-router')) { + return 'router'; + } + + // i18n + if (id.includes('react-i18next') || id.includes('i18next')) { + return 'i18n'; + } + + // Other large libraries + if (id.includes('lodash')) { + return 'lodash'; + } + + if (id.includes('dayjs')) { + return 'dayjs'; + } + + // Remaining vendor code + return 'vendor'; + } + + // Application code chunking + // Project view components + if (id.includes('src/pages/projects/projectView')) { + return 'project-view'; + } + + // Other project components + if (id.includes('src/pages/projects')) { + return 'projects'; + } + + // Task management components + if (id.includes('src/components/task-') || id.includes('src/features/tasks')) { + return 'tasks'; + } + + // Settings and admin components + if (id.includes('src/pages/settings') || id.includes('src/components/admin')) { + return 'admin'; + } + + // Common components + if (id.includes('src/components/common') || id.includes('src/shared')) { + return 'common'; + } + }, + // **File Naming Strategies** chunkFileNames: (chunkInfo) => { const facadeModuleId = chunkInfo.facadeModuleId ? chunkInfo.facadeModuleId.split('/').pop() : 'chunk'; @@ -103,6 +192,12 @@ export default defineConfig(({ command, mode }) => { }, }, + // **Tree shaking optimization** + treeshake: { + moduleSideEffects: false, + unknownGlobalSideEffects: false, + }, + // **External dependencies (if any should be externalized)** external: [], @@ -122,15 +217,17 @@ export default defineConfig(({ command, mode }) => { include: [ 'react', 'react-dom', - 'react/jsx-runtime', + 'react-router-dom', + '@reduxjs/toolkit', + 'react-redux', + 'react-i18next', + 'dayjs', + ], + exclude: [ + // Exclude antd from pre-bundling to allow better tree-shaking 'antd', '@ant-design/icons', ], - exclude: [ - // Add any packages that should not be pre-bundled - ], - // Force pre-bundling to avoid runtime issues - force: true, }, // **Define global constants**