From 94977f7255394cff43ed1dc2c491ea16f823964c Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Thu, 10 Jul 2025 20:39:15 +0530 Subject: [PATCH] feat(performance): enhance application performance with optimizations and monitoring - Updated package dependencies for improved localization support and performance. - Introduced CSS performance optimizations to prevent layout shifts and enhance rendering efficiency. - Implemented asset preloading and lazy loading strategies for critical components to improve load times. - Enhanced translation loading with optimized caching and background loading strategies. - Added performance monitoring utilities to track key metrics and improve user experience. - Refactored task management components to utilize new performance features and ensure efficient rendering. - Introduced new utility functions for asset and CSS optimizations to streamline resource management. --- worklenz-frontend/package-lock.json | 116 ++- worklenz-frontend/package.json | 3 +- worklenz-frontend/src/App.tsx | 14 + .../lazy-loading-optimizations.tsx | 403 +++++++++++ .../task-management/task-list-board.tsx | 34 +- .../src/hooks/useTranslationPreloader.ts | 324 +++++++-- worklenz-frontend/src/i18n.ts | 253 +++++-- worklenz-frontend/src/index.tsx | 5 + worklenz-frontend/src/layouts/MainLayout.tsx | 27 +- .../src/socket/socketContext.tsx | 45 +- .../src/styles/performance-optimizations.css | 296 ++++++++ .../src/utils/asset-optimizations.ts | 588 +++++++++++++++ .../src/utils/css-optimizations.ts | 598 +++++++++++++++ .../utils/enhanced-performance-monitoring.ts | 680 ++++++++++++++++++ .../src/utils/redux-optimizations.ts | 319 ++++++++ 15 files changed, 3572 insertions(+), 133 deletions(-) create mode 100644 worklenz-frontend/src/components/task-management/lazy-loading-optimizations.tsx create mode 100644 worklenz-frontend/src/styles/performance-optimizations.css create mode 100644 worklenz-frontend/src/utils/asset-optimizations.ts create mode 100644 worklenz-frontend/src/utils/css-optimizations.ts create mode 100644 worklenz-frontend/src/utils/enhanced-performance-monitoring.ts create mode 100644 worklenz-frontend/src/utils/redux-optimizations.ts diff --git a/worklenz-frontend/package-lock.json b/worklenz-frontend/package-lock.json index f12aaee4..721124f0 100644 --- a/worklenz-frontend/package-lock.json +++ b/worklenz-frontend/package-lock.json @@ -34,8 +34,9 @@ "gantt-task-react": "^0.3.9", "html2canvas": "^1.4.1", "i18next": "^23.16.8", - "i18next-browser-languagedetector": "^8.0.3", + "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^2.7.3", + "i18next-localstorage-backend": "^4.2.0", "jspdf": "^3.0.0", "mixpanel-browser": "^2.56.0", "nanoid": "^5.1.5", @@ -728,6 +729,8 @@ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", @@ -741,7 +744,9 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/@babel/code-frame": { "version": "7.27.1", @@ -1051,6 +1056,8 @@ } ], "license": "MIT-0", + "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -1071,6 +1078,8 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=18" }, @@ -1095,6 +1104,8 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@csstools/color-helpers": "^5.0.2", "@csstools/css-calc": "^2.1.4" @@ -1123,6 +1134,8 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=18" }, @@ -1146,6 +1159,8 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -2997,6 +3012,8 @@ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 14" } @@ -3738,6 +3755,8 @@ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" @@ -3758,6 +3777,8 @@ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" @@ -3772,6 +3793,8 @@ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "punycode": "^2.3.1" }, @@ -3785,6 +3808,8 @@ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, "license": "BSD-2-Clause", + "optional": true, + "peer": true, "engines": { "node": ">=12" } @@ -3795,6 +3820,8 @@ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" @@ -3841,7 +3868,9 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/deep-eql": { "version": "5.0.2", @@ -4016,6 +4045,8 @@ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, "license": "BSD-2-Clause", + "optional": true, + "peer": true, "engines": { "node": ">=0.12" }, @@ -4499,6 +4530,8 @@ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "whatwg-encoding": "^3.1.1" }, @@ -4534,6 +4567,8 @@ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -4548,6 +4583,8 @@ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -4586,9 +4623,9 @@ } }, "node_modules/i18next-browser-languagedetector": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.1.0.tgz", - "integrity": "sha512-mHZxNx1Lq09xt5kCauZ/4bsXOEA2pfpwSoU11/QTJB+pD94iONFwp+ohqi///PwiFvjFOxe1akYCdHyFo1ng5Q==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", + "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.2" @@ -4603,12 +4640,23 @@ "cross-fetch": "4.0.0" } }, + "node_modules/i18next-localstorage-backend": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/i18next-localstorage-backend/-/i18next-localstorage-backend-4.2.0.tgz", + "integrity": "sha512-vglEQF0AnLriX7dLA2drHnqAYzHxnLwWQzBDw8YxcIDjOvYZz5rvpal59Dq4In+IHNmGNM32YgF0TDjBT0fHmA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.22.15" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -4729,7 +4777,9 @@ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/isexe": { "version": "2.0.0", @@ -4818,6 +4868,8 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -4858,6 +4910,8 @@ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "punycode": "^2.3.1" }, @@ -4871,6 +4925,8 @@ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, "license": "BSD-2-Clause", + "optional": true, + "peer": true, "engines": { "node": ">=12" } @@ -4881,6 +4937,8 @@ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" @@ -4895,6 +4953,8 @@ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -5533,7 +5593,9 @@ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/object-assign": { "version": "4.1.1", @@ -5595,6 +5657,8 @@ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "entities": "^6.0.0" }, @@ -6060,6 +6124,8 @@ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=6" } @@ -7205,7 +7271,9 @@ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/rrweb-snapshot": { "version": "2.0.0-alpha.18", @@ -7253,7 +7321,9 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/saxes": { "version": "6.0.0", @@ -7261,6 +7331,8 @@ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "xmlchars": "^2.2.0" }, @@ -7663,7 +7735,9 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/tailwindcss": { "version": "3.4.17", @@ -7883,6 +7957,8 @@ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "tldts-core": "^6.1.86" }, @@ -7895,7 +7971,9 @@ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -7921,6 +7999,8 @@ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "dev": true, "license": "BSD-3-Clause", + "optional": true, + "peer": true, "dependencies": { "tldts": "^6.1.32" }, @@ -8284,6 +8364,8 @@ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "xml-name-validator": "^5.0.0" }, @@ -8318,6 +8400,8 @@ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "iconv-lite": "0.6.3" }, @@ -8331,6 +8415,8 @@ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -8487,6 +8573,8 @@ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -8496,7 +8584,9 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/xmlhttprequest-ssl": { "version": "2.1.2", diff --git a/worklenz-frontend/package.json b/worklenz-frontend/package.json index b83e80fd..7e25181c 100644 --- a/worklenz-frontend/package.json +++ b/worklenz-frontend/package.json @@ -38,8 +38,9 @@ "gantt-task-react": "^0.3.9", "html2canvas": "^1.4.1", "i18next": "^23.16.8", - "i18next-browser-languagedetector": "^8.0.3", + "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^2.7.3", + "i18next-localstorage-backend": "^4.2.0", "jspdf": "^3.0.0", "mixpanel-browser": "^2.56.0", "nanoid": "^5.1.5", diff --git a/worklenz-frontend/src/App.tsx b/worklenz-frontend/src/App.tsx index 3ed779c4..aa20e0ed 100644 --- a/worklenz-frontend/src/App.tsx +++ b/worklenz-frontend/src/App.tsx @@ -19,6 +19,9 @@ import { Language } from './features/i18n/localesSlice'; import logger from './utils/errorLogger'; import { SuspenseFallback } from './components/suspense-fallback/suspense-fallback'; +// Performance optimizations +import { CSSPerformanceMonitor, LayoutStabilizer, CriticalCSSManager } from './utils/css-optimizations'; + // Service Worker import { registerSW } from './utils/serviceWorkerRegistration'; @@ -84,6 +87,17 @@ const App: React.FC = memo(() => { try { // Initialize CSRF token immediately as it's needed for API calls await initializeCsrfToken(); + + // Start CSS performance monitoring + CSSPerformanceMonitor.monitorLayoutShifts(); + CSSPerformanceMonitor.monitorRenderBlocking(); + + // Preload critical fonts to prevent layout shifts + LayoutStabilizer.preloadFonts([ + { family: 'Inter', weight: '400' }, + { family: 'Inter', weight: '500' }, + { family: 'Inter', weight: '600' }, + ]); } catch (error) { if (isMounted) { logger.error('Failed to initialize critical app functionality:', error); diff --git a/worklenz-frontend/src/components/task-management/lazy-loading-optimizations.tsx b/worklenz-frontend/src/components/task-management/lazy-loading-optimizations.tsx new file mode 100644 index 00000000..afdf83bb --- /dev/null +++ b/worklenz-frontend/src/components/task-management/lazy-loading-optimizations.tsx @@ -0,0 +1,403 @@ +import React, { lazy, Suspense, ComponentType, ReactNode } from 'react'; +import { Skeleton, Spin } from 'antd'; + +// Enhanced lazy loading with error boundary and retry logic +export function createOptimizedLazy>( + importFunc: () => Promise<{ default: T }>, + fallback?: ReactNode +): React.LazyExoticComponent { + let retryCount = 0; + const maxRetries = 3; + + const retryImport = async (): Promise<{ default: T }> => { + try { + return await importFunc(); + } catch (error) { + if (retryCount < maxRetries) { + retryCount++; + console.warn(`Lazy loading failed, retrying... (${retryCount}/${maxRetries})`); + // Exponential backoff: 500ms, 1s, 2s + await new Promise(resolve => setTimeout(resolve, 500 * Math.pow(2, retryCount - 1))); + return retryImport(); + } + throw error; + } + }; + + return lazy(retryImport); +} + +// Preloading utility for components that will likely be used +export const preloadComponent = >( + importFunc: () => Promise<{ default: T }> +): void => { + // Preload on user interaction or after a delay + if ('requestIdleCallback' in window) { + (window as any).requestIdleCallback(() => { + importFunc().catch(() => { + // Ignore preload errors + }); + }); + } else { + setTimeout(() => { + importFunc().catch(() => { + // Ignore preload errors + }); + }, 2000); + } +}; + +// Lazy-loaded task management components with optimized fallbacks +export const LazyTaskListBoard = createOptimizedLazy( + () => import('./task-list-board'), +
+ +
+); + +export const LazyVirtualizedTaskList = createOptimizedLazy( + () => import('./virtualized-task-list'), + +); + +export const LazyTaskRow = createOptimizedLazy( + () => import('./task-row'), + +); + +export const LazyImprovedTaskFilters = createOptimizedLazy( + () => import('./improved-task-filters'), + +); + +export const LazyOptimizedBulkActionBar = createOptimizedLazy( + () => import('./optimized-bulk-action-bar'), + +); + +export const LazyPerformanceAnalysis = createOptimizedLazy( + () => import('./performance-analysis'), +
Loading performance tools...
+); + +// Kanban-specific components +export const LazyKanbanTaskListBoard = createOptimizedLazy( + () => import('../kanban-board-management-v2/kanbanTaskListBoard'), +
+ +
+); + +// Task list V2 components +export const LazyTaskListV2Table = createOptimizedLazy( + () => import('../task-list-v2/TaskListV2Table'), + +); + +export const LazyTaskRowWithSubtasks = createOptimizedLazy( + () => import('../task-list-v2/TaskRowWithSubtasks'), + +); + +export const LazyCustomColumnModal = createOptimizedLazy( + () => import('@/pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/custom-column-modal'), +
+); + +export const LazyLabelsSelector = createOptimizedLazy( + () => import('@/components/LabelsSelector'), + +); + +export const LazyAssigneeSelector = createOptimizedLazy( + () => import('./lazy-assignee-selector'), + +); + +export const LazyTaskStatusDropdown = createOptimizedLazy( + () => import('./task-status-dropdown'), + +); + +export const LazyTaskPriorityDropdown = createOptimizedLazy( + () => import('./task-priority-dropdown'), + +); + +export const LazyTaskPhaseDropdown = createOptimizedLazy( + () => import('./task-phase-dropdown'), + +); + +// HOC for progressive enhancement +interface ProgressiveEnhancementProps { + condition: boolean; + children: ReactNode; + fallback?: ReactNode; + loadingComponent?: ReactNode; +} + +export const ProgressiveEnhancement: React.FC = ({ + condition, + children, + fallback, + loadingComponent = +}) => { + if (!condition) { + return <>{fallback || loadingComponent}; + } + + return ( + + {children} + + ); +}; + +// Intersection observer based lazy loading for components +interface IntersectionLazyLoadProps { + children: ReactNode; + fallback?: ReactNode; + rootMargin?: string; + threshold?: number; + once?: boolean; +} + +export const IntersectionLazyLoad: React.FC = ({ + children, + fallback = , + rootMargin = '100px', + threshold = 0.1, + once = true +}) => { + const [isVisible, setIsVisible] = React.useState(false); + const [hasBeenVisible, setHasBeenVisible] = React.useState(false); + const ref = React.useRef(null); + + React.useEffect(() => { + const element = ref.current; + if (!element) return; + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true); + if (once) { + setHasBeenVisible(true); + observer.disconnect(); + } + } else if (!once) { + setIsVisible(false); + } + }, + { rootMargin, threshold } + ); + + observer.observe(element); + + return () => { + observer.disconnect(); + }; + }, [rootMargin, threshold, once]); + + const shouldRender = isVisible || hasBeenVisible; + + return ( +
+ {shouldRender ? ( + + {children} + + ) : ( + fallback + )} +
+ ); +}; + +// Route-based code splitting utility +export const createRouteComponent = >( + importFunc: () => Promise<{ default: T }>, + pageTitle?: string +) => { + const LazyComponent = createOptimizedLazy(importFunc); + + return React.memo(() => { + React.useEffect(() => { + if (pageTitle) { + document.title = pageTitle; + } + }, []); + + return ( + + + + } + > + + + ); + }); +}; + +// Bundle splitting by feature +export const TaskManagementBundle = { + TaskListBoard: LazyTaskListBoard, + VirtualizedTaskList: LazyVirtualizedTaskList, + TaskRow: LazyTaskRow, + TaskFilters: LazyImprovedTaskFilters, + BulkActionBar: LazyOptimizedBulkActionBar, + PerformanceAnalysis: LazyPerformanceAnalysis, +}; + +export const KanbanBundle = { + KanbanBoard: LazyKanbanTaskListBoard, +}; + +export const TaskListV2Bundle = { + TaskTable: LazyTaskListV2Table, + TaskRowWithSubtasks: LazyTaskRowWithSubtasks, +}; + +export const FormBundle = { + CustomColumnModal: LazyCustomColumnModal, + LabelsSelector: LazyLabelsSelector, + AssigneeSelector: LazyAssigneeSelector, +}; + +export const DropdownBundle = { + StatusDropdown: LazyTaskStatusDropdown, + PriorityDropdown: LazyTaskPriorityDropdown, + PhaseDropdown: LazyTaskPhaseDropdown, +}; + +// Preloading strategies +export const preloadTaskManagementComponents = () => { + // Preload core components that are likely to be used + preloadComponent(() => import('./task-list-board')); + preloadComponent(() => import('./virtualized-task-list')); + preloadComponent(() => import('./improved-task-filters')); +}; + +export const preloadKanbanComponents = () => { + preloadComponent(() => import('../kanban-board-management-v2/kanbanTaskListBoard')); +}; + +export const preloadFormComponents = () => { + preloadComponent(() => import('@/components/LabelsSelector')); +}; + +// Dynamic import utilities +export const importTaskComponent = async (componentName: string) => { + const componentMap: Record Promise> = { + 'task-list-board': () => import('./task-list-board'), + 'virtualized-task-list': () => import('./virtualized-task-list'), + 'task-row': () => import('./task-row'), + 'improved-task-filters': () => import('./improved-task-filters'), + 'optimized-bulk-action-bar': () => import('./optimized-bulk-action-bar'), + 'performance-analysis': () => import('./performance-analysis'), + }; + + const importFunc = componentMap[componentName]; + if (!importFunc) { + throw new Error(`Component ${componentName} not found`); + } + + return importFunc(); +}; + +// Error boundary for lazy loaded components +interface LazyErrorBoundaryState { + hasError: boolean; + error?: Error; +} + +export class LazyErrorBoundary extends React.Component< + { children: ReactNode; fallback?: ReactNode }, + LazyErrorBoundaryState +> { + constructor(props: { children: ReactNode; fallback?: ReactNode }) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): LazyErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('Lazy loading error:', error, errorInfo); + } + + render() { + if (this.state.hasError) { + return ( + this.props.fallback || ( +
+

Failed to load component

+ +
+ ) + ); + } + + return this.props.children; + } +} + +// Usage example and documentation +export const LazyLoadingExamples = { + // Basic lazy loading with suspense + BasicExample: () => ( + }> + + + ), + + // Progressive enhancement + ProgressiveExample: () => ( + + {}} + onToggleSubtasks={() => {}} + /> + + ), + + // Intersection observer lazy loading + IntersectionExample: () => ( + + + + ), + + // Error boundary with lazy loading + ErrorBoundaryExample: () => ( + + }> + {}} + onToggleSubtasks={() => {}} + /> + + + ), +}; \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-management/task-list-board.tsx b/worklenz-frontend/src/components/task-management/task-list-board.tsx index 02e4c758..f6ea1721 100644 --- a/worklenz-frontend/src/components/task-management/task-list-board.tsx +++ b/worklenz-frontend/src/components/task-management/task-list-board.tsx @@ -77,6 +77,12 @@ import { fetchLabels } from '@/features/taskAttributes/taskLabelSlice'; import ImprovedTaskFilters from './improved-task-filters'; import PerformanceAnalysis from './performance-analysis'; +// Import asset optimizations +import { AssetPreloader, LazyLoader } from '@/utils/asset-optimizations'; + +// Import performance monitoring +import { CustomPerformanceMeasurer } from '@/utils/enhanced-performance-monitoring'; + // Import drag and drop performance optimizations import './drag-drop-optimized.css'; import './optimized-bulk-action-bar.css'; @@ -210,13 +216,39 @@ const TaskListBoard: React.FC = ({ projectId, className = '' return () => clearInterval(interval); }, []); + // Initialize asset optimization + useEffect(() => { + // Preload critical task management assets + AssetPreloader.preloadAssets([ + { url: '/icons/task-status.svg', priority: 'high' }, + { url: '/icons/priority-high.svg', priority: 'high' }, + { url: '/icons/priority-medium.svg', priority: 'high' }, + { url: '/icons/priority-low.svg', priority: 'high' }, + { url: '/icons/phase.svg', priority: 'medium' }, + { url: '/icons/assignee.svg', priority: 'medium' }, + ]); + + // Preload critical images for better performance + LazyLoader.preloadCriticalImages([ + '/icons/task-status.svg', + '/icons/priority-high.svg', + '/icons/priority-medium.svg', + '/icons/priority-low.svg', + ]); + }, []); + // Fetch task groups when component mounts or dependencies change useEffect(() => { if (projectId && !hasInitialized.current) { hasInitialized.current = true; + // Measure task loading performance + CustomPerformanceMeasurer.mark('task-load-time'); + // Fetch real tasks from V3 API (minimal processing needed) - dispatch(fetchTasksV3(projectId)); + dispatch(fetchTasksV3(projectId)).finally(() => { + CustomPerformanceMeasurer.measure('task-load-time'); + }); } }, [projectId, dispatch]); diff --git a/worklenz-frontend/src/hooks/useTranslationPreloader.ts b/worklenz-frontend/src/hooks/useTranslationPreloader.ts index 46b3ad86..226953c8 100644 --- a/worklenz-frontend/src/hooks/useTranslationPreloader.ts +++ b/worklenz-frontend/src/hooks/useTranslationPreloader.ts @@ -1,83 +1,283 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { ensureTranslationsLoaded } from '@/i18n'; +import { + ensureTranslationsLoaded, + preloadPageTranslations, + getPerformanceMetrics, + changeLanguageOptimized +} from '../i18n'; +import logger from '../utils/errorLogger'; -interface UseTranslationPreloaderOptions { - namespaces?: string[]; - fallback?: React.ReactNode; +// Cache for preloaded translation states +const preloadCache = new Map(); +const loadingStates = new Map(); + +interface TranslationHookOptions { + preload?: boolean; + priority?: number; + fallbackReady?: boolean; } -/** - * Hook to ensure translations are loaded before rendering components - * This prevents Suspense issues when components use useTranslation - */ -export const useTranslationPreloader = ( - namespaces: string[] = ['tasks/task-table-bulk-actions', 'task-management'], - options: UseTranslationPreloaderOptions = {} -) => { - const [isLoaded, setIsLoaded] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const { t, ready } = useTranslation(namespaces); +interface TranslationHookReturn { + t: (key: string, defaultValue?: string) => string; + ready: boolean; + isLoading: boolean; + error: Error | null; + retryLoad: () => Promise; + performanceMetrics: any; +} +// Enhanced translation hook with better performance +export const useOptimizedTranslation = ( + namespace: string | string[], + options: TranslationHookOptions = {} +): TranslationHookReturn => { + const { preload = true, priority = 5, fallbackReady = true } = options; + + const namespaces = Array.isArray(namespace) ? namespace : [namespace]; + const namespaceKey = namespaces.join(','); + + const [ready, setReady] = useState(fallbackReady); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const hasInitialized = useRef(false); + const loadingPromise = useRef | null>(null); + + const { t, i18n } = useTranslation(namespaces); + + // Memoized preload function + const preloadTranslations = useCallback(async () => { + const cacheKey = `${i18n.language}:${namespaceKey}`; + + // Skip if already preloaded or currently loading + if (preloadCache.get(cacheKey) || loadingStates.get(cacheKey)) { + return; + } + + try { + setIsLoading(true); + setError(null); + loadingStates.set(cacheKey, true); + + const startTime = performance.now(); + + // Use the optimized preload function + await preloadPageTranslations(namespaces); + + const endTime = performance.now(); + const loadTime = endTime - startTime; + + if (process.env.NODE_ENV === 'development') { + console.log( + `✅ Preloaded translations for ${namespaceKey} in ${loadTime.toFixed(2)}ms` + ); + } + + preloadCache.set(cacheKey, true); + setReady(true); + } catch (err) { + const error = err instanceof Error ? err : new Error('Failed to preload translations'); + setError(error); + logger.error(`Failed to preload translations for ${namespaceKey}:`, error); + + // Fallback to ready state even on error to prevent blocking UI + if (fallbackReady) { + setReady(true); + } + } finally { + setIsLoading(false); + loadingStates.set(cacheKey, false); + } + }, [namespaces, namespaceKey, i18n.language, fallbackReady]); + + // Initialize preloading useEffect(() => { - let isMounted = true; + if (!hasInitialized.current && preload) { + hasInitialized.current = true; + + if (!loadingPromise.current) { + loadingPromise.current = preloadTranslations(); + } + } + }, [preload, preloadTranslations]); - const loadTranslations = async () => { - try { - setIsLoading(true); - - // Only load translations for current language to avoid multiple requests - await ensureTranslationsLoaded(namespaces); - - // Wait for i18next to be ready - if (!ready) { - // If i18next is not ready, wait a bit and check again - await new Promise(resolve => setTimeout(resolve, 100)); - } - - if (isMounted) { - setIsLoaded(true); - setIsLoading(false); - } - } catch (error) { - if (isMounted) { - setIsLoaded(true); // Still set as loaded to prevent infinite loading - setIsLoading(false); - } + // Handle language changes + useEffect(() => { + const handleLanguageChange = () => { + const cacheKey = `${i18n.language}:${namespaceKey}`; + if (!preloadCache.get(cacheKey) && preload) { + setReady(false); + preloadTranslations(); } }; - // Only load if not already loaded - if (!isLoaded && !ready) { - loadTranslations(); - } else if (ready && !isLoaded) { - setIsLoaded(true); - setIsLoading(false); - } - + i18n.on('languageChanged', handleLanguageChange); return () => { - isMounted = false; + i18n.off('languageChanged', handleLanguageChange); }; - }, [namespaces, ready, isLoaded]); + }, [i18n, namespaceKey, preload, preloadTranslations]); + + // Retry function + const retryLoad = useCallback(async () => { + const cacheKey = `${i18n.language}:${namespaceKey}`; + preloadCache.delete(cacheKey); + loadingStates.delete(cacheKey); + await preloadTranslations(); + }, [namespaceKey, i18n.language, preloadTranslations]); + + // Get performance metrics + const performanceMetrics = useMemo(() => getPerformanceMetrics(), [ready]); + + // Enhanced t function with better error handling + const enhancedT = useCallback((key: string, defaultValue?: string) => { + try { + const translation = t(key, { defaultValue }); + + // Return the translation if it's not the key itself (indicating it was found) + if (translation !== key) { + return translation; + } + + // If we have a default value, use it + if (defaultValue) { + return defaultValue; + } + + // Fallback to the key + return key; + } catch (err) { + logger.error(`Translation error for key ${key}:`, err); + return defaultValue || key; + } + }, [t]); return { - t, - ready: isLoaded && ready, + t: enhancedT, + ready, isLoading, - isLoaded, + error, + retryLoad, + performanceMetrics, }; }; -/** - * Hook specifically for bulk action bar translations - */ -export const useBulkActionTranslations = () => { - return useTranslationPreloader(['tasks/task-table-bulk-actions']); +// Specialized hooks for commonly used namespaces +export const useTaskManagementTranslations = (options?: TranslationHookOptions) => { + return useOptimizedTranslation(['task-management', 'task-list-table'], { + priority: 8, + ...options, + }); }; -/** - * Hook for task management translations - */ -export const useTaskManagementTranslations = () => { - return useTranslationPreloader(['task-management', 'tasks/task-table-bulk-actions']); +export const useBulkActionTranslations = (options?: TranslationHookOptions) => { + return useOptimizedTranslation(['tasks/task-table-bulk-actions', 'task-management'], { + priority: 6, + ...options, + }); +}; + +export const useTaskDrawerTranslations = (options?: TranslationHookOptions) => { + return useOptimizedTranslation(['task-drawer/task-drawer', 'task-list-table'], { + priority: 7, + ...options, + }); +}; + +export const useProjectTranslations = (options?: TranslationHookOptions) => { + return useOptimizedTranslation(['project-drawer', 'common'], { + priority: 7, + ...options, + }); +}; + +export const useSettingsTranslations = (options?: TranslationHookOptions) => { + return useOptimizedTranslation(['settings', 'common'], { + priority: 4, + ...options, + }); +}; + +// Utility function to preload multiple namespaces +export const preloadMultipleNamespaces = async ( + namespaces: string[], + priority: number = 5 +): Promise => { + try { + await Promise.all( + namespaces.map(ns => preloadPageTranslations([ns])) + ); + return true; + } catch (error) { + logger.error('Failed to preload multiple namespaces:', error); + return false; + } +}; + +// Hook for pages that need multiple translation namespaces +export const usePageTranslations = ( + namespaces: string[], + options?: TranslationHookOptions +) => { + const { ready, isLoading, error } = useOptimizedTranslation(namespaces, options); + + // Create individual translation functions for each namespace + const translations = useMemo(() => { + const result: Record = {}; + + namespaces.forEach(ns => { + const { t } = useTranslation(ns); + result[ns] = t; + }); + + return result; + }, [namespaces, ready]); + + return { + ...translations, + ready, + isLoading, + error, + }; +}; + +// Language switching utilities +export const useLanguageSwitcher = () => { + const [switching, setSwitching] = useState(false); + + const switchLanguage = useCallback(async (language: string) => { + try { + setSwitching(true); + await changeLanguageOptimized(language); + + // Clear preload cache for new language + preloadCache.clear(); + loadingStates.clear(); + + } catch (error) { + logger.error('Failed to switch language:', error); + } finally { + setSwitching(false); + } + }, []); + + return { + switchLanguage, + switching, + }; +}; + +// Performance monitoring hook +export const useTranslationPerformance = () => { + const [metrics, setMetrics] = useState(getPerformanceMetrics()); + + useEffect(() => { + const interval = setInterval(() => { + setMetrics(getPerformanceMetrics()); + }, 5000); // Update every 5 seconds + + return () => clearInterval(interval); + }, []); + + return metrics; }; diff --git a/worklenz-frontend/src/i18n.ts b/worklenz-frontend/src/i18n.ts index 8c96cd62..444dd60d 100644 --- a/worklenz-frontend/src/i18n.ts +++ b/worklenz-frontend/src/i18n.ts @@ -1,6 +1,8 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import HttpApi from 'i18next-http-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import LocalStorageBackend from 'i18next-localstorage-backend'; import logger from './utils/errorLogger'; // Essential namespaces that should be preloaded to prevent Suspense @@ -19,53 +21,133 @@ const SECONDARY_NAMESPACES = [ 'project-drawer', ]; +// Tertiary namespaces that can be loaded even later +const TERTIARY_NAMESPACES = [ + 'task-drawer/task-drawer', + 'task-list-table', + 'phases-drawer', + 'schedule', + 'reporting', + 'admin-center/current-bill', +]; + // Cache to track loaded translations and prevent duplicate requests const loadedTranslations = new Set(); const loadingPromises = new Map>(); // Background loading queue for non-essential translations -let backgroundLoadingQueue: Array<{ lang: string; ns: string }> = []; +let backgroundLoadingQueue: Array<{ lang: string; ns: string; priority: number }> = []; let isBackgroundLoading = false; +// Performance monitoring +const performanceMetrics = { + totalLoadTime: 0, + translationsLoaded: 0, + cacheHits: 0, + cacheMisses: 0, +}; + +// Enhanced caching configuration +const CACHE_CONFIG = { + EXPIRATION_TIME: 7 * 24 * 60 * 60 * 1000, // 7 days + MAX_CACHE_SIZE: 50, // Maximum number of namespaces to cache + CLEANUP_INTERVAL: 24 * 60 * 60 * 1000, // Clean cache daily +}; + i18n - .use(HttpApi) + .use(LocalStorageBackend) // Cache translations to localStorage + .use(LanguageDetector) // Detect user language + .use(HttpApi) // Fetch translations if not in cache .use(initReactI18next) .init({ fallbackLng: 'en', backend: { loadPath: '/locales/{{lng}}/{{ns}}.json', - // Add request timeout to prevent hanging on slow connections - requestOptions: { - cache: 'default', - mode: 'cors', - credentials: 'same-origin', - }, + addPath: '/locales/add/{{lng}}/{{ns}}', + // Enhanced LocalStorage caching options + backendOptions: [{ + expirationTime: CACHE_CONFIG.EXPIRATION_TIME, + // Store translations more efficiently + store: { + setItem: (key: string, value: string) => { + try { + // Compress large translation objects + const compressedValue = value.length > 1000 ? + JSON.stringify(JSON.parse(value)) : value; + localStorage.setItem(key, compressedValue); + performanceMetrics.cacheHits++; + } catch (error) { + logger.error('Failed to store translation in cache:', error); + } + }, + getItem: (key: string) => { + try { + const value = localStorage.getItem(key); + if (value) { + performanceMetrics.cacheHits++; + return value; + } + performanceMetrics.cacheMisses++; + return null; + } catch (error) { + logger.error('Failed to retrieve translation from cache:', error); + performanceMetrics.cacheMisses++; + return null; + } + } + } + }, { + loadPath: '/locales/{{lng}}/{{ns}}.json', + // Add request timeout and retry logic + requestOptions: { + cache: 'force-cache', // Use browser cache when possible + }, + parse: (data: string) => { + try { + return JSON.parse(data); + } catch (error) { + logger.error('Failed to parse translation data:', error); + return {}; + } + } + }], }, defaultNS: 'common', - // Only load essential namespaces initially ns: ESSENTIAL_NAMESPACES, interpolation: { escapeValue: false, }, - // Only preload current language to reduce initial load preload: [], load: 'languageOnly', - // Disable loading all namespaces on init initImmediate: false, - // Cache translations with shorter expiration for better performance - cache: { - enabled: true, - expirationTime: 12 * 60 * 60 * 1000, // 12 hours + detection: { + order: ['localStorage', 'navigator'], // Check localStorage first, then browser language + caches: ['localStorage'], + // Cache the detected language for faster subsequent loads + cookieMinutes: 60 * 24 * 7, // 1 week }, // Reduce debug output in production debug: process.env.NODE_ENV === 'development', + // Performance optimizations + cleanCode: true, // Remove code characters + keySeparator: false, // Disable key separator for better performance + nsSeparator: false, // Disable namespace separator for better performance + pluralSeparator: '_', // Use underscore for plural separation + react: { + useSuspense: false, // Disable suspense for better control + bindI18n: 'languageChanged loaded', // Only bind necessary events + bindI18nStore: false, // Disable store binding for better performance + }, }); -// Optimized function to ensure translations are loaded +// Optimized function to ensure translations are loaded with priority support export const ensureTranslationsLoaded = async ( namespaces: string[] = ESSENTIAL_NAMESPACES, - languages: string[] = [i18n.language || 'en'] + languages: string[] = [i18n.language || 'en'], + priority: number = 0 ) => { + const startTime = performance.now(); + try { const loadPromises: Promise[] = []; @@ -84,7 +166,7 @@ export const ensureTranslationsLoaded = async ( continue; } - // Create loading promise + // Create loading promise with enhanced error handling const loadingPromise = new Promise((resolve, reject) => { const currentLang = i18n.language; const shouldSwitchLang = currentLang !== lang; @@ -102,10 +184,12 @@ export const ensureTranslationsLoaded = async ( } loadedTranslations.add(key); + performanceMetrics.translationsLoaded++; resolve(); } catch (error) { logger.error(`Failed to load namespace: ${ns} for language: ${lang}`, error); - reject(error); + // Don't reject completely, just log and continue + resolve(); // Still resolve to prevent blocking other translations } finally { loadingPromises.delete(key); } @@ -120,6 +204,10 @@ export const ensureTranslationsLoaded = async ( } await Promise.all(loadPromises); + + const endTime = performance.now(); + performanceMetrics.totalLoadTime += (endTime - startTime); + return true; } catch (error) { logger.error('Failed to load translations:', error); @@ -127,28 +215,37 @@ export const ensureTranslationsLoaded = async ( } }; -// Background loading function for non-essential translations +// Enhanced background loading function with priority queue const processBackgroundQueue = async () => { if (isBackgroundLoading || backgroundLoadingQueue.length === 0) return; isBackgroundLoading = true; try { - // Process queue in batches to avoid overwhelming the network - const batchSize = 3; + // Sort by priority (higher priority first) + backgroundLoadingQueue.sort((a, b) => b.priority - a.priority); + + // Process queue in smaller batches to avoid overwhelming the network + const batchSize = 2; // Reduced batch size for better performance 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); - }) + ensureTranslationsLoaded([ns], [lang], 0).catch(error => { + logger.error(`Background loading failed for ${lang}:${ns}`, error); + }) ); await Promise.all(batchPromises); - // Add small delay between batches to prevent blocking + // Add delay between batches to prevent blocking main thread if (backgroundLoadingQueue.length > 0) { - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise(resolve => setTimeout(resolve, 200)); // Increased delay + } + + // Break if we've been loading for too long (prevent infinite loops) + if (performance.now() - performanceMetrics.totalLoadTime > 30000) { // 30 seconds max + logger.error('Background translation loading taking too long, stopping'); + break; } } } finally { @@ -156,17 +253,29 @@ const processBackgroundQueue = async () => { } }; -// Queue secondary translations for background loading -const queueSecondaryTranslations = (language: string) => { - SECONDARY_NAMESPACES.forEach(ns => { +// Enhanced queueing with priority support +const queueTranslations = (language: string, namespaces: string[], priority: number = 0) => { + namespaces.forEach(ns => { const key = `${language}:${ns}`; if (!loadedTranslations.has(key)) { - backgroundLoadingQueue.push({ lang: language, ns }); + // Remove existing entry if it exists with lower priority + const existingIndex = backgroundLoadingQueue.findIndex(item => + item.lang === language && item.ns === ns); + if (existingIndex >= 0) { + if (backgroundLoadingQueue[existingIndex].priority < priority) { + backgroundLoadingQueue.splice(existingIndex, 1); + } else { + return; // Don't add duplicate with lower or equal priority + } + } + + backgroundLoadingQueue.push({ lang: language, ns, priority }); } }); - // Start background loading with a delay to not interfere with initial render - setTimeout(processBackgroundQueue, 2000); + // Start background loading with appropriate delay based on priority + const delay = priority > 5 ? 1000 : priority > 2 ? 2000 : 3000; + setTimeout(processBackgroundQueue, delay); }; // Initialize only essential translations for current language @@ -174,11 +283,14 @@ const initializeTranslations = async () => { try { const currentLang = i18n.language || 'en'; - // Load only essential namespaces initially - await ensureTranslationsLoaded(ESSENTIAL_NAMESPACES, [currentLang]); + // Load only essential namespaces immediately + await ensureTranslationsLoaded(ESSENTIAL_NAMESPACES, [currentLang], 10); - // Queue secondary translations for background loading - queueSecondaryTranslations(currentLang); + // Queue secondary translations with medium priority + queueTranslations(currentLang, SECONDARY_NAMESPACES, 5); + + // Queue tertiary translations with low priority + queueTranslations(currentLang, TERTIARY_NAMESPACES, 1); return true; } catch (error) { @@ -187,17 +299,20 @@ const initializeTranslations = async () => { } }; -// Language change handler that prioritizes essential namespaces +// Enhanced language change handler with better prioritization export const changeLanguageOptimized = async (language: string) => { try { // Change language first await i18n.changeLanguage(language); - // Load essential namespaces immediately - await ensureTranslationsLoaded(ESSENTIAL_NAMESPACES, [language]); + // Load essential namespaces immediately with high priority + await ensureTranslationsLoaded(ESSENTIAL_NAMESPACES, [language], 10); - // Queue secondary translations for background loading - queueSecondaryTranslations(language); + // Queue secondary translations with medium priority + queueTranslations(language, SECONDARY_NAMESPACES, 5); + + // Queue tertiary translations with low priority + queueTranslations(language, TERTIARY_NAMESPACES, 1); return true; } catch (error) { @@ -206,7 +321,59 @@ export const changeLanguageOptimized = async (language: string) => { } }; -// Initialize translations on app startup (only essential ones) +// Cache cleanup functionality +const cleanupCache = () => { + try { + const keys = Object.keys(localStorage).filter(key => + key.startsWith('i18next_res_') + ); + + if (keys.length > CACHE_CONFIG.MAX_CACHE_SIZE) { + // Remove oldest entries + const entriesToRemove = keys.slice(0, keys.length - CACHE_CONFIG.MAX_CACHE_SIZE); + entriesToRemove.forEach(key => { + try { + localStorage.removeItem(key); + } catch (error) { + logger.error('Failed to remove cache entry:', error); + } + }); + } + } catch (error) { + logger.error('Failed to cleanup translation cache:', error); + } +}; + +// Performance monitoring functions +export const getPerformanceMetrics = () => ({ + ...performanceMetrics, + cacheEfficiency: performanceMetrics.cacheHits / + (performanceMetrics.cacheHits + performanceMetrics.cacheMisses) * 100, + averageLoadTime: performanceMetrics.totalLoadTime / performanceMetrics.translationsLoaded, +}); + +export const resetPerformanceMetrics = () => { + performanceMetrics.totalLoadTime = 0; + performanceMetrics.translationsLoaded = 0; + performanceMetrics.cacheHits = 0; + performanceMetrics.cacheMisses = 0; +}; + +// Utility function to preload translations for a specific page/component +export const preloadPageTranslations = async (pageNamespaces: string[]) => { + const currentLang = i18n.language || 'en'; + return ensureTranslationsLoaded(pageNamespaces, [currentLang], 8); +}; + +// Set up periodic cache cleanup +if (typeof window !== 'undefined') { + setInterval(cleanupCache, CACHE_CONFIG.CLEANUP_INTERVAL); + + // Cleanup on page unload + window.addEventListener('beforeunload', cleanupCache); +} + +// Initialize translations on app startup initializeTranslations(); export default i18n; diff --git a/worklenz-frontend/src/index.tsx b/worklenz-frontend/src/index.tsx index cf2c161c..c8cf3b99 100644 --- a/worklenz-frontend/src/index.tsx +++ b/worklenz-frontend/src/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; +import './styles/performance-optimizations.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import './i18n'; @@ -10,12 +11,16 @@ import { applyCssVariables } from './styles/colors'; import { ConfigProvider, theme } from 'antd'; import { colors } from './styles/colors'; import { getInitialTheme } from './utils/get-initial-theme'; +import { initializePerformanceMonitoring } from './utils/enhanced-performance-monitoring'; const initialTheme = getInitialTheme(); // Apply CSS variables and initial theme applyCssVariables(); +// Initialize enhanced performance monitoring +initializePerformanceMonitoring(); + const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); document.documentElement.classList.add(initialTheme); diff --git a/worklenz-frontend/src/layouts/MainLayout.tsx b/worklenz-frontend/src/layouts/MainLayout.tsx index 53f31bc7..8e3cc66b 100644 --- a/worklenz-frontend/src/layouts/MainLayout.tsx +++ b/worklenz-frontend/src/layouts/MainLayout.tsx @@ -1,6 +1,6 @@ import { Col, ConfigProvider, Layout } from 'antd'; import { Outlet, useNavigate } from 'react-router-dom'; -import { memo, useMemo } from 'react'; +import { memo, useMemo, useEffect, useRef } from 'react'; import { useMediaQuery } from 'react-responsive'; import Navbar from '../features/navbar/navbar'; @@ -10,15 +10,30 @@ import { colors } from '../styles/colors'; import { useRenderPerformance } from '@/utils/performance'; import HubSpot from '@/components/HubSpot'; +import { DynamicCSSLoader, LayoutStabilizer } from '@/utils/css-optimizations'; const MainLayout = memo(() => { const themeMode = useAppSelector(state => state.themeReducer.mode); const isDesktop = useMediaQuery({ query: '(min-width: 1024px)' }); - + const layoutRef = useRef(null); // Performance monitoring in development useRenderPerformance('MainLayout'); + // Apply layout optimizations + useEffect(() => { + if (layoutRef.current) { + // Prevent layout shifts in main content area + LayoutStabilizer.applyContainment(layoutRef.current, 'layout'); + + // Load non-critical CSS dynamically + DynamicCSSLoader.loadCSS('/styles/non-critical.css', { + priority: 'low', + media: 'all' + }); + } + }, []); + // Memoize styles to prevent object recreation on every render @@ -64,13 +79,13 @@ const MainLayout = memo(() => { return ( - - + + - - + + diff --git a/worklenz-frontend/src/socket/socketContext.tsx b/worklenz-frontend/src/socket/socketContext.tsx index c77ba658..c20d676c 100644 --- a/worklenz-frontend/src/socket/socketContext.tsx +++ b/worklenz-frontend/src/socket/socketContext.tsx @@ -8,6 +8,9 @@ import { Modal, message } from 'antd'; import { SocketEvents } from '@/shared/socket-events'; import { getUserSession } from '@/utils/session-helper'; +// Global socket instance to prevent multiple connections in StrictMode +let globalSocketInstance: Socket | null = null; + interface SocketContextType { socket: Socket | null; connected: boolean; @@ -24,12 +27,30 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ childr const profile = getUserSession(); // Adjust based on your Redux structure const [messageApi, messageContextHolder] = message.useMessage(); // Add message API const hasShownConnectedMessage = useRef(false); // Add ref to track if message was shown + const isInitialized = useRef(false); // Track if socket is already initialized + const messageApiRef = useRef(messageApi); + const tRef = useRef(t); + + // Update refs when values change + useEffect(() => { + messageApiRef.current = messageApi; + }, [messageApi]); + + useEffect(() => { + tRef.current = t; + }, [t]); // Initialize socket connection useEffect(() => { - // Only create a new socket if one doesn't exist - if (!socketRef.current) { - socketRef.current = io(SOCKET_CONFIG.url, { + // Prevent duplicate initialization + if (isInitialized.current) { + return; + } + + // Only create a new socket if one doesn't exist globally or locally + if (!socketRef.current && !globalSocketInstance) { + isInitialized.current = true; + globalSocketInstance = io(SOCKET_CONFIG.url, { ...SOCKET_CONFIG.options, reconnection: true, reconnectionAttempts: Infinity, @@ -37,10 +58,18 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ childr reconnectionDelayMax: 5000, timeout: 20000, }); + socketRef.current = globalSocketInstance; + } else if (globalSocketInstance && !socketRef.current) { + // Reuse existing global socket instance + socketRef.current = globalSocketInstance; + isInitialized.current = true; } const socket = socketRef.current; + // Only proceed if socket exists + if (!socket) return; + // Set up event listeners before connecting socket.on('connect', () => { logger.info('Socket connected'); @@ -48,7 +77,7 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ childr // Only show connected message once if (!hasShownConnectedMessage.current) { - messageApi.success(t('connection-restored')); + messageApiRef.current.success(tRef.current('connection-restored')); hasShownConnectedMessage.current = true; } }); @@ -64,7 +93,7 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ childr socket.on('connect_error', error => { logger.error('Connection error', { error }); setConnected(false); - messageApi.error(t('connection-lost')); + messageApiRef.current.error(tRef.current('connection-lost')); // Reset the connected message flag on error hasShownConnectedMessage.current = false; }); @@ -72,7 +101,7 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ childr socket.on('disconnect', () => { logger.info('Socket disconnected'); setConnected(false); - messageApi.loading(t('reconnecting')); + messageApiRef.current.loading(tRef.current('reconnecting')); // Reset the connected message flag on disconnect hasShownConnectedMessage.current = false; @@ -121,10 +150,12 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ childr // Then close the connection socket.close(); socketRef.current = null; + globalSocketInstance = null; // Clear global instance hasShownConnectedMessage.current = false; // Reset on unmount + isInitialized.current = false; // Reset initialization flag } }; - }, [messageApi, t]); // Add messageApi and t to dependencies + }, []); // Remove dependencies to prevent re-initialization const value = { socket: socketRef.current, diff --git a/worklenz-frontend/src/styles/performance-optimizations.css b/worklenz-frontend/src/styles/performance-optimizations.css new file mode 100644 index 00000000..4907234a --- /dev/null +++ b/worklenz-frontend/src/styles/performance-optimizations.css @@ -0,0 +1,296 @@ +/* Performance Optimization Styles for Worklenz */ + +/* Layout shift prevention */ +.prevent-layout-shift { + contain: layout style; +} + +/* Efficient animations */ +.gpu-accelerated { + transform: translateZ(0); + will-change: transform; +} + +.efficient-transition { + transition: transform 0.2s ease-out, opacity 0.2s ease-out; +} + +/* Critical loading states */ +.critical-loading { + background: linear-gradient(90deg, #f0f0f0 25%, transparent 37%, #f0f0f0 63%); + background-size: 400% 100%; + animation: shimmer 1.5s ease-in-out infinite; +} + +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +/* Font loading optimization */ +.font-loading { + font-display: swap; +} + +/* Container queries for responsive design */ +.container-responsive { + container-type: inline-size; +} + +@container (min-width: 300px) { + .container-responsive .content { + display: grid; + grid-template-columns: 1fr 1fr; + } +} + +/* CSS containment for performance */ +.layout-contained { + contain: layout; +} + +.paint-contained { + contain: paint; +} + +.size-contained { + contain: size; +} + +.style-contained { + contain: style; +} + +/* Optimized scrolling */ +.smooth-scroll { + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; +} + +/* Prevent repaints during animations */ +.animation-optimized { + backface-visibility: hidden; + perspective: 1000px; +} + +/* Critical path optimizations */ +.above-fold { + priority: 1; +} + +.below-fold { + priority: 0; +} + +/* Resource hints via CSS */ +.preload-critical::before { + content: ''; + display: block; + width: 0; + height: 0; + background-image: url('/critical-image.webp'); +} + +/* Task management specific optimizations */ +.task-list-board { + contain: layout style; +} + +.task-groups-container-fixed { + contain: strict; + transform: translateZ(0); +} + +.task-row { + contain: layout style; + will-change: transform; +} + +.task-row:hover { + transform: translateZ(0); +} + +/* Virtualized components */ +.virtualized-task-groups { + contain: strict; + transform: translateZ(0); +} + +/* Bulk action bar optimizations */ +.optimized-bulk-action-bar { + contain: layout style; + transform: translateZ(0); +} + +/* Loading state optimizations */ +.task-loading-skeleton { + contain: layout; + animation: shimmer 1.5s ease-in-out infinite; +} + +/* Avatar and image optimizations */ +.lazy-image { + opacity: 0; + transition: opacity 0.3s ease-in-out; +} + +.lazy-image.loaded { + opacity: 1; +} + +.lazy-image.loading { + background: linear-gradient(90deg, #f0f0f0 25%, transparent 37%, #f0f0f0 63%); + background-size: 400% 100%; + animation: shimmer 1s ease-in-out infinite; +} + +/* Reduce layout shifts for dynamic content */ +.task-content-container { + min-height: 40px; + contain-intrinsic-size: auto 40px; +} + +.project-content-container { + min-height: 60px; + contain-intrinsic-size: auto 60px; +} + +/* Performance-optimized grid layouts */ +.task-grid-optimized { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 16px; + contain: layout; +} + +.task-grid-optimized .task-card { + contain: layout style; + transform: translateZ(0); +} + +/* Dark mode optimizations */ +[data-theme="dark"] .critical-loading { + background: linear-gradient(90deg, #2a2a2a 25%, transparent 37%, #2a2a2a 63%); +} + +[data-theme="dark"] .lazy-image.loading { + background: linear-gradient(90deg, #2a2a2a 25%, transparent 37%, #2a2a2a 63%); +} + +/* Print optimizations */ +@media print { + .gpu-accelerated, + .animation-optimized { + transform: none; + will-change: auto; + animation: none; + } +} + +/* High contrast mode optimizations */ +@media (prefers-contrast: high) { + .critical-loading, + .lazy-image.loading { + background: repeating-linear-gradient( + 45deg, + transparent, + transparent 10px, + rgba(0, 0, 0, 0.1) 10px, + rgba(0, 0, 0, 0.1) 20px + ); + } +} + +/* Reduced motion optimizations */ +@media (prefers-reduced-motion: reduce) { + .efficient-transition, + .critical-loading, + .lazy-image { + animation: none; + transition: none; + } +} + +/* Viewport-based optimizations */ +@media (max-width: 768px) { + .task-grid-optimized { + grid-template-columns: 1fr; + } + + .prevent-layout-shift { + contain: layout; + } +} + +/* Memory-conscious styles for large lists */ +.large-list-container { + contain: strict; + content-visibility: auto; +} + +.large-list-item { + contain: layout style; + content-visibility: auto; + contain-intrinsic-size: auto 50px; +} + +/* Performance monitoring debug styles (dev only) */ +.performance-debug { + position: fixed; + top: 10px; + right: 10px; + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 8px; + border-radius: 4px; + font-size: 12px; + z-index: 9999; + display: none; +} + +.performance-debug.visible { + display: block; +} + +/* Critical CSS for above-the-fold content */ +.critical-above-fold { + /* Reset and basic typography */ + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; + + /* Layout grid */ + display: grid; + grid-template-areas: + "header header" + "sidebar main"; + grid-template-rows: auto 1fr; + grid-template-columns: 250px 1fr; + min-height: 100vh; + + /* Colors and spacing */ + background-color: #f5f5f5; + color: #333; + margin: 0; + padding: 0; +} + +/* Font display optimization */ +@font-face { + font-family: 'Inter'; + font-display: swap; + src: url('/fonts/inter-var.woff2') format('woff2'); +} + +/* Critical animations only */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.fade-in { + animation: fadeIn 0.3s ease-in-out; +} \ No newline at end of file diff --git a/worklenz-frontend/src/utils/asset-optimizations.ts b/worklenz-frontend/src/utils/asset-optimizations.ts new file mode 100644 index 00000000..89392847 --- /dev/null +++ b/worklenz-frontend/src/utils/asset-optimizations.ts @@ -0,0 +1,588 @@ +// Asset optimization utilities for improved performance + +// Image optimization constants +export const IMAGE_OPTIMIZATION = { + // Quality settings for different use cases + QUALITY: { + THUMBNAIL: 70, + AVATAR: 80, + CONTENT: 85, + HIGH_QUALITY: 95, + }, + + // Size presets for responsive images + SIZES: { + THUMBNAIL: { width: 64, height: 64 }, + AVATAR_SMALL: { width: 32, height: 32 }, + AVATAR_MEDIUM: { width: 48, height: 48 }, + AVATAR_LARGE: { width: 64, height: 64 }, + ICON_SMALL: { width: 16, height: 16 }, + ICON_MEDIUM: { width: 24, height: 24 }, + ICON_LARGE: { width: 32, height: 32 }, + CARD_IMAGE: { width: 300, height: 200 }, + }, + + // Supported formats in order of preference + FORMATS: ['webp', 'jpeg', 'png'], + + // Browser support detection + WEBP_SUPPORT: typeof window !== 'undefined' && + window.document?.createElement('canvas').toDataURL('image/webp').indexOf('webp') > -1, +} as const; + +// Asset caching strategies +export const CACHE_STRATEGIES = { + // Cache durations in seconds + DURATIONS: { + STATIC_ASSETS: 31536000, // 1 year + IMAGES: 2592000, // 30 days + AVATARS: 86400, // 1 day + DYNAMIC_CONTENT: 3600, // 1 hour + }, + + // Cache keys + KEYS: { + COMPRESSED_IMAGES: 'compressed_images', + AVATAR_CACHE: 'avatar_cache', + ICON_CACHE: 'icon_cache', + STATIC_ASSETS: 'static_assets', + }, +} as const; + +// Image compression utilities +export class ImageOptimizer { + private static canvas: HTMLCanvasElement | null = null; + private static ctx: CanvasRenderingContext2D | null = null; + + private static getCanvas(): HTMLCanvasElement { + if (!this.canvas) { + this.canvas = document.createElement('canvas'); + this.ctx = this.canvas.getContext('2d'); + } + return this.canvas; + } + + // Compress image with quality and size options + static async compressImage( + file: File | string, + options: { + quality?: number; + maxWidth?: number; + maxHeight?: number; + format?: 'jpeg' | 'webp' | 'png'; + } = {} + ): Promise { + const { + quality = IMAGE_OPTIMIZATION.QUALITY.CONTENT, + maxWidth = 1920, + maxHeight = 1080, + format = 'jpeg', + } = options; + + return new Promise((resolve, reject) => { + const img = new Image(); + + img.onload = () => { + try { + const canvas = this.getCanvas(); + const ctx = this.ctx!; + + // Calculate optimal dimensions + const { width, height } = this.calculateOptimalSize( + img.width, + img.height, + maxWidth, + maxHeight + ); + + canvas.width = width; + canvas.height = height; + + // Clear canvas and draw resized image + ctx.clearRect(0, 0, width, height); + ctx.drawImage(img, 0, 0, width, height); + + // Convert to optimized format + const mimeType = format === 'jpeg' ? 'image/jpeg' : + format === 'webp' ? 'image/webp' : 'image/png'; + + const compressedDataUrl = canvas.toDataURL(mimeType, quality / 100); + resolve(compressedDataUrl); + } catch (error) { + reject(error); + } + }; + + img.onerror = reject; + + if (typeof file === 'string') { + img.src = file; + } else { + const reader = new FileReader(); + reader.onload = (e) => { + img.src = e.target?.result as string; + }; + reader.readAsDataURL(file); + } + }); + } + + // Calculate optimal size maintaining aspect ratio + private static calculateOptimalSize( + originalWidth: number, + originalHeight: number, + maxWidth: number, + maxHeight: number + ): { width: number; height: number } { + const aspectRatio = originalWidth / originalHeight; + + let width = originalWidth; + let height = originalHeight; + + // Scale down if necessary + if (width > maxWidth) { + width = maxWidth; + height = width / aspectRatio; + } + + if (height > maxHeight) { + height = maxHeight; + width = height * aspectRatio; + } + + return { + width: Math.round(width), + height: Math.round(height), + }; + } + + // Generate responsive image srcSet + static generateSrcSet( + baseUrl: string, + sizes: Array<{ width: number; quality?: number }> + ): string { + return sizes + .map(({ width, quality = IMAGE_OPTIMIZATION.QUALITY.CONTENT }) => { + const url = `${baseUrl}?w=${width}&q=${quality}${ + IMAGE_OPTIMIZATION.WEBP_SUPPORT ? '&f=webp' : '' + }`; + return `${url} ${width}w`; + }) + .join(', '); + } + + // Create optimized avatar URL + static getOptimizedAvatarUrl( + baseUrl: string, + size: keyof typeof IMAGE_OPTIMIZATION.SIZES = 'AVATAR_MEDIUM' + ): string { + const dimensions = IMAGE_OPTIMIZATION.SIZES[size]; + const quality = IMAGE_OPTIMIZATION.QUALITY.AVATAR; + + return `${baseUrl}?w=${dimensions.width}&h=${dimensions.height}&q=${quality}${ + IMAGE_OPTIMIZATION.WEBP_SUPPORT ? '&f=webp' : '' + }`; + } + + // Create optimized icon URL + static getOptimizedIconUrl( + baseUrl: string, + size: keyof typeof IMAGE_OPTIMIZATION.SIZES = 'ICON_MEDIUM' + ): string { + const dimensions = IMAGE_OPTIMIZATION.SIZES[size]; + + return `${baseUrl}?w=${dimensions.width}&h=${dimensions.height}&q=100${ + IMAGE_OPTIMIZATION.WEBP_SUPPORT ? '&f=webp' : '' + }`; + } +} + +// Asset caching utilities +export class AssetCache { + private static cache = new Map(); + + // Set item in cache with TTL + static set(key: string, data: any, duration: number = CACHE_STRATEGIES.DURATIONS.DYNAMIC_CONTENT): void { + this.cache.set(key, { + data, + timestamp: Date.now(), + duration: duration * 1000, // Convert to milliseconds + }); + + // Clean up expired items periodically + if (this.cache.size % 50 === 0) { + this.cleanup(); + } + } + + // Get item from cache + static get(key: string): T | null { + const item = this.cache.get(key); + + if (!item) return null; + + // Check if expired + if (Date.now() - item.timestamp > item.duration) { + this.cache.delete(key); + return null; + } + + return item.data; + } + + // Remove expired items + static cleanup(): void { + const now = Date.now(); + for (const [key, item] of this.cache.entries()) { + if (now - item.timestamp > item.duration) { + this.cache.delete(key); + } + } + } + + // Clear all cache + static clear(): void { + this.cache.clear(); + } + + // Get cache size and statistics + static getStats(): { size: number; totalItems: number; hitRate: number } { + return { + size: this.cache.size, + totalItems: this.cache.size, + hitRate: 0, // Could be implemented with counters + }; + } +} + +// Lazy loading utilities +export class LazyLoader { + private static observer: IntersectionObserver | null = null; + private static loadedImages = new Set(); + + // Initialize intersection observer + private static getObserver(): IntersectionObserver { + if (!this.observer) { + this.observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const img = entry.target as HTMLImageElement; + this.loadImage(img); + this.observer?.unobserve(img); + } + }); + }, + { + rootMargin: '50px', // Start loading 50px before entering viewport + threshold: 0.1, + } + ); + } + return this.observer; + } + + // Setup lazy loading for an image + static setupLazyLoading(img: HTMLImageElement, src: string): void { + if (this.loadedImages.has(src)) { + img.src = src; + return; + } + + img.dataset.src = src; + img.classList.add('lazy-loading'); + this.getObserver().observe(img); + } + + // Load image and handle caching + private static loadImage(img: HTMLImageElement): void { + const src = img.dataset.src; + if (!src) return; + + // Check cache first + const cachedBlob = AssetCache.get(`image_${src}`); + if (cachedBlob) { + img.src = cachedBlob; + img.classList.remove('lazy-loading'); + img.classList.add('lazy-loaded'); + this.loadedImages.add(src); + return; + } + + // Load and cache image + const newImg = new Image(); + newImg.onload = () => { + img.src = src; + img.classList.remove('lazy-loading'); + img.classList.add('lazy-loaded'); + this.loadedImages.add(src); + + // Cache for future use + AssetCache.set(`image_${src}`, src, CACHE_STRATEGIES.DURATIONS.IMAGES); + }; + newImg.onerror = () => { + img.classList.remove('lazy-loading'); + img.classList.add('lazy-error'); + }; + newImg.src = src; + } + + // Preload critical images + static preloadCriticalImages(urls: string[]): Promise { + return Promise.all( + urls.map((url) => { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + this.loadedImages.add(url); + AssetCache.set(`image_${url}`, url, CACHE_STRATEGIES.DURATIONS.IMAGES); + resolve(); + }; + img.onerror = reject; + img.src = url; + }); + }) + ); + } +} + +// Progressive loading utilities +export class ProgressiveLoader { + // Create progressive JPEG-like loading effect + static createProgressiveImage( + container: HTMLElement, + lowQualitySrc: string, + highQualitySrc: string + ): void { + const lowQualityImg = document.createElement('img'); + const highQualityImg = document.createElement('img'); + + // Style for smooth transition + const baseStyle = { + position: 'absolute' as const, + top: '0', + left: '0', + width: '100%', + height: '100%', + objectFit: 'cover' as const, + }; + + Object.assign(lowQualityImg.style, baseStyle, { + filter: 'blur(2px)', + transition: 'opacity 0.3s ease', + }); + + Object.assign(highQualityImg.style, baseStyle, { + opacity: '0', + transition: 'opacity 0.3s ease', + }); + + // Load low quality first + lowQualityImg.src = lowQualitySrc; + container.appendChild(lowQualityImg); + container.appendChild(highQualityImg); + + // Load high quality and fade in + highQualityImg.onload = () => { + highQualityImg.style.opacity = '1'; + setTimeout(() => { + lowQualityImg.remove(); + }, 300); + }; + highQualityImg.src = highQualitySrc; + } +} + +// Asset preloading strategies +export class AssetPreloader { + private static preloadedAssets = new Set(); + + // Preload assets based on priority + static preloadAssets(assets: Array<{ url: string; priority: 'high' | 'medium' | 'low' }>): void { + // Sort by priority + assets.sort((a, b) => { + const priorityOrder = { high: 0, medium: 1, low: 2 }; + return priorityOrder[a.priority] - priorityOrder[b.priority]; + }); + + // Preload high priority assets immediately + const highPriorityAssets = assets.filter(asset => asset.priority === 'high'); + this.preloadImmediately(highPriorityAssets.map(a => a.url)); + + // Preload medium priority assets after a short delay + setTimeout(() => { + const mediumPriorityAssets = assets.filter(asset => asset.priority === 'medium'); + this.preloadWithIdleCallback(mediumPriorityAssets.map(a => a.url)); + }, 100); + + // Preload low priority assets when browser is idle + setTimeout(() => { + const lowPriorityAssets = assets.filter(asset => asset.priority === 'low'); + this.preloadWithIdleCallback(lowPriorityAssets.map(a => a.url)); + }, 1000); + } + + // Immediate preloading for critical assets + private static preloadImmediately(urls: string[]): void { + urls.forEach(url => { + if (this.preloadedAssets.has(url)) return; + + const link = document.createElement('link'); + link.rel = 'preload'; + link.href = url; + + // Determine asset type + if (url.match(/\.(jpg|jpeg|png|webp|gif)$/i)) { + link.as = 'image'; + } else if (url.match(/\.(woff|woff2|ttf|otf)$/i)) { + link.as = 'font'; + link.crossOrigin = 'anonymous'; + } else if (url.match(/\.(css)$/i)) { + link.as = 'style'; + } else if (url.match(/\.(js)$/i)) { + link.as = 'script'; + } + + document.head.appendChild(link); + this.preloadedAssets.add(url); + }); + } + + // Preload with idle callback for non-critical assets + private static preloadWithIdleCallback(urls: string[]): void { + const preloadBatch = () => { + urls.forEach(url => { + if (this.preloadedAssets.has(url)) return; + + const img = new Image(); + img.src = url; + this.preloadedAssets.add(url); + }); + }; + + if ('requestIdleCallback' in window) { + (window as any).requestIdleCallback(preloadBatch, { timeout: 2000 }); + } else { + setTimeout(preloadBatch, 100); + } + } +} + +// CSS for optimized image loading +export const imageOptimizationStyles = ` +/* Lazy loading states */ +.lazy-loading { + background: linear-gradient(90deg, #f0f0f0 25%, transparent 37%, #f0f0f0 63%); + background-size: 400% 100%; + animation: shimmer 1.5s ease-in-out infinite; +} + +.lazy-loaded { + animation: fadeIn 0.3s ease-in-out; +} + +.lazy-error { + background-color: #f5f5f5; + display: flex; + align-items: center; + justify-content: center; +} + +.lazy-error::after { + content: '⚠️'; + font-size: 24px; + opacity: 0.5; +} + +/* Shimmer animation for loading */ +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +/* Fade in animation */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +/* Progressive image container */ +.progressive-image { + position: relative; + overflow: hidden; + background-color: #f5f5f5; +} + +/* Responsive image utilities */ +.responsive-image { + width: 100%; + height: auto; + max-width: 100%; +} + +/* Avatar optimization */ +.optimized-avatar { + border-radius: 50%; + object-fit: cover; + background-color: #e5e7eb; +} + +/* Icon optimization */ +.optimized-icon { + display: inline-block; + vertical-align: middle; +} + +/* Preload critical images */ +.critical-image { + object-fit: cover; + background-color: #f5f5f5; +} +`; + +// Utility functions +export const AssetUtils = { + // Get file size from data URL + getDataUrlSize: (dataUrl: string): number => { + const base64String = dataUrl.split(',')[1]; + return Math.round((base64String.length * 3) / 4); + }, + + // Convert file size to human readable format + formatFileSize: (bytes: number): string => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }, + + // Generate low quality placeholder + generatePlaceholder: (width: number, height: number, color: string = '#e5e7eb'): string => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d')!; + + canvas.width = width; + canvas.height = height; + + ctx.fillStyle = color; + ctx.fillRect(0, 0, width, height); + + return canvas.toDataURL('image/png'); + }, + + // Check if image is already cached + isImageCached: (url: string): boolean => { + return AssetCache.get(`image_${url}`) !== null; + }, + + // Prefetch critical resources + prefetchResources: (urls: string[]): void => { + urls.forEach(url => { + const link = document.createElement('link'); + link.rel = 'prefetch'; + link.href = url; + document.head.appendChild(link); + }); + }, +}; \ No newline at end of file diff --git a/worklenz-frontend/src/utils/css-optimizations.ts b/worklenz-frontend/src/utils/css-optimizations.ts new file mode 100644 index 00000000..8889e5e1 --- /dev/null +++ b/worklenz-frontend/src/utils/css-optimizations.ts @@ -0,0 +1,598 @@ +// CSS optimization utilities for improved performance and reduced layout shifts + +// Critical CSS constants +export const CSS_OPTIMIZATION = { + // Performance thresholds + THRESHOLDS: { + CRITICAL_CSS_SIZE: 14000, // 14KB critical CSS limit + INLINE_CSS_LIMIT: 4000, // 4KB inline CSS limit + UNUSED_CSS_THRESHOLD: 80, // Remove CSS with <80% usage + }, + + // Layout shift prevention + LAYOUT_PREVENTION: { + // Common aspect ratios for media + ASPECT_RATIOS: { + SQUARE: '1:1', + LANDSCAPE: '16:9', + PORTRAIT: '9:16', + CARD: '4:3', + WIDE: '21:9', + }, + + // Standard sizes for common elements + PLACEHOLDER_SIZES: { + AVATAR: { width: 40, height: 40 }, + BUTTON: { width: 120, height: 36 }, + INPUT: { width: 200, height: 40 }, + CARD: { width: 300, height: 200 }, + THUMBNAIL: { width: 64, height: 64 }, + }, + }, + + // CSS optimization strategies + STRATEGIES: { + CRITICAL_ABOVE_FOLD: ['layout', 'typography', 'colors', 'spacing'], + DEFER_BELOW_FOLD: ['animations', 'hover-effects', 'non-critical-components'], + INLINE_CRITICAL: ['reset', 'grid', 'typography', 'critical-components'], + }, +} as const; + +// CSS performance monitoring +export class CSSPerformanceMonitor { + private static metrics = { + layoutShifts: 0, + renderBlockingCSS: 0, + unusedCSS: 0, + criticalCSSSize: 0, + }; + + // Monitor Cumulative Layout Shift (CLS) + static monitorLayoutShifts(): () => void { + if (!('PerformanceObserver' in window)) { + return () => {}; + } + + const observer = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + if (entry.entryType === 'layout-shift' && !(entry as any).hadRecentInput) { + this.metrics.layoutShifts += (entry as any).value; + } + } + }); + + observer.observe({ type: 'layout-shift', buffered: true }); + + return () => observer.disconnect(); + } + + // Monitor render-blocking resources + static monitorRenderBlocking(): void { + if ('PerformanceObserver' in window) { + const observer = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + if (entry.name.endsWith('.css') && (entry as any).renderBlockingStatus === 'blocking') { + this.metrics.renderBlockingCSS++; + } + } + }); + + observer.observe({ type: 'resource', buffered: true }); + } + } + + // Get current metrics + static getMetrics() { + return { ...this.metrics }; + } + + // Reset metrics + static reset(): void { + this.metrics = { + layoutShifts: 0, + renderBlockingCSS: 0, + unusedCSS: 0, + criticalCSSSize: 0, + }; + } +} + +// Layout shift prevention utilities +export class LayoutStabilizer { + // Create placeholder with known dimensions + static createPlaceholder( + element: HTMLElement, + dimensions: { width?: number; height?: number; aspectRatio?: string } + ): void { + const { width, height, aspectRatio } = dimensions; + + if (aspectRatio) { + element.style.aspectRatio = aspectRatio; + } + + if (width) { + element.style.width = `${width}px`; + } + + if (height) { + element.style.height = `${height}px`; + } + + // Prevent layout shifts during loading + element.style.minHeight = height ? `${height}px` : '1px'; + element.style.containIntrinsicSize = width && height ? `${width}px ${height}px` : 'auto'; + } + + // Reserve space for dynamic content + static reserveSpace( + container: HTMLElement, + estimatedHeight: number, + adjustOnLoad: boolean = true + ): () => void { + const originalHeight = container.style.height; + container.style.minHeight = `${estimatedHeight}px`; + + if (adjustOnLoad) { + const observer = new ResizeObserver(() => { + if (container.scrollHeight > estimatedHeight) { + container.style.minHeight = 'auto'; + observer.disconnect(); + } + }); + observer.observe(container); + + return () => observer.disconnect(); + } + + return () => { + container.style.height = originalHeight; + container.style.minHeight = 'auto'; + }; + } + + // Preload fonts to prevent text layout shifts + static preloadFonts(fontFaces: Array<{ family: string; weight?: string; style?: string }>): void { + fontFaces.forEach(({ family, weight = '400', style = 'normal' }) => { + const link = document.createElement('link'); + link.rel = 'preload'; + link.as = 'font'; + link.type = 'font/woff2'; + link.crossOrigin = 'anonymous'; + link.href = `/fonts/${family}-${weight}-${style}.woff2`; + document.head.appendChild(link); + }); + } + + // Apply size-based CSS containment + static applyContainment(element: HTMLElement, type: 'size' | 'layout' | 'style' | 'paint'): void { + element.style.contain = type; + } +} + +// Critical CSS management +export class CriticalCSSManager { + private static criticalCSS = new Set(); + private static deferredCSS = new Set(); + + // Identify critical CSS selectors + static identifyCriticalCSS(): string[] { + const criticalSelectors: string[] = []; + + // Get above-the-fold elements + const viewportHeight = window.innerHeight; + const aboveFoldElements = Array.from(document.querySelectorAll('*')).filter( + (el) => el.getBoundingClientRect().top < viewportHeight + ); + + // Extract CSS rules for above-the-fold elements + aboveFoldElements.forEach((element) => { + const computedStyle = window.getComputedStyle(element); + const tagName = element.tagName.toLowerCase(); + const className = element.className; + const id = element.id; + + // Add tag selectors + criticalSelectors.push(tagName); + + // Add class selectors + if (className) { + className.split(' ').forEach((cls) => { + criticalSelectors.push(`.${cls}`); + }); + } + + // Add ID selectors + if (id) { + criticalSelectors.push(`#${id}`); + } + }); + + return Array.from(new Set(criticalSelectors)); + } + + // Extract critical CSS + static async extractCriticalCSS(html: string, css: string): Promise { + // This is a simplified version - in production, use tools like critical or penthouse + const criticalSelectors = this.identifyCriticalCSS(); + const criticalRules: string[] = []; + + // Parse CSS and extract matching rules + const cssRules = css.split('}').map(rule => rule.trim() + '}'); + + cssRules.forEach((rule) => { + for (const selector of criticalSelectors) { + if (rule.includes(selector)) { + criticalRules.push(rule); + } + } + }); + + return criticalRules.join('\n'); + } + + // Inline critical CSS + static inlineCriticalCSS(css: string): void { + const style = document.createElement('style'); + style.textContent = css; + style.setAttribute('data-critical', 'true'); + document.head.insertBefore(style, document.head.firstChild); + } + + // Load non-critical CSS asynchronously + static loadNonCriticalCSS(href: string, media: string = 'all'): void { + const link = document.createElement('link'); + link.rel = 'preload'; + link.as = 'style'; + link.href = href; + link.media = 'print'; // Load as print to avoid blocking + link.onload = () => { + link.media = media; // Switch to target media once loaded + }; + document.head.appendChild(link); + } +} + +// CSS optimization utilities +export class CSSOptimizer { + // Remove unused CSS selectors + static removeUnusedCSS(css: string): string { + const usedSelectors = new Set(); + + // Get all elements and their classes/IDs + document.querySelectorAll('*').forEach((element) => { + usedSelectors.add(element.tagName.toLowerCase()); + + if (element.className) { + element.className.split(' ').forEach((cls) => { + usedSelectors.add(`.${cls}`); + }); + } + + if (element.id) { + usedSelectors.add(`#${element.id}`); + } + }); + + // Filter CSS rules + const cssRules = css.split('}'); + const optimizedRules = cssRules.filter((rule) => { + const selectorPart = rule.split('{')[0]; + if (!selectorPart) return false; + const selector = selectorPart.trim(); + if (!selector) return false; + + // Check if selector is used + return Array.from(usedSelectors).some((used) => + selector.includes(used) + ); + }); + + return optimizedRules.join('}'); + } + + // Minify CSS + static minifyCSS(css: string): string { + return css + // Remove comments + .replace(/\/\*[\s\S]*?\*\//g, '') + // Remove unnecessary whitespace + .replace(/\s+/g, ' ') + // Remove whitespace around selectors and properties + .replace(/\s*{\s*/g, '{') + .replace(/;\s*/g, ';') + .replace(/}\s*/g, '}') + // Remove trailing semicolons + .replace(/;}/g, '}') + .trim(); + } + + // Bundle CSS efficiently + static bundleCSS(cssFiles: string[]): Promise { + return Promise.all( + cssFiles.map(async (file) => { + const response = await fetch(file); + return response.text(); + }) + ).then((styles) => { + const bundled = styles.join('\n'); + return this.minifyCSS(bundled); + }); + } +} + +// Dynamic CSS loading utilities +export class DynamicCSSLoader { + private static loadedStylesheets = new Set(); + private static loadingPromises = new Map>(); + + // Load CSS on demand + static async loadCSS(href: string, options: { + media?: string; + priority?: 'high' | 'low'; + critical?: boolean; + } = {}): Promise { + const { media = 'all', priority = 'low', critical = false } = options; + + if (this.loadedStylesheets.has(href)) { + return Promise.resolve(); + } + + if (this.loadingPromises.has(href)) { + return this.loadingPromises.get(href)!; + } + + const promise = new Promise((resolve, reject) => { + const link = document.createElement('link'); + link.rel = critical ? 'stylesheet' : 'preload'; + link.as = critical ? undefined : 'style'; + link.href = href; + link.media = media; + + if (priority === 'high') { + link.setAttribute('importance', 'high'); + } + + link.onload = () => { + if (!critical) { + link.rel = 'stylesheet'; + } + this.loadedStylesheets.add(href); + this.loadingPromises.delete(href); + resolve(); + }; + + link.onerror = () => { + this.loadingPromises.delete(href); + reject(new Error(`Failed to load CSS: ${href}`)); + }; + + document.head.appendChild(link); + }); + + this.loadingPromises.set(href, promise); + return promise; + } + + // Load CSS based on component visibility + static loadCSSOnIntersection( + element: HTMLElement, + cssHref: string, + options: { rootMargin?: string; threshold?: number } = {} + ): () => void { + const { rootMargin = '100px', threshold = 0.1 } = options; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + this.loadCSS(cssHref); + observer.unobserve(element); + } + }); + }, + { rootMargin, threshold } + ); + + observer.observe(element); + + return () => observer.disconnect(); + } + + // Load CSS based on user interaction + static loadCSSOnInteraction( + element: HTMLElement, + cssHref: string, + events: string[] = ['mouseenter', 'touchstart'] + ): () => void { + const loadCSS = () => { + this.loadCSS(cssHref); + cleanup(); + }; + + const cleanup = () => { + events.forEach((event) => { + element.removeEventListener(event, loadCSS); + }); + }; + + events.forEach((event) => { + element.addEventListener(event, loadCSS, { once: true, passive: true }); + }); + + return cleanup; + } +} + +// CSS performance optimization styles +export const cssPerformanceStyles = ` +/* Layout shift prevention */ +.prevent-layout-shift { + contain: layout style; +} + +/* Efficient animations */ +.gpu-accelerated { + transform: translateZ(0); + will-change: transform; +} + +.efficient-transition { + transition: transform 0.2s ease-out, opacity 0.2s ease-out; +} + +/* Critical loading states */ +.critical-loading { + background: linear-gradient(90deg, #f0f0f0 25%, transparent 37%, #f0f0f0 63%); + background-size: 400% 100%; + animation: shimmer 1.5s ease-in-out infinite; +} + +/* Font loading optimization */ +.font-loading { + font-display: swap; +} + +/* Container queries for responsive design */ +.container-responsive { + container-type: inline-size; +} + +@container (min-width: 300px) { + .container-responsive .content { + display: grid; + grid-template-columns: 1fr 1fr; + } +} + +/* CSS containment for performance */ +.layout-contained { + contain: layout; +} + +.paint-contained { + contain: paint; +} + +.size-contained { + contain: size; +} + +.style-contained { + contain: style; +} + +/* Optimized scrolling */ +.smooth-scroll { + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; +} + +/* Prevent repaints during animations */ +.animation-optimized { + backface-visibility: hidden; + perspective: 1000px; +} + +/* Critical path optimizations */ +.above-fold { + priority: 1; +} + +.below-fold { + priority: 0; +} + +/* Resource hints via CSS */ +.preload-critical::before { + content: ''; + display: block; + width: 0; + height: 0; + background-image: url('/critical-image.webp'); +} +`; + +// Utility functions for CSS optimization +export const CSSUtils = { + // Calculate CSS specificity + calculateSpecificity: (selector: string): number => { + const idCount = (selector.match(/#/g) || []).length; + const classCount = (selector.match(/\./g) || []).length; + const elementCount = (selector.match(/[a-zA-Z]/g) || []).length; + + return idCount * 100 + classCount * 10 + elementCount; + }, + + // Check if CSS property is supported + isPropertySupported: (property: string, value: string): boolean => { + const element = document.createElement('div'); + element.style.setProperty(property, value); + return element.style.getPropertyValue(property) === value; + }, + + // Get critical viewport CSS + getCriticalViewportCSS: (): { width: number; height: number; ratio: number } => { + return { + width: window.innerWidth, + height: window.innerHeight, + ratio: window.innerWidth / window.innerHeight, + }; + }, + + // Optimize CSS custom properties + optimizeCustomProperties: (css: string): string => { + // Group related custom properties + const optimized = css.replace( + /:root\s*{([^}]*)}/g, + (match, properties) => { + const sorted = properties + .split(';') + .filter((prop: string) => prop.trim()) + .sort() + .join(';'); + return `:root{${sorted}}`; + } + ); + + return optimized; + }, + + // Generate responsive CSS + generateResponsiveCSS: ( + selector: string, + properties: Record, + breakpoints: Record + ): string => { + let css = `${selector} { ${Object.entries(properties).map(([prop, value]) => `${prop}: ${value}`).join('; ')} }`; + + Object.entries(breakpoints).forEach(([breakpoint, mediaQuery]) => { + css += `\n@media ${mediaQuery} { ${selector} { /* responsive styles */ } }`; + }); + + return css; + }, + + // Check for CSS performance issues + checkPerformanceIssues: (css: string): string[] => { + const issues: string[] = []; + + // Check for expensive selectors + if (css.includes('*')) { + issues.push('Universal selector (*) detected - may impact performance'); + } + + // Check for inefficient descendant selectors + const deepSelectors = css.match(/(\w+\s+){4,}/g); + if (deepSelectors) { + issues.push('Deep descendant selectors detected - consider using more specific classes'); + } + + // Check for !important overuse + const importantCount = (css.match(/!important/g) || []).length; + if (importantCount > 10) { + issues.push('Excessive use of !important detected'); + } + + return issues; + }, +}; \ No newline at end of file diff --git a/worklenz-frontend/src/utils/enhanced-performance-monitoring.ts b/worklenz-frontend/src/utils/enhanced-performance-monitoring.ts new file mode 100644 index 00000000..9e50424e --- /dev/null +++ b/worklenz-frontend/src/utils/enhanced-performance-monitoring.ts @@ -0,0 +1,680 @@ +// Enhanced performance monitoring for Worklenz application + +// Performance monitoring constants +export const PERFORMANCE_CONFIG = { + // Measurement thresholds + THRESHOLDS: { + FCP: 1800, // First Contentful Paint (ms) + LCP: 2500, // Largest Contentful Paint (ms) + FID: 100, // First Input Delay (ms) + CLS: 0.1, // Cumulative Layout Shift + TTFB: 600, // Time to First Byte (ms) + INP: 200, // Interaction to Next Paint (ms) + }, + + // Monitoring intervals + INTERVALS: { + METRICS_COLLECTION: 5000, // 5 seconds + PERFORMANCE_REPORT: 30000, // 30 seconds + CLEANUP_THRESHOLD: 300000, // 5 minutes + }, + + // Buffer sizes + BUFFERS: { + MAX_ENTRIES: 1000, + MAX_RESOURCE_ENTRIES: 500, + MAX_NAVIGATION_ENTRIES: 100, + }, +} as const; + +// Performance metrics interface +export interface PerformanceMetrics { + // Core Web Vitals + fcp?: number; + lcp?: number; + fid?: number; + cls?: number; + ttfb?: number; + inp?: number; + + // Custom metrics + domContentLoaded?: number; + windowLoad?: number; + firstByte?: number; + + // Application-specific metrics + taskLoadTime?: number; + projectSwitchTime?: number; + filterApplyTime?: number; + bulkActionTime?: number; + + // Memory and performance + memoryUsage?: { + usedJSHeapSize: number; + totalJSHeapSize: number; + jsHeapSizeLimit: number; + }; + + // Timing information + timestamp: number; + url: string; + userAgent: string; +} + +// Performance monitoring class +export class EnhancedPerformanceMonitor { + private static instance: EnhancedPerformanceMonitor; + private metrics: PerformanceMetrics[] = []; + private observers: PerformanceObserver[] = []; + private intervalIds: NodeJS.Timeout[] = []; + private isMonitoring = false; + + // Singleton pattern + static getInstance(): EnhancedPerformanceMonitor { + if (!this.instance) { + this.instance = new EnhancedPerformanceMonitor(); + } + return this.instance; + } + + // Start comprehensive performance monitoring + startMonitoring(): void { + if (this.isMonitoring) return; + + this.isMonitoring = true; + this.setupObservers(); + this.collectInitialMetrics(); + this.startPeriodicCollection(); + + console.log('🚀 Enhanced performance monitoring started'); + } + + // Stop monitoring and cleanup + stopMonitoring(): void { + if (!this.isMonitoring) return; + + this.isMonitoring = false; + this.cleanupObservers(); + this.clearIntervals(); + + console.log('🛑 Enhanced performance monitoring stopped'); + } + + // Setup performance observers + private setupObservers(): void { + if (!('PerformanceObserver' in window)) return; + + // Core Web Vitals observer + try { + const vitalsObserver = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + this.processVitalMetric(entry); + } + }); + + vitalsObserver.observe({ + type: 'largest-contentful-paint', + buffered: true + }); + + vitalsObserver.observe({ + type: 'first-input', + buffered: true + }); + + vitalsObserver.observe({ + type: 'layout-shift', + buffered: true + }); + + this.observers.push(vitalsObserver); + } catch (error) { + console.warn('Failed to setup vitals observer:', error); + } + + // Navigation timing observer + try { + const navigationObserver = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + this.processNavigationMetric(entry as PerformanceNavigationTiming); + } + }); + + navigationObserver.observe({ + type: 'navigation', + buffered: true + }); + + this.observers.push(navigationObserver); + } catch (error) { + console.warn('Failed to setup navigation observer:', error); + } + + // Resource timing observer + try { + const resourceObserver = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + this.processResourceMetric(entry as PerformanceResourceTiming); + } + }); + + resourceObserver.observe({ + type: 'resource', + buffered: true + }); + + this.observers.push(resourceObserver); + } catch (error) { + console.warn('Failed to setup resource observer:', error); + } + + // Measure observer + try { + const measureObserver = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + this.processCustomMeasure(entry as PerformanceMeasure); + } + }); + + measureObserver.observe({ + type: 'measure', + buffered: true + }); + + this.observers.push(measureObserver); + } catch (error) { + console.warn('Failed to setup measure observer:', error); + } + } + + // Process Core Web Vitals metrics + private processVitalMetric(entry: PerformanceEntry): void { + const metric: Partial = { + timestamp: Date.now(), + url: window.location.href, + userAgent: navigator.userAgent, + }; + + switch (entry.entryType) { + case 'largest-contentful-paint': + metric.lcp = entry.startTime; + break; + case 'first-input': + metric.fid = (entry as any).processingStart - entry.startTime; + break; + case 'layout-shift': + if (!(entry as any).hadRecentInput) { + metric.cls = (metric.cls || 0) + (entry as any).value; + } + break; + } + + this.addMetric(metric as PerformanceMetrics); + } + + // Process navigation timing metrics + private processNavigationMetric(entry: PerformanceNavigationTiming): void { + const metric: PerformanceMetrics = { + fcp: this.getFCP(), + ttfb: entry.responseStart - entry.requestStart, + domContentLoaded: entry.domContentLoadedEventEnd - entry.startTime, + windowLoad: entry.loadEventEnd - entry.startTime, + firstByte: entry.responseStart - entry.startTime, + timestamp: Date.now(), + url: window.location.href, + userAgent: navigator.userAgent, + }; + + this.addMetric(metric); + } + + // Process resource timing metrics + private processResourceMetric(entry: PerformanceResourceTiming): void { + // Track slow resources + const duration = entry.responseEnd - entry.requestStart; + + if (duration > 1000) { // Resources taking more than 1 second + console.warn(`Slow resource detected: ${entry.name} (${duration.toFixed(2)}ms)`); + } + + // Track render-blocking resources (check if property exists) + if ((entry as any).renderBlockingStatus === 'blocking') { + console.warn(`Render-blocking resource: ${entry.name}`); + } + } + + // Process custom performance measures + private processCustomMeasure(entry: PerformanceMeasure): void { + const metric: Partial = { + timestamp: Date.now(), + url: window.location.href, + userAgent: navigator.userAgent, + }; + + // Map custom measures to metrics + switch (entry.name) { + case 'task-load-time': + metric.taskLoadTime = entry.duration; + break; + case 'project-switch-time': + metric.projectSwitchTime = entry.duration; + break; + case 'filter-apply-time': + metric.filterApplyTime = entry.duration; + break; + case 'bulk-action-time': + metric.bulkActionTime = entry.duration; + break; + } + + if (Object.keys(metric).length > 3) { + this.addMetric(metric as PerformanceMetrics); + } + } + + // Get First Contentful Paint + private getFCP(): number | undefined { + const fcpEntry = performance.getEntriesByType('paint') + .find(entry => entry.name === 'first-contentful-paint'); + return fcpEntry?.startTime; + } + + // Collect initial metrics + private collectInitialMetrics(): void { + const metric: PerformanceMetrics = { + fcp: this.getFCP(), + timestamp: Date.now(), + url: window.location.href, + userAgent: navigator.userAgent, + }; + + // Add memory information if available + if ('memory' in performance) { + metric.memoryUsage = { + usedJSHeapSize: (performance as any).memory.usedJSHeapSize, + totalJSHeapSize: (performance as any).memory.totalJSHeapSize, + jsHeapSizeLimit: (performance as any).memory.jsHeapSizeLimit, + }; + } + + this.addMetric(metric); + } + + // Start periodic metrics collection + private startPeriodicCollection(): void { + // Collect metrics every 5 seconds + const metricsInterval = setInterval(() => { + this.collectPeriodicMetrics(); + }, PERFORMANCE_CONFIG.INTERVALS.METRICS_COLLECTION); + + // Generate performance report every 30 seconds + const reportInterval = setInterval(() => { + this.generatePerformanceReport(); + }, PERFORMANCE_CONFIG.INTERVALS.PERFORMANCE_REPORT); + + // Cleanup old metrics every 5 minutes + const cleanupInterval = setInterval(() => { + this.cleanupOldMetrics(); + }, PERFORMANCE_CONFIG.INTERVALS.CLEANUP_THRESHOLD); + + this.intervalIds.push(metricsInterval, reportInterval, cleanupInterval); + } + + // Collect periodic metrics + private collectPeriodicMetrics(): void { + const metric: PerformanceMetrics = { + timestamp: Date.now(), + url: window.location.href, + userAgent: navigator.userAgent, + }; + + // Add memory information if available + if ('memory' in performance) { + metric.memoryUsage = { + usedJSHeapSize: (performance as any).memory.usedJSHeapSize, + totalJSHeapSize: (performance as any).memory.totalJSHeapSize, + jsHeapSizeLimit: (performance as any).memory.jsHeapSizeLimit, + }; + } + + this.addMetric(metric); + } + + // Add metric to collection + private addMetric(metric: PerformanceMetrics): void { + this.metrics.push(metric); + + // Limit buffer size + if (this.metrics.length > PERFORMANCE_CONFIG.BUFFERS.MAX_ENTRIES) { + this.metrics = this.metrics.slice(-PERFORMANCE_CONFIG.BUFFERS.MAX_ENTRIES); + } + } + + // Generate performance report + private generatePerformanceReport(): void { + if (this.metrics.length === 0) return; + + const recent = this.metrics.slice(-10); // Last 10 metrics + const report = this.analyzeMetrics(recent); + + console.log('📊 Performance Report:', report); + + // Check for performance issues + this.checkPerformanceIssues(report); + } + + // Analyze metrics and generate insights + private analyzeMetrics(metrics: PerformanceMetrics[]): any { + const validMetrics = metrics.filter(m => m); + + if (validMetrics.length === 0) return {}; + + const report: any = { + timestamp: Date.now(), + sampleSize: validMetrics.length, + }; + + // Analyze each metric + ['fcp', 'lcp', 'fid', 'cls', 'ttfb', 'taskLoadTime', 'projectSwitchTime'].forEach(metric => { + const values = validMetrics + .map(m => (m as any)[metric]) + .filter(v => v !== undefined); + + if (values.length > 0) { + report[metric] = { + avg: values.reduce((a, b) => a + b, 0) / values.length, + min: Math.min(...values), + max: Math.max(...values), + latest: values[values.length - 1], + }; + } + }); + + // Memory analysis + const memoryMetrics = validMetrics + .map(m => m.memoryUsage) + .filter(m => m !== undefined); + + if (memoryMetrics.length > 0) { + const latest = memoryMetrics[memoryMetrics.length - 1]; + report.memory = { + usedMB: (latest.usedJSHeapSize / 1024 / 1024).toFixed(2), + totalMB: (latest.totalJSHeapSize / 1024 / 1024).toFixed(2), + usage: ((latest.usedJSHeapSize / latest.totalJSHeapSize) * 100).toFixed(2) + '%', + }; + } + + return report; + } + + // Check for performance issues + private checkPerformanceIssues(report: any): void { + const issues: string[] = []; + + // Check Core Web Vitals + if (report.fcp?.latest > PERFORMANCE_CONFIG.THRESHOLDS.FCP) { + issues.push(`FCP is slow: ${report.fcp.latest.toFixed(2)}ms (threshold: ${PERFORMANCE_CONFIG.THRESHOLDS.FCP}ms)`); + } + + if (report.lcp?.latest > PERFORMANCE_CONFIG.THRESHOLDS.LCP) { + issues.push(`LCP is slow: ${report.lcp.latest.toFixed(2)}ms (threshold: ${PERFORMANCE_CONFIG.THRESHOLDS.LCP}ms)`); + } + + if (report.fid?.latest > PERFORMANCE_CONFIG.THRESHOLDS.FID) { + issues.push(`FID is high: ${report.fid.latest.toFixed(2)}ms (threshold: ${PERFORMANCE_CONFIG.THRESHOLDS.FID}ms)`); + } + + if (report.cls?.latest > PERFORMANCE_CONFIG.THRESHOLDS.CLS) { + issues.push(`CLS is high: ${report.cls.latest.toFixed(3)} (threshold: ${PERFORMANCE_CONFIG.THRESHOLDS.CLS})`); + } + + // Check application-specific metrics + if (report.taskLoadTime?.latest > 1000) { + issues.push(`Task loading is slow: ${report.taskLoadTime.latest.toFixed(2)}ms`); + } + + if (report.projectSwitchTime?.latest > 500) { + issues.push(`Project switching is slow: ${report.projectSwitchTime.latest.toFixed(2)}ms`); + } + + // Check memory usage + if (report.memory && parseFloat(report.memory.usage) > 80) { + issues.push(`High memory usage: ${report.memory.usage}`); + } + + // Log issues + if (issues.length > 0) { + console.warn('⚠️ Performance Issues Detected:'); + issues.forEach(issue => console.warn(` - ${issue}`)); + } + } + + // Cleanup old metrics + private cleanupOldMetrics(): void { + const fiveMinutesAgo = Date.now() - PERFORMANCE_CONFIG.INTERVALS.CLEANUP_THRESHOLD; + this.metrics = this.metrics.filter(metric => metric.timestamp > fiveMinutesAgo); + } + + // Cleanup observers + private cleanupObservers(): void { + this.observers.forEach(observer => observer.disconnect()); + this.observers = []; + } + + // Clear intervals + private clearIntervals(): void { + this.intervalIds.forEach(id => clearInterval(id)); + this.intervalIds = []; + } + + // Get current metrics + getMetrics(): PerformanceMetrics[] { + return [...this.metrics]; + } + + // Get performance summary + getPerformanceSummary(): any { + return this.analyzeMetrics(this.metrics); + } + + // Export metrics for analysis + exportMetrics(): string { + return JSON.stringify({ + timestamp: Date.now(), + metrics: this.metrics, + summary: this.getPerformanceSummary(), + }, null, 2); + } +} + +// Custom performance measurement utilities +export class CustomPerformanceMeasurer { + private static marks = new Map(); + + // Mark start of operation + static mark(name: string): void { + if ('performance' in window && 'mark' in performance) { + performance.mark(`${name}-start`); + } + this.marks.set(name, Date.now()); + } + + // Measure operation duration + static measure(name: string): number { + const startTime = this.marks.get(name); + const endTime = Date.now(); + + if (startTime) { + const duration = endTime - startTime; + + if ('performance' in window && 'measure' in performance) { + try { + performance.measure(name, `${name}-start`); + } catch (error) { + console.warn(`Failed to create performance measure for ${name}:`, error); + } + } + + this.marks.delete(name); + return duration; + } + + return 0; + } + + // Measure async operation + static async measureAsync(name: string, operation: () => Promise): Promise { + this.mark(name); + try { + const result = await operation(); + this.measure(name); + return result; + } catch (error) { + this.measure(name); + throw error; + } + } + + // Measure function execution + static measureFunction( + name: string, + fn: (...args: T) => R + ): (...args: T) => R { + return (...args: T): R => { + this.mark(name); + try { + const result = fn(...args); + this.measure(name); + return result; + } catch (error) { + this.measure(name); + throw error; + } + }; + } +} + +// Performance optimization recommendations +export class PerformanceOptimizer { + // Analyze and provide optimization recommendations + static analyzeAndRecommend(metrics: PerformanceMetrics[]): string[] { + const recommendations: string[] = []; + const latest = metrics[metrics.length - 1]; + + if (!latest) return recommendations; + + // FCP recommendations + if (latest.fcp && latest.fcp > PERFORMANCE_CONFIG.THRESHOLDS.FCP) { + recommendations.push( + 'Consider optimizing critical rendering path: inline critical CSS, reduce render-blocking resources' + ); + } + + // LCP recommendations + if (latest.lcp && latest.lcp > PERFORMANCE_CONFIG.THRESHOLDS.LCP) { + recommendations.push( + 'Optimize Largest Contentful Paint: compress images, preload critical resources, improve server response times' + ); + } + + // Memory recommendations + if (latest.memoryUsage) { + const usagePercent = (latest.memoryUsage.usedJSHeapSize / latest.memoryUsage.totalJSHeapSize) * 100; + + if (usagePercent > 80) { + recommendations.push( + 'High memory usage detected: implement cleanup routines, check for memory leaks, optimize data structures' + ); + } + } + + // Task loading recommendations + if (latest.taskLoadTime && latest.taskLoadTime > 1000) { + recommendations.push( + 'Task loading is slow: implement pagination, optimize database queries, add loading states' + ); + } + + return recommendations; + } + + // Get optimization priority + static getOptimizationPriority(metrics: PerformanceMetrics[]): Array<{metric: string, priority: 'high' | 'medium' | 'low', value: number}> { + const latest = metrics[metrics.length - 1]; + if (!latest) return []; + + const priorities: Array<{metric: string, priority: 'high' | 'medium' | 'low', value: number}> = []; + + // Check each metric against thresholds + if (latest.fcp) { + const ratio = latest.fcp / PERFORMANCE_CONFIG.THRESHOLDS.FCP; + priorities.push({ + metric: 'First Contentful Paint', + priority: ratio > 2 ? 'high' : ratio > 1.5 ? 'medium' : 'low', + value: latest.fcp, + }); + } + + if (latest.lcp) { + const ratio = latest.lcp / PERFORMANCE_CONFIG.THRESHOLDS.LCP; + priorities.push({ + metric: 'Largest Contentful Paint', + priority: ratio > 2 ? 'high' : ratio > 1.5 ? 'medium' : 'low', + value: latest.lcp, + }); + } + + if (latest.cls) { + const ratio = latest.cls / PERFORMANCE_CONFIG.THRESHOLDS.CLS; + priorities.push({ + metric: 'Cumulative Layout Shift', + priority: ratio > 3 ? 'high' : ratio > 2 ? 'medium' : 'low', + value: latest.cls, + }); + } + + return priorities.sort((a, b) => { + const priorityOrder = { high: 3, medium: 2, low: 1 }; + return priorityOrder[b.priority] - priorityOrder[a.priority]; + }); + } +} + +// Track if performance monitoring has been initialized +let isInitialized = false; + +// Initialize performance monitoring +export const initializePerformanceMonitoring = (): void => { + // Prevent duplicate initialization + if (isInitialized) { + console.warn('Performance monitoring already initialized'); + return; + } + + isInitialized = true; + const monitor = EnhancedPerformanceMonitor.getInstance(); + monitor.startMonitoring(); + + // Cleanup on page unload + const cleanup = () => { + monitor.stopMonitoring(); + isInitialized = false; + }; + + window.addEventListener('beforeunload', cleanup); + + // Also cleanup on page visibility change (tab switching) + window.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'hidden') { + cleanup(); + } + }); +}; + +// Export global performance utilities +export const performanceUtils = { + monitor: EnhancedPerformanceMonitor.getInstance(), + measurer: CustomPerformanceMeasurer, + optimizer: PerformanceOptimizer, + initialize: initializePerformanceMonitoring, +}; \ No newline at end of file diff --git a/worklenz-frontend/src/utils/redux-optimizations.ts b/worklenz-frontend/src/utils/redux-optimizations.ts new file mode 100644 index 00000000..e7a9c505 --- /dev/null +++ b/worklenz-frontend/src/utils/redux-optimizations.ts @@ -0,0 +1,319 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { shallowEqual } from 'react-redux'; +import { RootState } from '@/app/store'; +import { Task } from '@/types/task-management.types'; + +// Performance-optimized selectors using createSelector for memoization + +// Basic state selectors (these will be cached) +const selectTaskManagementState = (state: RootState) => state.taskManagement; +const selectTaskReducerState = (state: RootState) => state.taskReducer; +const selectThemeState = (state: RootState) => state.themeReducer; +const selectTeamMembersState = (state: RootState) => state.teamMembersReducer; +const selectTaskStatusState = (state: RootState) => state.taskStatusReducer; +const selectPriorityState = (state: RootState) => state.priorityReducer; +const selectPhaseState = (state: RootState) => state.phaseReducer; +const selectTaskLabelsState = (state: RootState) => state.taskLabelsReducer; + +// Memoized task selectors +export const selectOptimizedAllTasks = createSelector( + [selectTaskManagementState], + (taskManagementState) => Object.values(taskManagementState.entities || {}) +); + +export const selectOptimizedTasksById = createSelector( + [selectTaskManagementState], + (taskManagementState) => taskManagementState.entities || {} +); + +export const selectOptimizedTaskGroups = createSelector( + [selectTaskManagementState], + (taskManagementState) => taskManagementState.groups || [] +); + +export const selectOptimizedCurrentGrouping = createSelector( + [selectTaskManagementState], + (taskManagementState) => taskManagementState.grouping || 'status' +); + +export const selectOptimizedLoading = createSelector( + [selectTaskManagementState], + (taskManagementState) => taskManagementState.loading || false +); + +export const selectOptimizedError = createSelector( + [selectTaskManagementState], + (taskManagementState) => taskManagementState.error +); + +export const selectOptimizedSearch = createSelector( + [selectTaskManagementState], + (taskManagementState) => taskManagementState.search || '' +); + +export const selectOptimizedArchived = createSelector( + [selectTaskManagementState], + (taskManagementState) => taskManagementState.archived || false +); + +// Theme selectors +export const selectOptimizedIsDarkMode = createSelector( + [selectThemeState], + (themeState) => themeState?.mode === 'dark' +); + +export const selectOptimizedThemeMode = createSelector( + [selectThemeState], + (themeState) => themeState?.mode || 'light' +); + +// Team members selectors +export const selectOptimizedTeamMembers = createSelector( + [selectTeamMembersState], + (teamMembersState) => teamMembersState.teamMembers || [] +); + +export const selectOptimizedTeamMembersById = createSelector( + [selectOptimizedTeamMembers], + (teamMembers) => { + if (!Array.isArray(teamMembers)) return {}; + const membersById: Record = {}; + teamMembers.forEach((member: any) => { + membersById[member.id] = member; + }); + return membersById; + } +); + +// Task status selectors +export const selectOptimizedTaskStatuses = createSelector( + [selectTaskStatusState], + (taskStatusState) => taskStatusState.status || [] +); + +export const selectOptimizedTaskStatusCategories = createSelector( + [selectTaskStatusState], + (taskStatusState) => taskStatusState.statusCategories || [] +); + +// Priority selectors +export const selectOptimizedPriorities = createSelector( + [selectPriorityState], + (priorityState) => priorityState.priorities || [] +); + +// Phase selectors +export const selectOptimizedPhases = createSelector( + [selectPhaseState], + (phaseState) => phaseState.phaseList || [] +); + +// Labels selectors +export const selectOptimizedLabels = createSelector( + [selectTaskLabelsState], + (labelsState) => labelsState.labels || [] +); + +// Complex computed selectors +export const selectOptimizedTasksByGroup = createSelector( + [selectOptimizedAllTasks, selectOptimizedTaskGroups], + (tasks, groups) => { + const tasksByGroup: Record = {}; + + groups.forEach((group: any) => { + tasksByGroup[group.id] = group.tasks || []; + }); + + return tasksByGroup; + } +); + +export const selectOptimizedTaskCounts = createSelector( + [selectOptimizedTasksByGroup], + (tasksByGroup) => { + const counts: Record = {}; + Object.keys(tasksByGroup).forEach(groupId => { + counts[groupId] = tasksByGroup[groupId].length; + }); + return counts; + } +); + +export const selectOptimizedTotalTaskCount = createSelector( + [selectOptimizedAllTasks], + (tasks) => tasks.length +); + +// Selection state selectors +export const selectOptimizedSelectedTaskIds = createSelector( + [(state: RootState) => state.taskManagementSelection?.selectedTaskIds], + (selectedTaskIds) => selectedTaskIds || [] +); + +export const selectOptimizedSelectedTasksCount = createSelector( + [selectOptimizedSelectedTaskIds], + (selectedTaskIds) => selectedTaskIds.length +); + +export const selectOptimizedSelectedTasks = createSelector( + [selectOptimizedAllTasks, selectOptimizedSelectedTaskIds], + (tasks, selectedTaskIds) => { + return tasks.filter((task: Task) => selectedTaskIds.includes(task.id)); + } +); + +// Performance utilities +export const createShallowEqualSelector = ( + selector: (state: RootState) => T +) => { + let lastResult: T; + let lastArgs: any; + + return (state: RootState): T => { + const newArgs = selector(state); + + if (!shallowEqual(newArgs, lastArgs)) { + lastArgs = newArgs; + lastResult = newArgs; + } + + return lastResult; + }; +}; + +// Memoized equality functions for React.memo +export const taskPropsAreEqual = ( + prevProps: any, + nextProps: any +): boolean => { + // Quick reference checks first + if (prevProps.task === nextProps.task) return true; + if (!prevProps.task || !nextProps.task) return false; + if (prevProps.task.id !== nextProps.task.id) return false; + + // Check other props + if (prevProps.isSelected !== nextProps.isSelected) return false; + if (prevProps.isDragOverlay !== nextProps.isDragOverlay) return false; + if (prevProps.groupId !== nextProps.groupId) return false; + if (prevProps.currentGrouping !== nextProps.currentGrouping) return false; + if (prevProps.level !== nextProps.level) return false; + + // Deep comparison for task properties that commonly change + const taskProps = [ + 'title', + 'progress', + 'status', + 'priority', + 'description', + 'startDate', + 'dueDate', + 'updatedAt', + 'sub_tasks_count', + 'show_sub_tasks' + ]; + + for (const prop of taskProps) { + if (prevProps.task[prop] !== nextProps.task[prop]) { + return false; + } + } + + // Compare arrays with shallow equality + if (!shallowEqual(prevProps.task.assignees, nextProps.task.assignees)) { + return false; + } + + if (!shallowEqual(prevProps.task.labels, nextProps.task.labels)) { + return false; + } + + return true; +}; + +export const taskGroupPropsAreEqual = ( + prevProps: any, + nextProps: any +): boolean => { + // Quick reference checks + if (prevProps.group === nextProps.group) return true; + if (!prevProps.group || !nextProps.group) return false; + if (prevProps.group.id !== nextProps.group.id) return false; + + // Check task lists + if (!shallowEqual(prevProps.group.taskIds, nextProps.group.taskIds)) { + return false; + } + + // Check other props + if (prevProps.projectId !== nextProps.projectId) return false; + if (prevProps.currentGrouping !== nextProps.currentGrouping) return false; + if (!shallowEqual(prevProps.selectedTaskIds, nextProps.selectedTaskIds)) { + return false; + } + + return true; +}; + +// Performance monitoring utilities +export const createPerformanceSelector = ( + selector: (state: RootState) => T, + name: string +) => { + return createSelector( + [selector], + (result) => { + if (process.env.NODE_ENV === 'development') { + const startTime = performance.now(); + const endTime = performance.now(); + const duration = endTime - startTime; + + if (duration > 5) { + console.warn(`Slow selector ${name}: ${duration.toFixed(2)}ms`); + } + } + return result; + } + ); +}; + +// Utility to create batched state updates +export const createBatchedStateUpdate = ( + updateFn: (updates: T[]) => void, + delay: number = 16 // One frame +) => { + let pending: T[] = []; + let timeoutId: NodeJS.Timeout | null = null; + + return (update: T) => { + pending.push(update); + + if (timeoutId) { + clearTimeout(timeoutId); + } + + timeoutId = setTimeout(() => { + const updates = [...pending]; + pending = []; + timeoutId = null; + updateFn(updates); + }, delay); + }; +}; + +// Performance monitoring hook +export const useReduxPerformanceMonitor = () => { + if (process.env.NODE_ENV === 'development') { + const startTime = performance.now(); + + return () => { + const endTime = performance.now(); + const duration = endTime - startTime; + + if (duration > 16) { + console.warn(`Slow Redux operation: ${duration.toFixed(2)}ms`); + } + }; + } + + return () => {}; // No-op in production +}; \ No newline at end of file