diff --git a/perf-results-2026-01-04.json b/perf-results-2026-01-04.json new file mode 100644 index 0000000..cc5ef50 --- /dev/null +++ b/perf-results-2026-01-04.json @@ -0,0 +1,1137 @@ +{ + "timestamp": "2026-01-04T16:35:18.768Z", + "baseUrl": "http://localhost:3000", + "runsPerPage": 5, + "results": [ + { + "page": "Home", + "url": "http://localhost:3000/", + "runs": [ + { + "fcp": 152, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8, + "domContentLoaded": 157.70000000298023, + "loadComplete": 586.6000000014901, + "totalRequests": 100, + "totalBytes": 8103339, + "jsBytes": 4295712, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 144, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.799999997019768, + "domContentLoaded": 158.10000000149012, + "loadComplete": 565.3999999985099, + "totalRequests": 98, + "totalBytes": 8299347, + "jsBytes": 4295712, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 156, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 10.399999998509884, + "domContentLoaded": 181.39999999850988, + "loadComplete": 614.5, + "totalRequests": 98, + "totalBytes": 8299347, + "jsBytes": 4295712, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 148, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.700000002980232, + "domContentLoaded": 157.20000000298023, + "loadComplete": 565.3000000044703, + "totalRequests": 100, + "totalBytes": 8234411, + "jsBytes": 4295712, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 152, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.699999995529652, + "domContentLoaded": 164.20000000298023, + "loadComplete": 585.5, + "totalRequests": 98, + "totalBytes": 8299347, + "jsBytes": 4295712, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + ], + "average": { + "fcp": 150.4, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.519999998807908, + "domContentLoaded": 163.72000000178815, + "loadComplete": 583.4600000008941, + "totalRequests": 98.8, + "totalBytes": 8247158.2, + "jsBytes": 4295712, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "median": { + "fcp": 156, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 10.399999998509884, + "domContentLoaded": 181.39999999850988, + "loadComplete": 614.5, + "totalRequests": 98, + "totalBytes": 8299347, + "jsBytes": 4295712, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "p95": { + "fcp": 152, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.699999995529652, + "domContentLoaded": 164.20000000298023, + "loadComplete": 585.5, + "totalRequests": 98, + "totalBytes": 8299347, + "jsBytes": 4295712, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "min": { + "fcp": 144, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.700000002980232, + "domContentLoaded": 157.20000000298023, + "loadComplete": 565.3000000044703, + "totalRequests": 98, + "totalBytes": 8103339, + "jsBytes": 4295712, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "max": { + "fcp": 156, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 10.399999998509884, + "domContentLoaded": 181.39999999850988, + "loadComplete": 614.5, + "totalRequests": 100, + "totalBytes": 8299347, + "jsBytes": 4295712, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + }, + { + "page": "About", + "url": "http://localhost:3000/about", + "runs": [ + { + "fcp": 76, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 5.799999997019768, + "domContentLoaded": 82, + "loadComplete": 431.70000000298023, + "totalRequests": 92, + "totalBytes": 4338478, + "jsBytes": 4274041, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 80, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 6.700000002980232, + "domContentLoaded": 89, + "loadComplete": 462.5, + "totalRequests": 92, + "totalBytes": 4338478, + "jsBytes": 4274041, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 96, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.899999998509884, + "domContentLoaded": 105.89999999850988, + "loadComplete": 496.20000000298023, + "totalRequests": 92, + "totalBytes": 4338478, + "jsBytes": 4274041, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.399999998509884, + "domContentLoaded": 91.20000000298023, + "loadComplete": 470.1000000014901, + "totalRequests": 92, + "totalBytes": 4338478, + "jsBytes": 4274041, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.700000002980232, + "domContentLoaded": 89.89999999850988, + "loadComplete": 475.19999999552965, + "totalRequests": 92, + "totalBytes": 4338478, + "jsBytes": 4274041, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + ], + "average": { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.3, + "domContentLoaded": 91.6, + "loadComplete": 467.14000000059605, + "totalRequests": 92, + "totalBytes": 4338478, + "jsBytes": 4274041, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "median": { + "fcp": 96, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.899999998509884, + "domContentLoaded": 105.89999999850988, + "loadComplete": 496.20000000298023, + "totalRequests": 92, + "totalBytes": 4338478, + "jsBytes": 4274041, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "p95": { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.700000002980232, + "domContentLoaded": 89.89999999850988, + "loadComplete": 475.19999999552965, + "totalRequests": 92, + "totalBytes": 4338478, + "jsBytes": 4274041, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "min": { + "fcp": 76, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 5.799999997019768, + "domContentLoaded": 82, + "loadComplete": 431.70000000298023, + "totalRequests": 92, + "totalBytes": 4338478, + "jsBytes": 4274041, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "max": { + "fcp": 96, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.899999998509884, + "domContentLoaded": 105.89999999850988, + "loadComplete": 496.20000000298023, + "totalRequests": 92, + "totalBytes": 4338478, + "jsBytes": 4274041, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 87, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + }, + { + "page": "Blog Index", + "url": "http://localhost:3000/blog", + "runs": [ + { + "fcp": 100, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 11.100000001490116, + "domContentLoaded": 97.29999999701977, + "loadComplete": 545.5999999940395, + "totalRequests": 112, + "totalBytes": 4472603, + "jsBytes": 4408166, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 98, + "cssRequests": 0, + "imageRequests": 12, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 104, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 11.399999998509884, + "domContentLoaded": 101.5, + "loadComplete": 474.80000000447035, + "totalRequests": 112, + "totalBytes": 4472603, + "jsBytes": 4408166, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 98, + "cssRequests": 0, + "imageRequests": 12, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 100, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 10.899999998509884, + "domContentLoaded": 97.20000000298023, + "loadComplete": 528.5, + "totalRequests": 112, + "totalBytes": 4472603, + "jsBytes": 4408166, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 98, + "cssRequests": 0, + "imageRequests": 12, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 100, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 11, + "domContentLoaded": 98.69999999552965, + "loadComplete": 525.5, + "totalRequests": 112, + "totalBytes": 4472603, + "jsBytes": 4408166, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 98, + "cssRequests": 0, + "imageRequests": 12, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 100, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 9.5, + "domContentLoaded": 95.40000000596046, + "loadComplete": 505.1000000014901, + "totalRequests": 112, + "totalBytes": 4472603, + "jsBytes": 4408166, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 98, + "cssRequests": 0, + "imageRequests": 12, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + ], + "average": { + "fcp": 100.8, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 10.779999999701976, + "domContentLoaded": 98.02000000029803, + "loadComplete": 515.9, + "totalRequests": 112, + "totalBytes": 4472603, + "jsBytes": 4408166, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 98, + "cssRequests": 0, + "imageRequests": 12, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "median": { + "fcp": 100, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 10.899999998509884, + "domContentLoaded": 97.20000000298023, + "loadComplete": 528.5, + "totalRequests": 112, + "totalBytes": 4472603, + "jsBytes": 4408166, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 98, + "cssRequests": 0, + "imageRequests": 12, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "p95": { + "fcp": 100, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 9.5, + "domContentLoaded": 95.40000000596046, + "loadComplete": 505.1000000014901, + "totalRequests": 112, + "totalBytes": 4472603, + "jsBytes": 4408166, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 98, + "cssRequests": 0, + "imageRequests": 12, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "min": { + "fcp": 100, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 9.5, + "domContentLoaded": 95.40000000596046, + "loadComplete": 474.80000000447035, + "totalRequests": 112, + "totalBytes": 4472603, + "jsBytes": 4408166, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 98, + "cssRequests": 0, + "imageRequests": 12, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "max": { + "fcp": 104, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 11.399999998509884, + "domContentLoaded": 101.5, + "loadComplete": 545.5999999940395, + "totalRequests": 112, + "totalBytes": 4472603, + "jsBytes": 4408166, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 98, + "cssRequests": 0, + "imageRequests": 12, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + }, + { + "page": "Resume", + "url": "http://localhost:3000/resume", + "runs": [ + { + "fcp": 80, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 5.600000001490116, + "domContentLoaded": 81.89999999850988, + "loadComplete": 487.79999999701977, + "totalRequests": 95, + "totalBytes": 9252918, + "jsBytes": 7777227, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 88, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 80, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7, + "domContentLoaded": 85.60000000149012, + "loadComplete": 502, + "totalRequests": 95, + "totalBytes": 9252918, + "jsBytes": 7777227, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 88, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 80, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 6.600000001490116, + "domContentLoaded": 86.69999999552965, + "loadComplete": 597.5999999940395, + "totalRequests": 95, + "totalBytes": 9252918, + "jsBytes": 7777227, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 88, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.699999995529652, + "domContentLoaded": 88.29999999701977, + "loadComplete": 568.1000000014901, + "totalRequests": 95, + "totalBytes": 9252918, + "jsBytes": 7777227, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 88, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.700000002980232, + "domContentLoaded": 88.5, + "loadComplete": 482, + "totalRequests": 95, + "totalBytes": 9252918, + "jsBytes": 7777227, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 88, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + ], + "average": { + "fcp": 81.6, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 6.920000000298023, + "domContentLoaded": 86.19999999850988, + "loadComplete": 527.4999999985099, + "totalRequests": 95, + "totalBytes": 9252918, + "jsBytes": 7777227, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 88, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "median": { + "fcp": 80, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 6.600000001490116, + "domContentLoaded": 86.69999999552965, + "loadComplete": 597.5999999940395, + "totalRequests": 95, + "totalBytes": 9252918, + "jsBytes": 7777227, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 88, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "p95": { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.700000002980232, + "domContentLoaded": 88.5, + "loadComplete": 482, + "totalRequests": 95, + "totalBytes": 9252918, + "jsBytes": 7777227, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 88, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "min": { + "fcp": 80, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 5.600000001490116, + "domContentLoaded": 81.89999999850988, + "loadComplete": 482, + "totalRequests": 95, + "totalBytes": 9252918, + "jsBytes": 7777227, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 88, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "max": { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.700000002980232, + "domContentLoaded": 88.5, + "loadComplete": 597.5999999940395, + "totalRequests": 95, + "totalBytes": 9252918, + "jsBytes": 7777227, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 88, + "cssRequests": 0, + "imageRequests": 3, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + }, + { + "page": "Contact", + "url": "http://localhost:3000/contact", + "runs": [ + { + "fcp": 116, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.299999997019768, + "domContentLoaded": 91.39999999850988, + "loadComplete": 457.69999999552965, + "totalRequests": 97, + "totalBytes": 5076898, + "jsBytes": 4351416, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 4, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 116, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.799999997019768, + "domContentLoaded": 93.29999999701977, + "loadComplete": 472.1000000014901, + "totalRequests": 97, + "totalBytes": 5076898, + "jsBytes": 4351416, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 4, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 120, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.400000005960464, + "domContentLoaded": 97.59999999403954, + "loadComplete": 467.19999999552965, + "totalRequests": 97, + "totalBytes": 5076898, + "jsBytes": 4351416, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 4, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 120, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.100000001490116, + "domContentLoaded": 92.20000000298023, + "loadComplete": 451.20000000298023, + "totalRequests": 97, + "totalBytes": 5076898, + "jsBytes": 4351416, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 4, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 120, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8, + "domContentLoaded": 93.60000000149012, + "loadComplete": 537.8000000044703, + "totalRequests": 97, + "totalBytes": 5076898, + "jsBytes": 4351416, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 4, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + ], + "average": { + "fcp": 118.4, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.120000000298024, + "domContentLoaded": 93.61999999880791, + "loadComplete": 477.2, + "totalRequests": 97, + "totalBytes": 5076898, + "jsBytes": 4351416, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 4, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "median": { + "fcp": 120, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.400000005960464, + "domContentLoaded": 97.59999999403954, + "loadComplete": 467.19999999552965, + "totalRequests": 97, + "totalBytes": 5076898, + "jsBytes": 4351416, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 4, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "p95": { + "fcp": 120, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8, + "domContentLoaded": 93.60000000149012, + "loadComplete": 537.8000000044703, + "totalRequests": 97, + "totalBytes": 5076898, + "jsBytes": 4351416, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 4, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "min": { + "fcp": 116, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.799999997019768, + "domContentLoaded": 91.39999999850988, + "loadComplete": 451.20000000298023, + "totalRequests": 97, + "totalBytes": 5076898, + "jsBytes": 4351416, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 4, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "max": { + "fcp": 120, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.400000005960464, + "domContentLoaded": 97.59999999403954, + "loadComplete": 537.8000000044703, + "totalRequests": 97, + "totalBytes": 5076898, + "jsBytes": 4351416, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 4, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + } + ] +} \ No newline at end of file diff --git a/perf-results-baseline-7e89e6d.json b/perf-results-baseline-7e89e6d.json new file mode 100644 index 0000000..be0f8af --- /dev/null +++ b/perf-results-baseline-7e89e6d.json @@ -0,0 +1,1137 @@ +{ + "timestamp": "2026-01-04T16:33:45.014Z", + "baseUrl": "http://localhost:3000", + "runsPerPage": 5, + "results": [ + { + "page": "Home", + "url": "http://localhost:3000/", + "runs": [ + { + "fcp": 152, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.300000004470348, + "domContentLoaded": 156.29999999701977, + "loadComplete": 567.6000000014901, + "totalRequests": 105, + "totalBytes": 8432409, + "jsBytes": 4427576, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 7, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 152, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.899999998509884, + "domContentLoaded": 166.60000000149012, + "loadComplete": 567.3000000044703, + "totalRequests": 105, + "totalBytes": 8432409, + "jsBytes": 4427576, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 7, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 152, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 6.9000000059604645, + "domContentLoaded": 154.60000000149012, + "loadComplete": 569, + "totalRequests": 105, + "totalBytes": 8432409, + "jsBytes": 4427576, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 7, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 156, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.200000002980232, + "domContentLoaded": 159.39999999850988, + "loadComplete": 552, + "totalRequests": 105, + "totalBytes": 8432409, + "jsBytes": 4427576, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 7, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 156, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 10.600000001490116, + "domContentLoaded": 161.19999999552965, + "loadComplete": 591.5999999940395, + "totalRequests": 105, + "totalBytes": 8432409, + "jsBytes": 4427576, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 7, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + ], + "average": { + "fcp": 153.6, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.380000002682209, + "domContentLoaded": 159.6199999988079, + "loadComplete": 569.5, + "totalRequests": 105, + "totalBytes": 8432409, + "jsBytes": 4427576, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 7, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "median": { + "fcp": 152, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 6.9000000059604645, + "domContentLoaded": 154.60000000149012, + "loadComplete": 569, + "totalRequests": 105, + "totalBytes": 8432409, + "jsBytes": 4427576, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 7, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "p95": { + "fcp": 156, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 10.600000001490116, + "domContentLoaded": 161.19999999552965, + "loadComplete": 591.5999999940395, + "totalRequests": 105, + "totalBytes": 8432409, + "jsBytes": 4427576, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 7, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "min": { + "fcp": 152, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 6.9000000059604645, + "domContentLoaded": 154.60000000149012, + "loadComplete": 552, + "totalRequests": 105, + "totalBytes": 8432409, + "jsBytes": 4427576, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 7, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "max": { + "fcp": 156, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 10.600000001490116, + "domContentLoaded": 166.60000000149012, + "loadComplete": 591.5999999940395, + "totalRequests": 105, + "totalBytes": 8432409, + "jsBytes": 4427576, + "cssBytes": 0, + "imageBytes": 810035, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 7, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + }, + { + "page": "About", + "url": "http://localhost:3000/about", + "runs": [ + { + "fcp": 88, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.600000001490116, + "domContentLoaded": 92.70000000298023, + "loadComplete": 456, + "totalRequests": 103, + "totalBytes": 4494798, + "jsBytes": 4429163, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 88, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.100000001490116, + "domContentLoaded": 93.80000000447035, + "loadComplete": 458.6000000014901, + "totalRequests": 103, + "totalBytes": 4494798, + "jsBytes": 4429163, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 88, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.299999997019768, + "domContentLoaded": 94.70000000298023, + "loadComplete": 496.20000000298023, + "totalRequests": 103, + "totalBytes": 4494798, + "jsBytes": 4429163, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 88, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.4000000059604645, + "domContentLoaded": 91.10000000149012, + "loadComplete": 445.29999999701977, + "totalRequests": 103, + "totalBytes": 4494798, + "jsBytes": 4429163, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 88, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.899999998509884, + "domContentLoaded": 93, + "loadComplete": 504.79999999701977, + "totalRequests": 103, + "totalBytes": 4494798, + "jsBytes": 4429163, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + ], + "average": { + "fcp": 88, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.86000000089407, + "domContentLoaded": 93.06000000238419, + "loadComplete": 472.179999999702, + "totalRequests": 103, + "totalBytes": 4494798, + "jsBytes": 4429163, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "median": { + "fcp": 88, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.299999997019768, + "domContentLoaded": 94.70000000298023, + "loadComplete": 496.20000000298023, + "totalRequests": 103, + "totalBytes": 4494798, + "jsBytes": 4429163, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "p95": { + "fcp": 88, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.899999998509884, + "domContentLoaded": 93, + "loadComplete": 504.79999999701977, + "totalRequests": 103, + "totalBytes": 4494798, + "jsBytes": 4429163, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "min": { + "fcp": 88, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.4000000059604645, + "domContentLoaded": 91.10000000149012, + "loadComplete": 445.29999999701977, + "totalRequests": 103, + "totalBytes": 4494798, + "jsBytes": 4429163, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "max": { + "fcp": 88, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.299999997019768, + "domContentLoaded": 94.70000000298023, + "loadComplete": 504.79999999701977, + "totalRequests": 103, + "totalBytes": 4494798, + "jsBytes": 4429163, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + }, + { + "page": "Blog Index", + "url": "http://localhost:3000/blog", + "runs": [ + { + "fcp": 108, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 12.099999994039536, + "domContentLoaded": 102.29999999701977, + "loadComplete": 509.3999999985099, + "totalRequests": 121, + "totalBytes": 4613030, + "jsBytes": 4547395, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 103, + "cssRequests": 0, + "imageRequests": 15, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 108, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 11.5, + "domContentLoaded": 110, + "loadComplete": 503.80000000447035, + "totalRequests": 121, + "totalBytes": 4613030, + "jsBytes": 4547395, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 103, + "cssRequests": 0, + "imageRequests": 15, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 96, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 10.899999998509884, + "domContentLoaded": 92.69999999552965, + "loadComplete": 471.69999999552965, + "totalRequests": 121, + "totalBytes": 4613030, + "jsBytes": 4547395, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 103, + "cssRequests": 0, + "imageRequests": 15, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 104, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 11.299999997019768, + "domContentLoaded": 99.30000000447035, + "loadComplete": 475.70000000298023, + "totalRequests": 121, + "totalBytes": 4613030, + "jsBytes": 4547395, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 103, + "cssRequests": 0, + "imageRequests": 15, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 104, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 11.5, + "domContentLoaded": 99.40000000596046, + "loadComplete": 537.6000000014901, + "totalRequests": 121, + "totalBytes": 4613030, + "jsBytes": 4547395, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 103, + "cssRequests": 0, + "imageRequests": 15, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + ], + "average": { + "fcp": 104, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 11.459999997913837, + "domContentLoaded": 100.74000000059604, + "loadComplete": 499.64000000059605, + "totalRequests": 121, + "totalBytes": 4613030, + "jsBytes": 4547395, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 103, + "cssRequests": 0, + "imageRequests": 15, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "median": { + "fcp": 96, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 10.899999998509884, + "domContentLoaded": 92.69999999552965, + "loadComplete": 471.69999999552965, + "totalRequests": 121, + "totalBytes": 4613030, + "jsBytes": 4547395, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 103, + "cssRequests": 0, + "imageRequests": 15, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "p95": { + "fcp": 104, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 11.5, + "domContentLoaded": 99.40000000596046, + "loadComplete": 537.6000000014901, + "totalRequests": 121, + "totalBytes": 4613030, + "jsBytes": 4547395, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 103, + "cssRequests": 0, + "imageRequests": 15, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "min": { + "fcp": 96, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 10.899999998509884, + "domContentLoaded": 92.69999999552965, + "loadComplete": 471.69999999552965, + "totalRequests": 121, + "totalBytes": 4613030, + "jsBytes": 4547395, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 103, + "cssRequests": 0, + "imageRequests": 15, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "max": { + "fcp": 108, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 12.099999994039536, + "domContentLoaded": 110, + "loadComplete": 537.6000000014901, + "totalRequests": 121, + "totalBytes": 4613030, + "jsBytes": 4547395, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 103, + "cssRequests": 0, + "imageRequests": 15, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + }, + { + "page": "Resume", + "url": "http://localhost:3000/resume", + "runs": [ + { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.9000000059604645, + "domContentLoaded": 90.19999999552965, + "loadComplete": 462.3999999985099, + "totalRequests": 99, + "totalBytes": 4475494, + "jsBytes": 4409859, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.700000002980232, + "domContentLoaded": 89.5, + "loadComplete": 632.6999999955297, + "totalRequests": 99, + "totalBytes": 4475494, + "jsBytes": 4409859, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.300000004470348, + "domContentLoaded": 97.29999999701977, + "loadComplete": 453.79999999701977, + "totalRequests": 99, + "totalBytes": 4475494, + "jsBytes": 4409859, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.299999997019768, + "domContentLoaded": 92.20000000298023, + "loadComplete": 491.3999999985099, + "totalRequests": 99, + "totalBytes": 4475494, + "jsBytes": 4409859, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.100000001490116, + "domContentLoaded": 90.70000000298023, + "loadComplete": 429, + "totalRequests": 99, + "totalBytes": 4475494, + "jsBytes": 4409859, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + ], + "average": { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.460000002384186, + "domContentLoaded": 91.97999999970197, + "loadComplete": 493.85999999791386, + "totalRequests": 99, + "totalBytes": 4475494, + "jsBytes": 4409859, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "median": { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.300000004470348, + "domContentLoaded": 97.29999999701977, + "loadComplete": 453.79999999701977, + "totalRequests": 99, + "totalBytes": 4475494, + "jsBytes": 4409859, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "p95": { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.100000001490116, + "domContentLoaded": 90.70000000298023, + "loadComplete": 429, + "totalRequests": 99, + "totalBytes": 4475494, + "jsBytes": 4409859, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "min": { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.100000001490116, + "domContentLoaded": 89.5, + "loadComplete": 429, + "totalRequests": 99, + "totalBytes": 4475494, + "jsBytes": 4409859, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "max": { + "fcp": 84, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 7.9000000059604645, + "domContentLoaded": 97.29999999701977, + "loadComplete": 632.6999999955297, + "totalRequests": 99, + "totalBytes": 4475494, + "jsBytes": 4409859, + "cssBytes": 0, + "imageBytes": 0, + "fontBytes": 0, + "jsRequests": 91, + "cssRequests": 0, + "imageRequests": 5, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + }, + { + "page": "Contact", + "url": "http://localhost:3000/contact", + "runs": [ + { + "fcp": 120, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.200000002980232, + "domContentLoaded": 100.89999999850988, + "loadComplete": 469.6000000014901, + "totalRequests": 104, + "totalBytes": 5210481, + "jsBytes": 4483801, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 6, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 120, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.600000001490116, + "domContentLoaded": 93.60000000149012, + "loadComplete": 483.6000000014901, + "totalRequests": 104, + "totalBytes": 5210481, + "jsBytes": 4483801, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 6, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 120, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.700000002980232, + "domContentLoaded": 94.79999999701977, + "loadComplete": 470, + "totalRequests": 104, + "totalBytes": 5210481, + "jsBytes": 4483801, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 6, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 120, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8, + "domContentLoaded": 94.80000000447035, + "loadComplete": 505.30000000447035, + "totalRequests": 104, + "totalBytes": 5210481, + "jsBytes": 4483801, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 6, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + { + "fcp": 116, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.299999997019768, + "domContentLoaded": 92.89999999850988, + "loadComplete": 462.1000000014901, + "totalRequests": 104, + "totalBytes": 5210481, + "jsBytes": 4483801, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 6, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + ], + "average": { + "fcp": 119.2, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.36000000089407, + "domContentLoaded": 95.4, + "loadComplete": 478.1200000017881, + "totalRequests": 104, + "totalBytes": 5210481, + "jsBytes": 4483801, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 6, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "median": { + "fcp": 120, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.700000002980232, + "domContentLoaded": 94.79999999701977, + "loadComplete": 470, + "totalRequests": 104, + "totalBytes": 5210481, + "jsBytes": 4483801, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 6, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "p95": { + "fcp": 116, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.299999997019768, + "domContentLoaded": 92.89999999850988, + "loadComplete": 462.1000000014901, + "totalRequests": 104, + "totalBytes": 5210481, + "jsBytes": 4483801, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 6, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "min": { + "fcp": 116, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8, + "domContentLoaded": 92.89999999850988, + "loadComplete": 462.1000000014901, + "totalRequests": 104, + "totalBytes": 5210481, + "jsBytes": 4483801, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 6, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + }, + "max": { + "fcp": 120, + "lcp": 0, + "cls": 0, + "fid": 0, + "ttfb": 8.700000002980232, + "domContentLoaded": 100.89999999850988, + "loadComplete": 505.30000000447035, + "totalRequests": 104, + "totalBytes": 5210481, + "jsBytes": 4483801, + "cssBytes": 0, + "imageBytes": 660146, + "fontBytes": 0, + "jsRequests": 95, + "cssRequests": 0, + "imageRequests": 6, + "jsExecutionTime": 0, + "taskDuration": 0, + "layoutDuration": 0, + "paintDuration": 0 + } + } + ] +} \ No newline at end of file diff --git a/scripts/perf-compare.ts b/scripts/perf-compare.ts new file mode 100644 index 0000000..e81827c --- /dev/null +++ b/scripts/perf-compare.ts @@ -0,0 +1,391 @@ +#!/usr/bin/env bun +/** + * Performance Test Comparison Tool + * + * Compares two performance test results and shows the differences + */ + +import { readFileSync } from "fs"; + +interface PerformanceMetrics { + fcp: number; + lcp: number; + cls: number; + fid: number; + ttfb: number; + domContentLoaded: number; + loadComplete: number; + totalRequests: number; + totalBytes: number; + jsBytes: number; + cssBytes: number; + imageBytes: number; + fontBytes: number; + jsRequests: number; + cssRequests: number; + imageRequests: number; + jsExecutionTime: number; + taskDuration: number; + layoutDuration: number; + paintDuration: number; +} + +interface TestResult { + page: string; + url: string; + median: PerformanceMetrics; +} + +interface TestOutput { + timestamp: string; + baseUrl: string; + runsPerPage: number; + results: TestResult[]; +} + +function formatBytes(bytes: number): string { + if (bytes < 1024) return `${bytes.toFixed(0)}B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`; + return `${(bytes / (1024 * 1024)).toFixed(2)}MB`; +} + +function formatTime(ms: number): string { + if (ms < 1000) return `${ms.toFixed(0)}ms`; + return `${(ms / 1000).toFixed(2)}s`; +} + +function formatDiff(value: number, unit: "ms" | "bytes" | "count"): string { + const sign = value > 0 ? "+" : ""; + + if (unit === "ms") { + return value === 0 ? "→" : `${sign}${formatTime(Math.abs(value))}`; + } else if (unit === "bytes") { + return value === 0 ? "→" : `${sign}${formatBytes(Math.abs(value))}`; + } else { + return value === 0 ? "→" : `${sign}${value.toFixed(0)}`; + } +} + +function getImpact(value: number, threshold: number = 5): string { + const percentChange = Math.abs(value); + if (percentChange < threshold) return ""; + if (value < 0) return " šŸŽ‰"; // Improvement + if (value > 0) return " āš ļø"; // Regression + return ""; +} + +function calculatePercentChange(before: number, after: number): number { + if (before === 0) return after === 0 ? 0 : 100; + return ((after - before) / before) * 100; +} + +function compareResults(baseline: TestOutput, optimized: TestOutput) { + console.log("\n"); + console.log( + "═══════════════════════════════════════════════════════════════════" + ); + console.log( + " PERFORMANCE COMPARISON REPORT " + ); + console.log( + "═══════════════════════════════════════════════════════════════════" + ); + console.log(`Baseline: ${baseline.timestamp}`); + console.log(`Optimized: ${optimized.timestamp}`); + console.log( + "───────────────────────────────────────────────────────────────────\n" + ); + + // Compare each page + for (const baseResult of baseline.results) { + const optResult = optimized.results.find((r) => r.page === baseResult.page); + if (!optResult) continue; + + const base = baseResult.median; + const opt = optResult.median; + + console.log(`\nšŸ“„ ${baseResult.page}`); + console.log("─".repeat(70)); + + // Core Web Vitals + console.log("\n Core Web Vitals:"); + + const fcpDiff = opt.fcp - base.fcp; + const fcpPercent = calculatePercentChange(base.fcp, opt.fcp); + console.log( + ` FCP: ${formatTime(base.fcp)} → ${formatTime(opt.fcp)} (${formatDiff(fcpDiff, "ms")}, ${fcpPercent.toFixed(1)}%)${getImpact(fcpPercent)}` + ); + + const clsDiff = opt.cls - base.cls; + console.log( + ` CLS: ${base.cls.toFixed(3)} → ${opt.cls.toFixed(3)} (${formatDiff(clsDiff * 1000, "ms")})` + ); + + // Loading Metrics + console.log("\n Loading Metrics:"); + + const ttfbDiff = opt.ttfb - base.ttfb; + const ttfbPercent = calculatePercentChange(base.ttfb, opt.ttfb); + console.log( + ` TTFB: ${formatTime(base.ttfb)} → ${formatTime(opt.ttfb)} (${formatDiff(ttfbDiff, "ms")}, ${ttfbPercent.toFixed(1)}%)${getImpact(ttfbPercent)}` + ); + + const dclDiff = opt.domContentLoaded - base.domContentLoaded; + const dclPercent = calculatePercentChange( + base.domContentLoaded, + opt.domContentLoaded + ); + console.log( + ` DCL: ${formatTime(base.domContentLoaded)} → ${formatTime(opt.domContentLoaded)} (${formatDiff(dclDiff, "ms")}, ${dclPercent.toFixed(1)}%)${getImpact(dclPercent)}` + ); + + const loadDiff = opt.loadComplete - base.loadComplete; + const loadPercent = calculatePercentChange( + base.loadComplete, + opt.loadComplete + ); + console.log( + ` Load: ${formatTime(base.loadComplete)} → ${formatTime(opt.loadComplete)} (${formatDiff(loadDiff, "ms")}, ${loadPercent.toFixed(1)}%)${getImpact(loadPercent)}` + ); + + // Resource Loading + console.log("\n Resources:"); + + const reqDiff = opt.totalRequests - base.totalRequests; + const reqPercent = calculatePercentChange( + base.totalRequests, + opt.totalRequests + ); + console.log( + ` Requests: ${base.totalRequests} → ${opt.totalRequests} (${formatDiff(reqDiff, "count")}, ${reqPercent.toFixed(1)}%)${getImpact(reqPercent, 10)}` + ); + + const bytesDiff = opt.totalBytes - base.totalBytes; + const bytesPercent = calculatePercentChange( + base.totalBytes, + opt.totalBytes + ); + console.log( + ` Total Size: ${formatBytes(base.totalBytes)} → ${formatBytes(opt.totalBytes)} (${formatDiff(bytesDiff, "bytes")}, ${bytesPercent.toFixed(1)}%)${getImpact(bytesPercent, 10)}` + ); + + const jsDiff = opt.jsBytes - base.jsBytes; + const jsPercent = calculatePercentChange(base.jsBytes, opt.jsBytes); + console.log( + ` JS Size: ${formatBytes(base.jsBytes)} → ${formatBytes(opt.jsBytes)} (${formatDiff(jsDiff, "bytes")}, ${jsPercent.toFixed(1)}%)${getImpact(jsPercent, 10)}` + ); + + const jsReqDiff = opt.jsRequests - base.jsRequests; + const jsReqPercent = calculatePercentChange( + base.jsRequests, + opt.jsRequests + ); + console.log( + ` JS Requests: ${base.jsRequests} → ${opt.jsRequests} (${formatDiff(jsReqDiff, "count")}, ${jsReqPercent.toFixed(1)}%)${getImpact(jsReqPercent, 10)}` + ); + } + + // Overall Summary + console.log( + "\n\n═══════════════════════════════════════════════════════════════════" + ); + console.log( + " OVERALL SUMMARY " + ); + console.log( + "═══════════════════════════════════════════════════════════════════\n" + ); + + const baseAvg = { + fcp: + baseline.results.reduce((sum, r) => sum + r.median.fcp, 0) / + baseline.results.length, + ttfb: + baseline.results.reduce((sum, r) => sum + r.median.ttfb, 0) / + baseline.results.length, + dcl: + baseline.results.reduce((sum, r) => sum + r.median.domContentLoaded, 0) / + baseline.results.length, + load: + baseline.results.reduce((sum, r) => sum + r.median.loadComplete, 0) / + baseline.results.length, + requests: + baseline.results.reduce((sum, r) => sum + r.median.totalRequests, 0) / + baseline.results.length, + bytes: + baseline.results.reduce((sum, r) => sum + r.median.totalBytes, 0) / + baseline.results.length, + jsBytes: + baseline.results.reduce((sum, r) => sum + r.median.jsBytes, 0) / + baseline.results.length, + jsRequests: + baseline.results.reduce((sum, r) => sum + r.median.jsRequests, 0) / + baseline.results.length + }; + + const optAvg = { + fcp: + optimized.results.reduce((sum, r) => sum + r.median.fcp, 0) / + optimized.results.length, + ttfb: + optimized.results.reduce((sum, r) => sum + r.median.ttfb, 0) / + optimized.results.length, + dcl: + optimized.results.reduce((sum, r) => sum + r.median.domContentLoaded, 0) / + optimized.results.length, + load: + optimized.results.reduce((sum, r) => sum + r.median.loadComplete, 0) / + optimized.results.length, + requests: + optimized.results.reduce((sum, r) => sum + r.median.totalRequests, 0) / + optimized.results.length, + bytes: + optimized.results.reduce((sum, r) => sum + r.median.totalBytes, 0) / + optimized.results.length, + jsBytes: + optimized.results.reduce((sum, r) => sum + r.median.jsBytes, 0) / + optimized.results.length, + jsRequests: + optimized.results.reduce((sum, r) => sum + r.median.jsRequests, 0) / + optimized.results.length + }; + + console.log(" Average Across All Pages:\n"); + + const metrics = [ + { name: "FCP", base: baseAvg.fcp, opt: optAvg.fcp, unit: "ms" as const }, + { name: "TTFB", base: baseAvg.ttfb, opt: optAvg.ttfb, unit: "ms" as const }, + { + name: "DOM Content Loaded", + base: baseAvg.dcl, + opt: optAvg.dcl, + unit: "ms" as const + }, + { + name: "Load Complete", + base: baseAvg.load, + opt: optAvg.load, + unit: "ms" as const + }, + { + name: "Total Requests", + base: baseAvg.requests, + opt: optAvg.requests, + unit: "count" as const + }, + { + name: "Total Size", + base: baseAvg.bytes, + opt: optAvg.bytes, + unit: "bytes" as const + }, + { + name: "JS Size", + base: baseAvg.jsBytes, + opt: optAvg.jsBytes, + unit: "bytes" as const + }, + { + name: "JS Requests", + base: baseAvg.jsRequests, + opt: optAvg.jsRequests, + unit: "count" as const + } + ]; + + metrics.forEach((metric) => { + const diff = metric.opt - metric.base; + const percent = calculatePercentChange(metric.base, metric.opt); + const baseStr = + metric.unit === "bytes" + ? formatBytes(metric.base) + : metric.unit === "ms" + ? formatTime(metric.base) + : metric.base.toFixed(1); + const optStr = + metric.unit === "bytes" + ? formatBytes(metric.opt) + : metric.unit === "ms" + ? formatTime(metric.opt) + : metric.opt.toFixed(1); + + console.log( + ` ${metric.name.padEnd(20)} ${baseStr.padEnd(10)} → ${optStr.padEnd(10)} (${formatDiff(diff, metric.unit).padEnd(12)}, ${percent.toFixed(1).padStart(6)}%)${getImpact(percent, 5)}` + ); + }); + + console.log("\n Key Findings:\n"); + + let improvements = 0; + let regressions = 0; + + metrics.forEach((metric) => { + const percent = calculatePercentChange(metric.base, metric.opt); + if (Math.abs(percent) >= 5) { + if (percent < 0) improvements++; + else regressions++; + } + }); + + if (improvements > 0) { + console.log( + ` āœ… ${improvements} significant improvement${improvements === 1 ? "" : "s"}` + ); + } + if (regressions > 0) { + console.log( + ` āš ļø ${regressions} significant regression${regressions === 1 ? "" : "s"}` + ); + } + + // Specific findings + const reqPercent = calculatePercentChange(baseAvg.requests, optAvg.requests); + if (reqPercent < -5) { + console.log( + ` šŸŽÆ Reduced HTTP requests by ${Math.abs(reqPercent).toFixed(1)}%` + ); + } + + const jsPercent = calculatePercentChange(baseAvg.jsBytes, optAvg.jsBytes); + if (jsPercent < -5) { + console.log( + ` šŸ“¦ Reduced JS bundle size by ${Math.abs(jsPercent).toFixed(1)}%` + ); + } + + const loadPercent = calculatePercentChange(baseAvg.load, optAvg.load); + if (Math.abs(loadPercent) < 5) { + console.log( + ` āš–ļø Load time remained stable (${Math.abs(loadPercent).toFixed(1)}% change)` + ); + } + + console.log("\n"); +} + +function main() { + const args = process.argv.slice(2); + + if (args.length !== 2) { + console.error("Usage: bun run compare.ts "); + process.exit(1); + } + + const [baselinePath, optimizedPath] = args; + + try { + const baseline: TestOutput = JSON.parse( + readFileSync(baselinePath, "utf-8") + ); + const optimized: TestOutput = JSON.parse( + readFileSync(optimizedPath, "utf-8") + ); + + compareResults(baseline, optimized); + } catch (error) { + console.error("Error reading or parsing files:", error); + process.exit(1); + } +} + +main(); diff --git a/scripts/perf-test.ts b/scripts/perf-test.ts new file mode 100644 index 0000000..2e5ba99 --- /dev/null +++ b/scripts/perf-test.ts @@ -0,0 +1,586 @@ +#!/usr/bin/env bun +/** + * Comprehensive Page Load Performance Testing Suite + * + * Measures: + * - First Contentful Paint (FCP) + * - Largest Contentful Paint (LCP) + * - Time to Interactive (TTI) + * - Total Blocking Time (TBT) + * - Cumulative Layout Shift (CLS) + * - First Input Delay (FID) + * - Network requests and bundle sizes + * - JavaScript execution time + */ + +import { chromium, type Browser, type Page } from "playwright"; +import { writeFileSync } from "fs"; + +interface PageTestConfig { + name: string; + path: string; +} + +interface PerformanceMetrics { + fcp: number; + lcp: number; + cls: number; + fid: number; + ttfb: number; + domContentLoaded: number; + loadComplete: number; + totalRequests: number; + totalBytes: number; + jsBytes: number; + cssBytes: number; + imageBytes: number; + fontBytes: number; + jsRequests: number; + cssRequests: number; + imageRequests: number; + jsExecutionTime: number; + taskDuration: number; + layoutDuration: number; + paintDuration: number; +} + +interface TestResult { + page: string; + url: string; + runs: PerformanceMetrics[]; + average: PerformanceMetrics; + median: PerformanceMetrics; + p95: PerformanceMetrics; + min: PerformanceMetrics; + max: PerformanceMetrics; +} + +const BASE_URL = process.env.TEST_URL || "http://localhost:3000"; +const RUNS_PER_PAGE = parseInt(process.env.RUNS || "5", 10); +const WARMUP_RUNS = 1; + +// Pages to test +const TEST_PAGES: PageTestConfig[] = [ + { name: "Home", path: "/" }, + { name: "About", path: "/about" }, + { name: "Blog Index", path: "/blog" }, + { name: "Resume", path: "/resume" }, + { name: "Contact", path: "/contact" } +]; + +// Add blog post path if provided +if (process.env.TEST_BLOG_POST) { + TEST_PAGES.push({ name: "Blog Post", path: process.env.TEST_BLOG_POST }); +} + +async function collectPerformanceMetrics( + page: Page +): Promise { + // Wait for page to be fully loaded + await page.waitForLoadState("networkidle"); + + // Collect comprehensive performance metrics + const metrics = await page.evaluate(() => { + const perf = performance.getEntriesByType( + "navigation" + )[0] as PerformanceNavigationTiming; + const paint = performance.getEntriesByType("paint"); + const fcp = paint.find((entry) => entry.name === "first-contentful-paint"); + + // Get LCP using PerformanceObserver + let lcp = 0; + let cls = 0; + let fid = 0; + + // Try to get LCP from existing entries + const lcpEntries = performance.getEntriesByType( + "largest-contentful-paint" + ) as any[]; + if (lcpEntries.length > 0) { + lcp = + lcpEntries[lcpEntries.length - 1].renderTime || + lcpEntries[lcpEntries.length - 1].loadTime; + } + + // Get layout shift entries + const layoutShiftEntries = performance.getEntriesByType( + "layout-shift" + ) as any[]; + cls = layoutShiftEntries + .filter((entry: any) => !entry.hadRecentInput) + .reduce((sum: number, entry: any) => sum + entry.value, 0); + + // Get resource timing + const resources = performance.getEntriesByType( + "resource" + ) as PerformanceResourceTiming[]; + + let totalBytes = 0; + let jsBytes = 0; + let cssBytes = 0; + let imageBytes = 0; + let fontBytes = 0; + let jsRequests = 0; + let cssRequests = 0; + let imageRequests = 0; + + resources.forEach((resource) => { + const size = resource.transferSize || resource.encodedBodySize || 0; + totalBytes += size; + + const isJS = + resource.name.includes(".js") || + resource.name.includes("/_build/") || + resource.initiatorType === "script"; + const isCSS = + resource.name.includes(".css") || resource.initiatorType === "css"; + const isImage = + resource.initiatorType === "img" || + /\.(jpg|jpeg|png|gif|svg|webp|avif)/.test(resource.name); + const isFont = /\.(woff|woff2|ttf|otf|eot)/.test(resource.name); + + if (isJS) { + jsBytes += size; + jsRequests++; + } else if (isCSS) { + cssBytes += size; + cssRequests++; + } else if (isImage) { + imageBytes += size; + imageRequests++; + } else if (isFont) { + fontBytes += size; + } + }); + + // Get performance measure entries for JS execution + const measures = performance.getEntriesByType("measure"); + let jsExecutionTime = 0; + let taskDuration = 0; + let layoutDuration = 0; + let paintDuration = 0; + + measures.forEach((entry) => { + if (entry.name.includes("script") || entry.name.includes("js")) { + jsExecutionTime += entry.duration; + } + }); + + // Try to get long task entries + const longTasks = performance.getEntriesByType("longtask") as any[]; + longTasks.forEach((task: any) => { + taskDuration += task.duration; + }); + + return { + fcp: fcp?.startTime || 0, + lcp, + cls, + fid, + ttfb: perf.responseStart - perf.requestStart, + domContentLoaded: perf.domContentLoadedEventEnd - perf.fetchStart, + loadComplete: perf.loadEventEnd - perf.fetchStart, + totalRequests: resources.length, + totalBytes, + jsBytes, + cssBytes, + imageBytes, + fontBytes, + jsRequests, + cssRequests, + imageRequests, + jsExecutionTime, + taskDuration, + layoutDuration, + paintDuration + }; + }); + + return metrics; +} + +async function testPagePerformance( + browser: Browser, + pageConfig: PageTestConfig +): Promise { + const url = `${BASE_URL}${pageConfig.path}`; + const runs: PerformanceMetrics[] = []; + + console.log(`\nšŸ“Š Testing: ${pageConfig.name} (${url})`); + console.log( + ` Running ${WARMUP_RUNS} warmup + ${RUNS_PER_PAGE} measured runs...\n` + ); + + // Warmup runs (not counted) + for (let i = 0; i < WARMUP_RUNS; i++) { + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto(url, { waitUntil: "networkidle" }); + await page.close(); + await context.close(); + console.log(` āœ“ Warmup run ${i + 1}/${WARMUP_RUNS}`); + } + + // Measured runs + for (let i = 0; i < RUNS_PER_PAGE; i++) { + console.log(` → Run ${i + 1}/${RUNS_PER_PAGE}...`); + + // Create new context for each run to ensure clean state + const context = await browser.newContext({ + viewport: { width: 1920, height: 1080 } + }); + const page = await context.newPage(); + + // Navigate and collect metrics + await page.goto(url, { waitUntil: "networkidle" }); + const metrics = await collectPerformanceMetrics(page); + + await page.close(); + await context.close(); + + runs.push(metrics); + console.log( + ` FCP: ${metrics.fcp.toFixed(0)}ms | LCP: ${metrics.lcp.toFixed(0)}ms | CLS: ${metrics.cls.toFixed(3)} | Requests: ${metrics.totalRequests}` + ); + } + + // Calculate statistics + const average = calculateAverage(runs); + const median = calculateMedian(runs); + const p95 = calculatePercentile(runs, 95); + const min = calculateMin(runs); + const max = calculateMax(runs); + + return { + page: pageConfig.name, + url, + runs, + average, + median, + p95, + min, + max + }; +} + +function calculateAverage(runs: PerformanceMetrics[]): PerformanceMetrics { + const sum = runs.reduce((acc, run) => { + Object.keys(run).forEach((key) => { + acc[key] = (acc[key] || 0) + run[key as keyof PerformanceMetrics]; + }); + return acc; + }, {} as any); + + Object.keys(sum).forEach((key) => { + sum[key] /= runs.length; + }); + + return sum; +} + +function calculateMedian(runs: PerformanceMetrics[]): PerformanceMetrics { + const sorted = runs.slice().sort((a, b) => a.lcp - b.lcp); + const mid = Math.floor(sorted.length / 2); + return sorted[mid]; +} + +function calculatePercentile( + runs: PerformanceMetrics[], + percentile: number +): PerformanceMetrics { + const sorted = runs.slice().sort((a, b) => a.lcp - b.lcp); + const index = Math.ceil((percentile / 100) * sorted.length) - 1; + return sorted[index]; +} + +function calculateMin(runs: PerformanceMetrics[]): PerformanceMetrics { + return runs.reduce( + (min, run) => { + const result: any = {}; + Object.keys(run).forEach((key) => { + const k = key as keyof PerformanceMetrics; + result[k] = Math.min(min[k], run[k]); + }); + return result; + }, + { ...runs[0] } + ); +} + +function calculateMax(runs: PerformanceMetrics[]): PerformanceMetrics { + return runs.reduce( + (max, run) => { + const result: any = {}; + Object.keys(run).forEach((key) => { + const k = key as keyof PerformanceMetrics; + result[k] = Math.max(max[k], run[k]); + }); + return result; + }, + { ...runs[0] } + ); +} + +function formatBytes(bytes: number): string { + if (bytes < 1024) return `${bytes.toFixed(0)}B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`; + return `${(bytes / (1024 * 1024)).toFixed(2)}MB`; +} + +function formatTime(ms: number): string { + if (ms < 1000) return `${ms.toFixed(0)}ms`; + return `${(ms / 1000).toFixed(2)}s`; +} + +function getWebVitalRating( + metric: "lcp" | "fcp" | "cls" | "fid", + value: number +): string { + const thresholds = { + lcp: { good: 2500, needsImprovement: 4000 }, + fcp: { good: 1800, needsImprovement: 3000 }, + cls: { good: 0.1, needsImprovement: 0.25 }, + fid: { good: 100, needsImprovement: 300 } + }; + + const t = thresholds[metric]; + if (value <= t.good) return "🟢 Good"; + if (value <= t.needsImprovement) return "🟔 Needs Improvement"; + return "šŸ”“ Poor"; +} + +function printResults(results: TestResult[]) { + console.log("\n\n"); + console.log( + "═══════════════════════════════════════════════════════════════════" + ); + console.log( + " PERFORMANCE TEST RESULTS " + ); + console.log( + "═══════════════════════════════════════════════════════════════════" + ); + console.log(`Base URL: ${BASE_URL}`); + console.log(`Runs per page: ${RUNS_PER_PAGE}`); + console.log(`Date: ${new Date().toLocaleString()}`); + console.log( + "───────────────────────────────────────────────────────────────────\n" + ); + + results.forEach((result) => { + console.log(`\nšŸ“„ ${result.page} - ${result.url}`); + console.log("─".repeat(70)); + + console.log("\n Core Web Vitals (Median | Min → Max):"); + console.log( + ` LCP (Largest Contentful Paint): ${formatTime(result.median.lcp).padEnd(8)} | ${formatTime(result.min.lcp)} → ${formatTime(result.max.lcp)} ${getWebVitalRating("lcp", result.median.lcp)}` + ); + console.log( + ` FCP (First Contentful Paint): ${formatTime(result.median.fcp).padEnd(8)} | ${formatTime(result.min.fcp)} → ${formatTime(result.max.fcp)} ${getWebVitalRating("fcp", result.median.fcp)}` + ); + console.log( + ` CLS (Cumulative Layout Shift): ${result.median.cls.toFixed(3).padEnd(8)} | ${result.min.cls.toFixed(3)} → ${result.max.cls.toFixed(3)} ${getWebVitalRating("cls", result.median.cls)}` + ); + + console.log("\n Loading Metrics (Median):"); + console.log( + ` TTFB (Time to First Byte): ${formatTime(result.median.ttfb)}` + ); + console.log( + ` DOM Content Loaded: ${formatTime(result.median.domContentLoaded)}` + ); + console.log( + ` Load Complete: ${formatTime(result.median.loadComplete)}` + ); + + console.log("\n Resource Loading (Median):"); + console.log( + ` Total Requests: ${result.median.totalRequests.toFixed(0)}` + ); + console.log( + ` Total Transfer Size: ${formatBytes(result.median.totalBytes)}` + ); + console.log( + ` ā”œā”€ JavaScript (${result.median.jsRequests.toFixed(0)} req): ${formatBytes(result.median.jsBytes)}` + ); + console.log( + ` ā”œā”€ CSS (${result.median.cssRequests.toFixed(0)} req): ${formatBytes(result.median.cssBytes)}` + ); + console.log( + ` ā”œā”€ Images (${result.median.imageRequests.toFixed(0)} req): ${formatBytes(result.median.imageBytes)}` + ); + console.log( + ` └─ Fonts: ${formatBytes(result.median.fontBytes)}` + ); + + if (result.median.jsExecutionTime > 0) { + console.log("\n Performance Details (Median):"); + console.log( + ` JS Execution Time: ${formatTime(result.median.jsExecutionTime)}` + ); + if (result.median.taskDuration > 0) { + console.log( + ` Long Task Duration: ${formatTime(result.median.taskDuration)}` + ); + } + } + + console.log("\n Variability (Standard Deviation):"); + const lcpStdDev = Math.sqrt( + result.runs.reduce( + (sum, run) => sum + Math.pow(run.lcp - result.average.lcp, 2), + 0 + ) / result.runs.length + ); + const fcpStdDev = Math.sqrt( + result.runs.reduce( + (sum, run) => sum + Math.pow(run.fcp - result.average.fcp, 2), + 0 + ) / result.runs.length + ); + console.log(` LCP: ±${formatTime(lcpStdDev)}`); + console.log(` FCP: ±${formatTime(fcpStdDev)}`); + }); + + console.log( + "\n\n═══════════════════════════════════════════════════════════════════" + ); + console.log( + " SUMMARY " + ); + console.log( + "═══════════════════════════════════════════════════════════════════\n" + ); + + // Overall averages + const overallAverage = { + lcp: results.reduce((sum, r) => sum + r.median.lcp, 0) / results.length, + fcp: results.reduce((sum, r) => sum + r.median.fcp, 0) / results.length, + cls: results.reduce((sum, r) => sum + r.median.cls, 0) / results.length, + ttfb: results.reduce((sum, r) => sum + r.median.ttfb, 0) / results.length, + totalBytes: + results.reduce((sum, r) => sum + r.median.totalBytes, 0) / results.length, + jsBytes: + results.reduce((sum, r) => sum + r.median.jsBytes, 0) / results.length, + totalRequests: + results.reduce((sum, r) => sum + r.median.totalRequests, 0) / + results.length + }; + + console.log(" Overall Averages (Median across all pages):"); + console.log(` LCP: ${formatTime(overallAverage.lcp)}`); + console.log(` FCP: ${formatTime(overallAverage.fcp)}`); + console.log(` CLS: ${overallAverage.cls.toFixed(3)}`); + console.log(` TTFB: ${formatTime(overallAverage.ttfb)}`); + console.log( + ` Total Size: ${formatBytes(overallAverage.totalBytes)}` + ); + console.log(` JS Size: ${formatBytes(overallAverage.jsBytes)}`); + console.log( + ` Total Requests: ${overallAverage.totalRequests.toFixed(0)}` + ); + + console.log("\n Page Rankings (by LCP):"); + const sortedResults = [...results].sort( + (a, b) => a.median.lcp - b.median.lcp + ); + sortedResults.forEach((result, index) => { + const rating = + result.median.lcp <= 2500 + ? "🟢" + : result.median.lcp <= 4000 + ? "🟔" + : "šŸ”“"; + console.log( + ` ${index + 1}. ${rating} ${result.page.padEnd(20)} ${formatTime(result.median.lcp)}` + ); + }); + + console.log("\n Optimization Opportunities:"); + + // Find pages with highest JS bytes + const highestJS = [...results].sort( + (a, b) => b.median.jsBytes - a.median.jsBytes + )[0]; + if (highestJS.median.jsBytes > 500 * 1024) { + // > 500KB + console.log( + ` šŸ“¦ ${highestJS.page}: High JS bundle (${formatBytes(highestJS.median.jsBytes)}) - consider code splitting` + ); + } + + // Find pages with slow LCP + const slowLCP = results.filter((r) => r.median.lcp > 2500); + if (slowLCP.length > 0) { + console.log( + ` 🐌 ${slowLCP.length} page(s) with LCP > 2.5s - optimize largest content element` + ); + } + + // Find pages with high CLS + const highCLS = results.filter((r) => r.median.cls > 0.1); + if (highCLS.length > 0) { + console.log( + ` šŸ“ ${highCLS.length} page(s) with CLS > 0.1 - add size attributes to images/elements` + ); + } + + console.log("\n"); +} + +async function main() { + console.log("šŸš€ Starting Performance Testing Suite...\n"); + console.log(`Target: ${BASE_URL}`); + console.log(`Pages to test: ${TEST_PAGES.length}`); + console.log(`Runs per page: ${RUNS_PER_PAGE} (+ ${WARMUP_RUNS} warmup)\n`); + + // Check if server is running + try { + const response = await fetch(BASE_URL); + if (!response.ok) { + throw new Error(`Server returned ${response.status}`); + } + console.log("āœ… Server is reachable\n"); + } catch (error) { + console.error(`āŒ Error: Cannot connect to ${BASE_URL}`); + console.error(" Make sure the dev server is running with: bun run dev"); + process.exit(1); + } + + const browser = await chromium.launch({ + headless: true, + args: ["--disable-dev-shm-usage"] + }); + const results: TestResult[] = []; + + for (const pageConfig of TEST_PAGES) { + try { + const result = await testPagePerformance(browser, pageConfig); + results.push(result); + } catch (error) { + console.error(`āŒ Error testing ${pageConfig.name}:`, error); + } + } + + await browser.close(); + + // Print results + printResults(results); + + // Save results to JSON file + const timestamp = new Date() + .toISOString() + .replace(/[:.]/g, "-") + .split("T")[0]; + const filename = `perf-results-${timestamp}.json`; + const output = { + timestamp: new Date().toISOString(), + baseUrl: BASE_URL, + runsPerPage: RUNS_PER_PAGE, + results + }; + writeFileSync(filename, JSON.stringify(output, null, 2)); + console.log(`šŸ“ Detailed results saved to: ${filename}\n`); +} + +main().catch(console.error); diff --git a/src/components/blog/MermaidRenderer.tsx b/src/components/blog/MermaidRenderer.tsx index 3155e0d..4cdfcb2 100644 --- a/src/components/blog/MermaidRenderer.tsx +++ b/src/components/blog/MermaidRenderer.tsx @@ -1,26 +1,30 @@ import { onMount } from "solid-js"; -import mermaid from "mermaid"; - -mermaid.initialize({ - startOnLoad: false, - theme: "dark", - securityLevel: "loose", - fontFamily: "monospace", - themeVariables: { - darkMode: true, - primaryColor: "#2c2f40", - primaryTextColor: "#b5c1f1", - primaryBorderColor: "#739df2", - lineColor: "#739df2", - secondaryColor: "#3e4255", - tertiaryColor: "#505469" - } -}); export default function MermaidRenderer() { - onMount(() => { + onMount(async () => { const mermaidPres = document.querySelectorAll('pre[data-type="mermaid"]'); + // Only load mermaid if there are diagrams to render + if (mermaidPres.length === 0) return; + + const mermaid = (await import("mermaid")).default; + + mermaid.initialize({ + startOnLoad: false, + theme: "dark", + securityLevel: "loose", + fontFamily: "monospace", + themeVariables: { + darkMode: true, + primaryColor: "#2c2f40", + primaryTextColor: "#b5c1f1", + primaryBorderColor: "#739df2", + lineColor: "#739df2", + secondaryColor: "#3e4255", + tertiaryColor: "#505469" + } + }); + mermaidPres.forEach(async (pre, index) => { const code = pre.querySelector("code"); if (!code) return; diff --git a/src/components/blog/PostBodyClient.tsx b/src/components/blog/PostBodyClient.tsx index 74de047..f8dbfdc 100644 --- a/src/components/blog/PostBodyClient.tsx +++ b/src/components/blog/PostBodyClient.tsx @@ -1,6 +1,7 @@ -import { createEffect, createSignal, onMount } from "solid-js"; +import { createEffect, createSignal, onMount, lazy } from "solid-js"; import type { HLJSApi } from "highlight.js"; -import MermaidRenderer from "./MermaidRenderer"; + +const MermaidRenderer = lazy(() => import("./MermaidRenderer")); export interface PostBodyClientProps { body: string; diff --git a/src/components/blog/TextEditor.tsx b/src/components/blog/TextEditor.tsx index 8c9a271..243f7c2 100644 --- a/src/components/blog/TextEditor.tsx +++ b/src/components/blog/TextEditor.tsx @@ -31,7 +31,7 @@ import { ConditionalInline } from "./extensions/ConditionalInline"; import TextAlign from "@tiptap/extension-text-align"; import Superscript from "@tiptap/extension-superscript"; import Subscript from "@tiptap/extension-subscript"; -import mermaid from "mermaid"; +import type { default as MermaidType } from "mermaid"; import css from "highlight.js/lib/languages/css"; import js from "highlight.js/lib/languages/javascript"; import ts from "highlight.js/lib/languages/typescript"; @@ -699,8 +699,14 @@ export default function TextEditor(props: TextEditorProps) { let bubbleMenuRef!: HTMLDivElement; let containerRef!: HTMLDivElement; - onMount(() => { - mermaid.initialize({ + const [mermaid, setMermaid] = createSignal(null); + + onMount(async () => { + // Lazy load mermaid only when editor is mounted + const mermaidModule = await import("mermaid"); + const mermaidInstance = mermaidModule.default; + + mermaidInstance.initialize({ startOnLoad: false, theme: "dark", securityLevel: "loose", @@ -715,6 +721,8 @@ export default function TextEditor(props: TextEditorProps) { tertiaryColor: "#505469" } }); + + setMermaid(() => mermaidInstance); }); const [showBubbleMenu, setShowBubbleMenu] = createSignal(false); @@ -1017,6 +1025,9 @@ export default function TextEditor(props: TextEditorProps) { }; const validateAndPreviewMermaid = async (code: string) => { + const mermaidInstance = mermaid(); + if (!mermaidInstance) return; // Wait for mermaid to load + if (!code.trim()) { setMermaidValidation({ valid: true, error: null }); setMermaidPreviewSvg(""); @@ -1024,10 +1035,10 @@ export default function TextEditor(props: TextEditorProps) { } try { - await mermaid.parse(code); + await mermaidInstance.parse(code); const id = `mermaid-preview-${Date.now()}`; - const { svg } = await mermaid.render(id, code); + const { svg } = await mermaidInstance.render(id, code); setMermaidValidation({ valid: true, error: null }); setMermaidPreviewSvg(svg); diff --git a/src/routes/blog/create/index.tsx b/src/routes/blog/create/index.tsx index a89c69d..9573fe1 100644 --- a/src/routes/blog/create/index.tsx +++ b/src/routes/blog/create/index.tsx @@ -1,11 +1,13 @@ -import { Show } from "solid-js"; +import { Show, lazy } from "solid-js"; import { query, redirect } from "@solidjs/router"; import { Title, Meta } from "@solidjs/meta"; import { createAsync } from "@solidjs/router"; import { getEvent } from "vinxi/http"; -import PostForm from "~/components/blog/PostForm"; +import { Spinner } from "~/components/Spinner"; import "../post.css"; +const PostForm = lazy(() => import("~/components/blog/PostForm")); + const getAuthState = query(async () => { "use server"; const { getPrivilegeLevel, getUserID } = await import("~/server/utils"); @@ -36,7 +38,7 @@ export default function CreatePost() { content="Create a new blog post with rich text editing, image uploads, and tag management." /> - + }> diff --git a/src/routes/blog/edit/[id].tsx b/src/routes/blog/edit/[id].tsx index 306f444..5b49195 100644 --- a/src/routes/blog/edit/[id].tsx +++ b/src/routes/blog/edit/[id].tsx @@ -1,11 +1,12 @@ -import { Show } from "solid-js"; +import { Show, lazy } from "solid-js"; import { useParams, query } from "@solidjs/router"; import { Title, Meta } from "@solidjs/meta"; import { createAsync } from "@solidjs/router"; import { getEvent } from "vinxi/http"; -import PostForm from "~/components/blog/PostForm"; import "../post.css"; +const PostForm = lazy(() => import("~/components/blog/PostForm")); + const getPostForEdit = query(async (id: string) => { "use server"; const { getPrivilegeLevel, getUserID, ConnectionFactory } =