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.
This commit is contained in:
chamiakJ
2025-07-10 20:39:15 +05:30
parent cf686ef8c5
commit 94977f7255
15 changed files with 3572 additions and 133 deletions

View File

@@ -34,8 +34,9 @@
"gantt-task-react": "^0.3.9", "gantt-task-react": "^0.3.9",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"i18next": "^23.16.8", "i18next": "^23.16.8",
"i18next-browser-languagedetector": "^8.0.3", "i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^2.7.3", "i18next-http-backend": "^2.7.3",
"i18next-localstorage-backend": "^4.2.0",
"jspdf": "^3.0.0", "jspdf": "^3.0.0",
"mixpanel-browser": "^2.56.0", "mixpanel-browser": "^2.56.0",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
@@ -728,6 +729,8 @@
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"@csstools/css-calc": "^2.1.3", "@csstools/css-calc": "^2.1.3",
"@csstools/css-color-parser": "^3.0.9", "@csstools/css-color-parser": "^3.0.9",
@@ -741,7 +744,9 @@
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC",
"optional": true,
"peer": true
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.27.1", "version": "7.27.1",
@@ -1051,6 +1056,8 @@
} }
], ],
"license": "MIT-0", "license": "MIT-0",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -1071,6 +1078,8 @@
} }
], ],
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@@ -1095,6 +1104,8 @@
} }
], ],
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"@csstools/color-helpers": "^5.0.2", "@csstools/color-helpers": "^5.0.2",
"@csstools/css-calc": "^2.1.4" "@csstools/css-calc": "^2.1.4"
@@ -1123,6 +1134,8 @@
} }
], ],
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@@ -1146,6 +1159,8 @@
} }
], ],
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -2997,6 +3012,8 @@
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">= 14" "node": ">= 14"
} }
@@ -3738,6 +3755,8 @@
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"@asamuzakjp/css-color": "^3.2.0", "@asamuzakjp/css-color": "^3.2.0",
"rrweb-cssom": "^0.8.0" "rrweb-cssom": "^0.8.0"
@@ -3758,6 +3777,8 @@
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"whatwg-mimetype": "^4.0.0", "whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.0.0" "whatwg-url": "^14.0.0"
@@ -3772,6 +3793,8 @@
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"punycode": "^2.3.1" "punycode": "^2.3.1"
}, },
@@ -3785,6 +3808,8 @@
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
@@ -3795,6 +3820,8 @@
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"tr46": "^5.1.0", "tr46": "^5.1.0",
"webidl-conversions": "^7.0.0" "webidl-conversions": "^7.0.0"
@@ -3841,7 +3868,9 @@
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"optional": true,
"peer": true
}, },
"node_modules/deep-eql": { "node_modules/deep-eql": {
"version": "5.0.2", "version": "5.0.2",
@@ -4016,6 +4045,8 @@
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"
}, },
@@ -4499,6 +4530,8 @@
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"whatwg-encoding": "^3.1.1" "whatwg-encoding": "^3.1.1"
}, },
@@ -4534,6 +4567,8 @@
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"agent-base": "^7.1.0", "agent-base": "^7.1.0",
"debug": "^4.3.4" "debug": "^4.3.4"
@@ -4548,6 +4583,8 @@
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"agent-base": "^7.1.2", "agent-base": "^7.1.2",
"debug": "4" "debug": "4"
@@ -4586,9 +4623,9 @@
} }
}, },
"node_modules/i18next-browser-languagedetector": { "node_modules/i18next-browser-languagedetector": {
"version": "8.1.0", "version": "8.2.0",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.1.0.tgz", "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz",
"integrity": "sha512-mHZxNx1Lq09xt5kCauZ/4bsXOEA2pfpwSoU11/QTJB+pD94iONFwp+ohqi///PwiFvjFOxe1akYCdHyFo1ng5Q==", "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.23.2" "@babel/runtime": "^7.23.2"
@@ -4603,12 +4640,23 @@
"cross-fetch": "4.0.0" "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": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0" "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", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"optional": true,
"peer": true
}, },
"node_modules/isexe": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
@@ -4818,6 +4868,8 @@
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"cssstyle": "^4.2.1", "cssstyle": "^4.2.1",
"data-urls": "^5.0.0", "data-urls": "^5.0.0",
@@ -4858,6 +4910,8 @@
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"punycode": "^2.3.1" "punycode": "^2.3.1"
}, },
@@ -4871,6 +4925,8 @@
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
@@ -4881,6 +4937,8 @@
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"tr46": "^5.1.0", "tr46": "^5.1.0",
"webidl-conversions": "^7.0.0" "webidl-conversions": "^7.0.0"
@@ -4895,6 +4953,8 @@
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },
@@ -5533,7 +5593,9 @@
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
"integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"optional": true,
"peer": true
}, },
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
@@ -5595,6 +5657,8 @@
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"entities": "^6.0.0" "entities": "^6.0.0"
}, },
@@ -6060,6 +6124,8 @@
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
@@ -7205,7 +7271,9 @@
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"optional": true,
"peer": true
}, },
"node_modules/rrweb-snapshot": { "node_modules/rrweb-snapshot": {
"version": "2.0.0-alpha.18", "version": "2.0.0-alpha.18",
@@ -7253,7 +7321,9 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"optional": true,
"peer": true
}, },
"node_modules/saxes": { "node_modules/saxes": {
"version": "6.0.0", "version": "6.0.0",
@@ -7261,6 +7331,8 @@
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"xmlchars": "^2.2.0" "xmlchars": "^2.2.0"
}, },
@@ -7663,7 +7735,9 @@
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"optional": true,
"peer": true
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.4.17", "version": "3.4.17",
@@ -7883,6 +7957,8 @@
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"tldts-core": "^6.1.86" "tldts-core": "^6.1.86"
}, },
@@ -7895,7 +7971,9 @@
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"optional": true,
"peer": true
}, },
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
@@ -7921,6 +7999,8 @@
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
"dev": true, "dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"tldts": "^6.1.32" "tldts": "^6.1.32"
}, },
@@ -8284,6 +8364,8 @@
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"xml-name-validator": "^5.0.0" "xml-name-validator": "^5.0.0"
}, },
@@ -8318,6 +8400,8 @@
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"iconv-lite": "0.6.3" "iconv-lite": "0.6.3"
}, },
@@ -8331,6 +8415,8 @@
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -8487,6 +8573,8 @@
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -8496,7 +8584,9 @@
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"optional": true,
"peer": true
}, },
"node_modules/xmlhttprequest-ssl": { "node_modules/xmlhttprequest-ssl": {
"version": "2.1.2", "version": "2.1.2",

View File

@@ -38,8 +38,9 @@
"gantt-task-react": "^0.3.9", "gantt-task-react": "^0.3.9",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"i18next": "^23.16.8", "i18next": "^23.16.8",
"i18next-browser-languagedetector": "^8.0.3", "i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^2.7.3", "i18next-http-backend": "^2.7.3",
"i18next-localstorage-backend": "^4.2.0",
"jspdf": "^3.0.0", "jspdf": "^3.0.0",
"mixpanel-browser": "^2.56.0", "mixpanel-browser": "^2.56.0",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",

View File

@@ -19,6 +19,9 @@ import { Language } from './features/i18n/localesSlice';
import logger from './utils/errorLogger'; import logger from './utils/errorLogger';
import { SuspenseFallback } from './components/suspense-fallback/suspense-fallback'; import { SuspenseFallback } from './components/suspense-fallback/suspense-fallback';
// Performance optimizations
import { CSSPerformanceMonitor, LayoutStabilizer, CriticalCSSManager } from './utils/css-optimizations';
// Service Worker // Service Worker
import { registerSW } from './utils/serviceWorkerRegistration'; import { registerSW } from './utils/serviceWorkerRegistration';
@@ -84,6 +87,17 @@ const App: React.FC = memo(() => {
try { try {
// Initialize CSRF token immediately as it's needed for API calls // Initialize CSRF token immediately as it's needed for API calls
await initializeCsrfToken(); 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) { } catch (error) {
if (isMounted) { if (isMounted) {
logger.error('Failed to initialize critical app functionality:', error); logger.error('Failed to initialize critical app functionality:', error);

View File

@@ -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<T extends ComponentType<any>>(
importFunc: () => Promise<{ default: T }>,
fallback?: ReactNode
): React.LazyExoticComponent<T> {
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 = <T extends ComponentType<any>>(
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'),
<div className="h-96 flex items-center justify-center">
<Spin size="large" tip="Loading task board..." />
</div>
);
export const LazyVirtualizedTaskList = createOptimizedLazy(
() => import('./virtualized-task-list'),
<Skeleton active paragraph={{ rows: 8 }} />
);
export const LazyTaskRow = createOptimizedLazy(
() => import('./task-row'),
<Skeleton.Input active style={{ width: '100%', height: 40 }} />
);
export const LazyImprovedTaskFilters = createOptimizedLazy(
() => import('./improved-task-filters'),
<Skeleton.Button active style={{ width: '100%', height: 60 }} />
);
export const LazyOptimizedBulkActionBar = createOptimizedLazy(
() => import('./optimized-bulk-action-bar'),
<Skeleton.Button active style={{ width: '100%', height: 48 }} />
);
export const LazyPerformanceAnalysis = createOptimizedLazy(
() => import('./performance-analysis'),
<div className="p-4 text-center">Loading performance tools...</div>
);
// Kanban-specific components
export const LazyKanbanTaskListBoard = createOptimizedLazy(
() => import('../kanban-board-management-v2/kanbanTaskListBoard'),
<div className="h-96 flex items-center justify-center">
<Spin size="large" tip="Loading kanban board..." />
</div>
);
// Task list V2 components
export const LazyTaskListV2Table = createOptimizedLazy(
() => import('../task-list-v2/TaskListV2Table'),
<Skeleton active paragraph={{ rows: 10 }} />
);
export const LazyTaskRowWithSubtasks = createOptimizedLazy(
() => import('../task-list-v2/TaskRowWithSubtasks'),
<Skeleton.Input active style={{ width: '100%', height: 40 }} />
);
export const LazyCustomColumnModal = createOptimizedLazy(
() => import('@/pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/custom-column-modal'),
<div className="p-4"><Skeleton active /></div>
);
export const LazyLabelsSelector = createOptimizedLazy(
() => import('@/components/LabelsSelector'),
<Skeleton.Button active style={{ width: 120, height: 24 }} />
);
export const LazyAssigneeSelector = createOptimizedLazy(
() => import('./lazy-assignee-selector'),
<Skeleton.Avatar active size="small" />
);
export const LazyTaskStatusDropdown = createOptimizedLazy(
() => import('./task-status-dropdown'),
<Skeleton.Button active style={{ width: 100, height: 24 }} />
);
export const LazyTaskPriorityDropdown = createOptimizedLazy(
() => import('./task-priority-dropdown'),
<Skeleton.Button active style={{ width: 80, height: 24 }} />
);
export const LazyTaskPhaseDropdown = createOptimizedLazy(
() => import('./task-phase-dropdown'),
<Skeleton.Button active style={{ width: 90, height: 24 }} />
);
// HOC for progressive enhancement
interface ProgressiveEnhancementProps {
condition: boolean;
children: ReactNode;
fallback?: ReactNode;
loadingComponent?: ReactNode;
}
export const ProgressiveEnhancement: React.FC<ProgressiveEnhancementProps> = ({
condition,
children,
fallback,
loadingComponent = <Skeleton active />
}) => {
if (!condition) {
return <>{fallback || loadingComponent}</>;
}
return (
<Suspense fallback={loadingComponent}>
{children}
</Suspense>
);
};
// Intersection observer based lazy loading for components
interface IntersectionLazyLoadProps {
children: ReactNode;
fallback?: ReactNode;
rootMargin?: string;
threshold?: number;
once?: boolean;
}
export const IntersectionLazyLoad: React.FC<IntersectionLazyLoadProps> = ({
children,
fallback = <Skeleton active />,
rootMargin = '100px',
threshold = 0.1,
once = true
}) => {
const [isVisible, setIsVisible] = React.useState(false);
const [hasBeenVisible, setHasBeenVisible] = React.useState(false);
const ref = React.useRef<HTMLDivElement>(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 (
<div ref={ref}>
{shouldRender ? (
<Suspense fallback={fallback}>
{children}
</Suspense>
) : (
fallback
)}
</div>
);
};
// Route-based code splitting utility
export const createRouteComponent = <T extends ComponentType<any>>(
importFunc: () => Promise<{ default: T }>,
pageTitle?: string
) => {
const LazyComponent = createOptimizedLazy(importFunc);
return React.memo(() => {
React.useEffect(() => {
if (pageTitle) {
document.title = pageTitle;
}
}, []);
return (
<Suspense
fallback={
<div className="min-h-screen flex items-center justify-center">
<Spin size="large" tip={`Loading ${pageTitle || 'page'}...`} />
</div>
}
>
<LazyComponent {...({} as any)} />
</Suspense>
);
});
};
// 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<string, () => Promise<any>> = {
'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 || (
<div className="p-4 text-center text-red-600">
<p>Failed to load component</p>
<button
onClick={() => window.location.reload()}
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Reload Page
</button>
</div>
)
);
}
return this.props.children;
}
}
// Usage example and documentation
export const LazyLoadingExamples = {
// Basic lazy loading with suspense
BasicExample: () => (
<Suspense fallback={<Skeleton active />}>
<LazyTaskListBoard projectId="123" />
</Suspense>
),
// Progressive enhancement
ProgressiveExample: () => (
<ProgressiveEnhancement condition={true}>
<LazyTaskRow
task={{ id: '1', status: 'todo', priority: 'medium', created_at: '', updated_at: '' }}
projectId="123"
groupId="group1"
currentGrouping="status"
isSelected={false}
onSelect={() => {}}
onToggleSubtasks={() => {}}
/>
</ProgressiveEnhancement>
),
// Intersection observer lazy loading
IntersectionExample: () => (
<IntersectionLazyLoad rootMargin="200px">
<LazyPerformanceAnalysis projectId="123" />
</IntersectionLazyLoad>
),
// Error boundary with lazy loading
ErrorBoundaryExample: () => (
<LazyErrorBoundary>
<Suspense fallback={<Skeleton active />}>
<LazyTaskRow
task={{ id: '1', status: 'todo', priority: 'medium', created_at: '', updated_at: '' }}
projectId="123"
groupId="group1"
currentGrouping="status"
isSelected={false}
onSelect={() => {}}
onToggleSubtasks={() => {}}
/>
</Suspense>
</LazyErrorBoundary>
),
};

View File

@@ -77,6 +77,12 @@ import { fetchLabels } from '@/features/taskAttributes/taskLabelSlice';
import ImprovedTaskFilters from './improved-task-filters'; import ImprovedTaskFilters from './improved-task-filters';
import PerformanceAnalysis from './performance-analysis'; 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 and drop performance optimizations
import './drag-drop-optimized.css'; import './drag-drop-optimized.css';
import './optimized-bulk-action-bar.css'; import './optimized-bulk-action-bar.css';
@@ -210,13 +216,39 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
return () => clearInterval(interval); 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 // Fetch task groups when component mounts or dependencies change
useEffect(() => { useEffect(() => {
if (projectId && !hasInitialized.current) { if (projectId && !hasInitialized.current) {
hasInitialized.current = true; hasInitialized.current = true;
// Measure task loading performance
CustomPerformanceMeasurer.mark('task-load-time');
// Fetch real tasks from V3 API (minimal processing needed) // Fetch real tasks from V3 API (minimal processing needed)
dispatch(fetchTasksV3(projectId)); dispatch(fetchTasksV3(projectId)).finally(() => {
CustomPerformanceMeasurer.measure('task-load-time');
});
} }
}, [projectId, dispatch]); }, [projectId, dispatch]);

View File

@@ -1,83 +1,283 @@
import { useEffect, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ensureTranslationsLoaded } from '@/i18n'; import {
ensureTranslationsLoaded,
preloadPageTranslations,
getPerformanceMetrics,
changeLanguageOptimized
} from '../i18n';
import logger from '../utils/errorLogger';
interface UseTranslationPreloaderOptions { // Cache for preloaded translation states
namespaces?: string[]; const preloadCache = new Map<string, boolean>();
fallback?: React.ReactNode; const loadingStates = new Map<string, boolean>();
interface TranslationHookOptions {
preload?: boolean;
priority?: number;
fallbackReady?: boolean;
} }
/** interface TranslationHookReturn {
* Hook to ensure translations are loaded before rendering components t: (key: string, defaultValue?: string) => string;
* This prevents Suspense issues when components use useTranslation ready: boolean;
*/ isLoading: boolean;
export const useTranslationPreloader = ( error: Error | null;
namespaces: string[] = ['tasks/task-table-bulk-actions', 'task-management'], retryLoad: () => Promise<void>;
options: UseTranslationPreloaderOptions = {} performanceMetrics: any;
) => { }
const [isLoaded, setIsLoaded] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const { t, ready } = useTranslation(namespaces);
// 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<Error | null>(null);
const hasInitialized = useRef(false);
const loadingPromise = useRef<Promise<void> | 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(() => { useEffect(() => {
let isMounted = true; if (!hasInitialized.current && preload) {
hasInitialized.current = true;
if (!loadingPromise.current) {
loadingPromise.current = preloadTranslations();
}
}
}, [preload, preloadTranslations]);
const loadTranslations = async () => { // Handle language changes
try { useEffect(() => {
setIsLoading(true); const handleLanguageChange = () => {
const cacheKey = `${i18n.language}:${namespaceKey}`;
// Only load translations for current language to avoid multiple requests if (!preloadCache.get(cacheKey) && preload) {
await ensureTranslationsLoaded(namespaces); setReady(false);
preloadTranslations();
// 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);
}
} }
}; };
// Only load if not already loaded i18n.on('languageChanged', handleLanguageChange);
if (!isLoaded && !ready) {
loadTranslations();
} else if (ready && !isLoaded) {
setIsLoaded(true);
setIsLoading(false);
}
return () => { 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 { return {
t, t: enhancedT,
ready: isLoaded && ready, ready,
isLoading, isLoading,
isLoaded, error,
retryLoad,
performanceMetrics,
}; };
}; };
/** // Specialized hooks for commonly used namespaces
* Hook specifically for bulk action bar translations export const useTaskManagementTranslations = (options?: TranslationHookOptions) => {
*/ return useOptimizedTranslation(['task-management', 'task-list-table'], {
export const useBulkActionTranslations = () => { priority: 8,
return useTranslationPreloader(['tasks/task-table-bulk-actions']); ...options,
});
}; };
/** export const useBulkActionTranslations = (options?: TranslationHookOptions) => {
* Hook for task management translations return useOptimizedTranslation(['tasks/task-table-bulk-actions', 'task-management'], {
*/ priority: 6,
export const useTaskManagementTranslations = () => { ...options,
return useTranslationPreloader(['task-management', 'tasks/task-table-bulk-actions']); });
};
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<boolean> => {
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<string, any> = {};
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;
}; };

View File

@@ -1,6 +1,8 @@
import i18n from 'i18next'; import i18n from 'i18next';
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next';
import HttpApi from 'i18next-http-backend'; import HttpApi from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import LocalStorageBackend from 'i18next-localstorage-backend';
import logger from './utils/errorLogger'; import logger from './utils/errorLogger';
// Essential namespaces that should be preloaded to prevent Suspense // Essential namespaces that should be preloaded to prevent Suspense
@@ -19,53 +21,133 @@ const SECONDARY_NAMESPACES = [
'project-drawer', '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 // Cache to track loaded translations and prevent duplicate requests
const loadedTranslations = new Set<string>(); const loadedTranslations = new Set<string>();
const loadingPromises = new Map<string, Promise<any>>(); const loadingPromises = new Map<string, Promise<any>>();
// Background loading queue for non-essential translations // 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; 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 i18n
.use(HttpApi) .use(LocalStorageBackend) // Cache translations to localStorage
.use(LanguageDetector) // Detect user language
.use(HttpApi) // Fetch translations if not in cache
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
fallbackLng: 'en', fallbackLng: 'en',
backend: { backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json', loadPath: '/locales/{{lng}}/{{ns}}.json',
// Add request timeout to prevent hanging on slow connections addPath: '/locales/add/{{lng}}/{{ns}}',
requestOptions: { // Enhanced LocalStorage caching options
cache: 'default', backendOptions: [{
mode: 'cors', expirationTime: CACHE_CONFIG.EXPIRATION_TIME,
credentials: 'same-origin', // 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', defaultNS: 'common',
// Only load essential namespaces initially
ns: ESSENTIAL_NAMESPACES, ns: ESSENTIAL_NAMESPACES,
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,
}, },
// Only preload current language to reduce initial load
preload: [], preload: [],
load: 'languageOnly', load: 'languageOnly',
// Disable loading all namespaces on init
initImmediate: false, initImmediate: false,
// Cache translations with shorter expiration for better performance detection: {
cache: { order: ['localStorage', 'navigator'], // Check localStorage first, then browser language
enabled: true, caches: ['localStorage'],
expirationTime: 12 * 60 * 60 * 1000, // 12 hours // Cache the detected language for faster subsequent loads
cookieMinutes: 60 * 24 * 7, // 1 week
}, },
// Reduce debug output in production // Reduce debug output in production
debug: process.env.NODE_ENV === 'development', 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 ( export const ensureTranslationsLoaded = async (
namespaces: string[] = ESSENTIAL_NAMESPACES, namespaces: string[] = ESSENTIAL_NAMESPACES,
languages: string[] = [i18n.language || 'en'] languages: string[] = [i18n.language || 'en'],
priority: number = 0
) => { ) => {
const startTime = performance.now();
try { try {
const loadPromises: Promise<any>[] = []; const loadPromises: Promise<any>[] = [];
@@ -84,7 +166,7 @@ export const ensureTranslationsLoaded = async (
continue; continue;
} }
// Create loading promise // Create loading promise with enhanced error handling
const loadingPromise = new Promise<void>((resolve, reject) => { const loadingPromise = new Promise<void>((resolve, reject) => {
const currentLang = i18n.language; const currentLang = i18n.language;
const shouldSwitchLang = currentLang !== lang; const shouldSwitchLang = currentLang !== lang;
@@ -102,10 +184,12 @@ export const ensureTranslationsLoaded = async (
} }
loadedTranslations.add(key); loadedTranslations.add(key);
performanceMetrics.translationsLoaded++;
resolve(); resolve();
} catch (error) { } catch (error) {
logger.error(`Failed to load namespace: ${ns} for language: ${lang}`, 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 { } finally {
loadingPromises.delete(key); loadingPromises.delete(key);
} }
@@ -120,6 +204,10 @@ export const ensureTranslationsLoaded = async (
} }
await Promise.all(loadPromises); await Promise.all(loadPromises);
const endTime = performance.now();
performanceMetrics.totalLoadTime += (endTime - startTime);
return true; return true;
} catch (error) { } catch (error) {
logger.error('Failed to load translations:', 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 () => { const processBackgroundQueue = async () => {
if (isBackgroundLoading || backgroundLoadingQueue.length === 0) return; if (isBackgroundLoading || backgroundLoadingQueue.length === 0) return;
isBackgroundLoading = true; isBackgroundLoading = true;
try { try {
// Process queue in batches to avoid overwhelming the network // Sort by priority (higher priority first)
const batchSize = 3; 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) { while (backgroundLoadingQueue.length > 0) {
const batch = backgroundLoadingQueue.splice(0, batchSize); const batch = backgroundLoadingQueue.splice(0, batchSize);
const batchPromises = batch.map(({ lang, ns }) => const batchPromises = batch.map(({ lang, ns }) =>
ensureTranslationsLoaded([ns], [lang]).catch(error => { ensureTranslationsLoaded([ns], [lang], 0).catch(error => {
logger.error(`Background loading failed for ${lang}:${ns}`, error); logger.error(`Background loading failed for ${lang}:${ns}`, error);
}) })
); );
await Promise.all(batchPromises); 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) { 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 { } finally {
@@ -156,17 +253,29 @@ const processBackgroundQueue = async () => {
} }
}; };
// Queue secondary translations for background loading // Enhanced queueing with priority support
const queueSecondaryTranslations = (language: string) => { const queueTranslations = (language: string, namespaces: string[], priority: number = 0) => {
SECONDARY_NAMESPACES.forEach(ns => { namespaces.forEach(ns => {
const key = `${language}:${ns}`; const key = `${language}:${ns}`;
if (!loadedTranslations.has(key)) { 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 // Start background loading with appropriate delay based on priority
setTimeout(processBackgroundQueue, 2000); const delay = priority > 5 ? 1000 : priority > 2 ? 2000 : 3000;
setTimeout(processBackgroundQueue, delay);
}; };
// Initialize only essential translations for current language // Initialize only essential translations for current language
@@ -174,11 +283,14 @@ const initializeTranslations = async () => {
try { try {
const currentLang = i18n.language || 'en'; const currentLang = i18n.language || 'en';
// Load only essential namespaces initially // Load only essential namespaces immediately
await ensureTranslationsLoaded(ESSENTIAL_NAMESPACES, [currentLang]); await ensureTranslationsLoaded(ESSENTIAL_NAMESPACES, [currentLang], 10);
// Queue secondary translations for background loading // Queue secondary translations with medium priority
queueSecondaryTranslations(currentLang); queueTranslations(currentLang, SECONDARY_NAMESPACES, 5);
// Queue tertiary translations with low priority
queueTranslations(currentLang, TERTIARY_NAMESPACES, 1);
return true; return true;
} catch (error) { } 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) => { export const changeLanguageOptimized = async (language: string) => {
try { try {
// Change language first // Change language first
await i18n.changeLanguage(language); await i18n.changeLanguage(language);
// Load essential namespaces immediately // Load essential namespaces immediately with high priority
await ensureTranslationsLoaded(ESSENTIAL_NAMESPACES, [language]); await ensureTranslationsLoaded(ESSENTIAL_NAMESPACES, [language], 10);
// Queue secondary translations for background loading // Queue secondary translations with medium priority
queueSecondaryTranslations(language); queueTranslations(language, SECONDARY_NAMESPACES, 5);
// Queue tertiary translations with low priority
queueTranslations(language, TERTIARY_NAMESPACES, 1);
return true; return true;
} catch (error) { } 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(); initializeTranslations();
export default i18n; export default i18n;

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import './index.css'; import './index.css';
import './styles/performance-optimizations.css';
import App from './App'; import App from './App';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
import './i18n'; import './i18n';
@@ -10,12 +11,16 @@ import { applyCssVariables } from './styles/colors';
import { ConfigProvider, theme } from 'antd'; import { ConfigProvider, theme } from 'antd';
import { colors } from './styles/colors'; import { colors } from './styles/colors';
import { getInitialTheme } from './utils/get-initial-theme'; import { getInitialTheme } from './utils/get-initial-theme';
import { initializePerformanceMonitoring } from './utils/enhanced-performance-monitoring';
const initialTheme = getInitialTheme(); const initialTheme = getInitialTheme();
// Apply CSS variables and initial theme // Apply CSS variables and initial theme
applyCssVariables(); applyCssVariables();
// Initialize enhanced performance monitoring
initializePerformanceMonitoring();
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
document.documentElement.classList.add(initialTheme); document.documentElement.classList.add(initialTheme);

View File

@@ -1,6 +1,6 @@
import { Col, ConfigProvider, Layout } from 'antd'; import { Col, ConfigProvider, Layout } from 'antd';
import { Outlet, useNavigate } from 'react-router-dom'; 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 { useMediaQuery } from 'react-responsive';
import Navbar from '../features/navbar/navbar'; import Navbar from '../features/navbar/navbar';
@@ -10,15 +10,30 @@ import { colors } from '../styles/colors';
import { useRenderPerformance } from '@/utils/performance'; import { useRenderPerformance } from '@/utils/performance';
import HubSpot from '@/components/HubSpot'; import HubSpot from '@/components/HubSpot';
import { DynamicCSSLoader, LayoutStabilizer } from '@/utils/css-optimizations';
const MainLayout = memo(() => { const MainLayout = memo(() => {
const themeMode = useAppSelector(state => state.themeReducer.mode); const themeMode = useAppSelector(state => state.themeReducer.mode);
const isDesktop = useMediaQuery({ query: '(min-width: 1024px)' }); const isDesktop = useMediaQuery({ query: '(min-width: 1024px)' });
const layoutRef = useRef<HTMLDivElement>(null);
// Performance monitoring in development // Performance monitoring in development
useRenderPerformance('MainLayout'); 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 // Memoize styles to prevent object recreation on every render
@@ -64,13 +79,13 @@ const MainLayout = memo(() => {
return ( return (
<ConfigProvider theme={themeConfig}> <ConfigProvider theme={themeConfig}>
<Layout style={{ minHeight: '100vh' }}> <Layout ref={layoutRef} style={{ minHeight: '100vh' }} className="prevent-layout-shift">
<Layout.Header className={headerClassName} style={headerStyles}> <Layout.Header className={`${headerClassName} gpu-accelerated`} style={headerStyles}>
<Navbar /> <Navbar />
</Layout.Header> </Layout.Header>
<Layout.Content> <Layout.Content className="layout-contained">
<Col xxl={{ span: 18, offset: 3, flex: '100%' }} style={contentStyles}> <Col xxl={{ span: 18, offset: 3, flex: '100%' }} style={contentStyles} className="task-content-container">
<Outlet /> <Outlet />
</Col> </Col>
</Layout.Content> </Layout.Content>

View File

@@ -8,6 +8,9 @@ import { Modal, message } from 'antd';
import { SocketEvents } from '@/shared/socket-events'; import { SocketEvents } from '@/shared/socket-events';
import { getUserSession } from '@/utils/session-helper'; import { getUserSession } from '@/utils/session-helper';
// Global socket instance to prevent multiple connections in StrictMode
let globalSocketInstance: Socket | null = null;
interface SocketContextType { interface SocketContextType {
socket: Socket | null; socket: Socket | null;
connected: boolean; 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 profile = getUserSession(); // Adjust based on your Redux structure
const [messageApi, messageContextHolder] = message.useMessage(); // Add message API const [messageApi, messageContextHolder] = message.useMessage(); // Add message API
const hasShownConnectedMessage = useRef(false); // Add ref to track if message was shown 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 // Initialize socket connection
useEffect(() => { useEffect(() => {
// Only create a new socket if one doesn't exist // Prevent duplicate initialization
if (!socketRef.current) { if (isInitialized.current) {
socketRef.current = io(SOCKET_CONFIG.url, { 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, ...SOCKET_CONFIG.options,
reconnection: true, reconnection: true,
reconnectionAttempts: Infinity, reconnectionAttempts: Infinity,
@@ -37,10 +58,18 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ childr
reconnectionDelayMax: 5000, reconnectionDelayMax: 5000,
timeout: 20000, 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; const socket = socketRef.current;
// Only proceed if socket exists
if (!socket) return;
// Set up event listeners before connecting // Set up event listeners before connecting
socket.on('connect', () => { socket.on('connect', () => {
logger.info('Socket connected'); logger.info('Socket connected');
@@ -48,7 +77,7 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ childr
// Only show connected message once // Only show connected message once
if (!hasShownConnectedMessage.current) { if (!hasShownConnectedMessage.current) {
messageApi.success(t('connection-restored')); messageApiRef.current.success(tRef.current('connection-restored'));
hasShownConnectedMessage.current = true; hasShownConnectedMessage.current = true;
} }
}); });
@@ -64,7 +93,7 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ childr
socket.on('connect_error', error => { socket.on('connect_error', error => {
logger.error('Connection error', { error }); logger.error('Connection error', { error });
setConnected(false); setConnected(false);
messageApi.error(t('connection-lost')); messageApiRef.current.error(tRef.current('connection-lost'));
// Reset the connected message flag on error // Reset the connected message flag on error
hasShownConnectedMessage.current = false; hasShownConnectedMessage.current = false;
}); });
@@ -72,7 +101,7 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ childr
socket.on('disconnect', () => { socket.on('disconnect', () => {
logger.info('Socket disconnected'); logger.info('Socket disconnected');
setConnected(false); setConnected(false);
messageApi.loading(t('reconnecting')); messageApiRef.current.loading(tRef.current('reconnecting'));
// Reset the connected message flag on disconnect // Reset the connected message flag on disconnect
hasShownConnectedMessage.current = false; hasShownConnectedMessage.current = false;
@@ -121,10 +150,12 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ childr
// Then close the connection // Then close the connection
socket.close(); socket.close();
socketRef.current = null; socketRef.current = null;
globalSocketInstance = null; // Clear global instance
hasShownConnectedMessage.current = false; // Reset on unmount 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 = { const value = {
socket: socketRef.current, socket: socketRef.current,

View File

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

View File

@@ -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<string> {
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<string, { data: any; timestamp: number; duration: number }>();
// 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<T>(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<string>();
// 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<string>(`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<void[]> {
return Promise.all(
urls.map((url) => {
return new Promise<void>((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<string>();
// 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);
});
},
};

View File

@@ -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<string>();
private static deferredCSS = new Set<string>();
// 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<string> {
// 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<string>();
// 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<string> {
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<string>();
private static loadingPromises = new Map<string, Promise<void>>();
// Load CSS on demand
static async loadCSS(href: string, options: {
media?: string;
priority?: 'high' | 'low';
critical?: boolean;
} = {}): Promise<void> {
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<void>((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<string, string>,
breakpoints: Record<string, string>
): 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;
},
};

View File

@@ -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<PerformanceMetrics> = {
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<PerformanceMetrics> = {
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<string, number>();
// 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<T>(name: string, operation: () => Promise<T>): Promise<T> {
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<T extends any[], R>(
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,
};

View File

@@ -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<string, any> = {};
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<string, Task[]> = {};
groups.forEach((group: any) => {
tasksByGroup[group.id] = group.tasks || [];
});
return tasksByGroup;
}
);
export const selectOptimizedTaskCounts = createSelector(
[selectOptimizedTasksByGroup],
(tasksByGroup) => {
const counts: Record<string, number> = {};
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 = <T>(
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 = <T>(
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 = <T>(
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
};