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:
116
worklenz-frontend/package-lock.json
generated
116
worklenz-frontend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
),
|
||||||
|
};
|
||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
296
worklenz-frontend/src/styles/performance-optimizations.css
Normal file
296
worklenz-frontend/src/styles/performance-optimizations.css
Normal 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;
|
||||||
|
}
|
||||||
588
worklenz-frontend/src/utils/asset-optimizations.ts
Normal file
588
worklenz-frontend/src/utils/asset-optimizations.ts
Normal 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);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
598
worklenz-frontend/src/utils/css-optimizations.ts
Normal file
598
worklenz-frontend/src/utils/css-optimizations.ts
Normal 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;
|
||||||
|
},
|
||||||
|
};
|
||||||
680
worklenz-frontend/src/utils/enhanced-performance-monitoring.ts
Normal file
680
worklenz-frontend/src/utils/enhanced-performance-monitoring.ts
Normal 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,
|
||||||
|
};
|
||||||
319
worklenz-frontend/src/utils/redux-optimizations.ts
Normal file
319
worklenz-frontend/src/utils/redux-optimizations.ts
Normal 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
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user