Bundle analysis and monitoring

This commit is contained in:
adilallo
2025-10-07 17:08:57 -06:00
parent 2ed878af81
commit 104208c7df
15 changed files with 1554 additions and 46938 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+114
View File
@@ -0,0 +1,114 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const WEB_VITALS_DIR = path.join(process.cwd(), ".next", "web-vitals");
// Ensure web-vitals directory exists
if (!fs.existsSync(WEB_VITALS_DIR)) {
fs.mkdirSync(WEB_VITALS_DIR, { recursive: true });
}
export async function POST(request) {
try {
const { metric, data, url, userAgent, timestamp } = await request.json();
// Store the metric data
const vitalsData = {
metric,
data,
url,
userAgent,
timestamp: new Date(timestamp).toISOString(),
receivedAt: new Date().toISOString(),
};
// Save to file (in production, you would save to a database)
const filePath = path.join(WEB_VITALS_DIR, `${metric}.json`);
let existingData = [];
if (fs.existsSync(filePath)) {
try {
existingData = JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch (error) {
console.warn("Could not parse existing vitals data:", error.message);
}
}
existingData.push(vitalsData);
// Keep only last 100 entries per metric
if (existingData.length > 100) {
existingData = existingData.slice(-100);
}
fs.writeFileSync(filePath, JSON.stringify(existingData, null, 2));
// Log for monitoring
console.log(
`Web Vital received: ${metric} = ${data.value}ms (${data.rating})`
);
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error processing web vital:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
export async function GET() {
try {
const metrics = {};
if (fs.existsSync(WEB_VITALS_DIR)) {
const files = fs.readdirSync(WEB_VITALS_DIR);
files.forEach((file) => {
if (file.endsWith(".json")) {
const metric = file.replace(".json", "");
const data = JSON.parse(
fs.readFileSync(path.join(WEB_VITALS_DIR, file), "utf8")
);
if (data.length > 0) {
const values = data
.map((d) => d.data.value)
.filter((v) => v !== undefined);
const ratings = data
.map((d) => d.data.rating)
.filter((r) => r !== undefined);
metrics[metric] = {
count: data.length,
average:
values.length > 0
? Math.round(
values.reduce((a, b) => a + b, 0) / values.length
)
: 0,
min: values.length > 0 ? Math.min(...values) : 0,
max: values.length > 0 ? Math.max(...values) : 0,
goodCount: ratings.filter((r) => r === "good").length,
needsImprovementCount: ratings.filter(
(r) => r === "needs-improvement"
).length,
poorCount: ratings.filter((r) => r === "poor").length,
lastUpdated: data[data.length - 1]?.receivedAt,
};
}
}
});
}
return NextResponse.json({ metrics });
} catch (error) {
console.error("Error fetching web vitals:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
+245
View File
@@ -0,0 +1,245 @@
"use client";
import React, { useState, useEffect, memo } from "react";
const WebVitalsDashboard = memo(() => {
const [vitals, setVitals] = useState({
lcp: { value: 0, rating: "unknown" },
fid: { value: 0, rating: "unknown" },
cls: { value: 0, rating: "unknown" },
fcp: { value: 0, rating: "unknown" },
ttfb: { value: 0, rating: "unknown" },
});
const [metrics, setMetrics] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch Web Vitals data from API
const fetchVitals = async () => {
try {
const response = await fetch("/api/web-vitals");
const data = await response.json();
setMetrics(data.metrics || {});
} catch (error) {
console.error("Error fetching web vitals:", error);
} finally {
setLoading(false);
}
};
fetchVitals();
// Set up Web Vitals tracking
if (typeof window !== "undefined") {
import("web-vitals").then(
({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
// Track Largest Contentful Paint
getLCP((metric) => {
setVitals((prev) => ({
...prev,
lcp: {
value: Math.round(metric.value),
rating: metric.rating,
},
}));
});
// Track First Input Delay
getFID((metric) => {
setVitals((prev) => ({
...prev,
fid: {
value: Math.round(metric.value),
rating: metric.rating,
},
}));
});
// Track Cumulative Layout Shift
getCLS((metric) => {
setVitals((prev) => ({
...prev,
cls: {
value: Math.round(metric.value * 1000) / 1000,
rating: metric.rating,
},
}));
});
// Track First Contentful Paint
getFCP((metric) => {
setVitals((prev) => ({
...prev,
fcp: {
value: Math.round(metric.value),
rating: metric.rating,
},
}));
});
// Track Time to First Byte
getTTFB((metric) => {
setVitals((prev) => ({
...prev,
ttfb: {
value: Math.round(metric.value),
rating: metric.rating,
},
}));
});
}
);
}
}, []);
const getRatingColor = (rating) => {
switch (rating) {
case "good":
return "text-green-600 bg-green-50";
case "needs-improvement":
return "text-yellow-600 bg-yellow-50";
case "poor":
return "text-red-600 bg-red-50";
default:
return "text-gray-600 bg-gray-50";
}
};
const getRatingIcon = (rating) => {
switch (rating) {
case "good":
return "✅";
case "needs-improvement":
return "⚠️";
case "poor":
return "❌";
default:
return "❓";
}
};
const formatValue = (metric, value) => {
if (metric === "cls") {
return value.toFixed(3);
}
return `${value}ms`;
};
if (loading) {
return (
<div className="p-6 bg-white rounded-lg shadow-lg">
<div className="animate-pulse">
<div className="h-6 bg-gray-200 rounded w-1/3 mb-4"></div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="p-4 border rounded-lg">
<div className="h-4 bg-gray-200 rounded w-1/2 mb-2"></div>
<div className="h-3 bg-gray-200 rounded w-3/4"></div>
</div>
))}
</div>
</div>
</div>
);
}
return (
<div className="p-6 bg-white rounded-lg shadow-lg">
<h2 className="text-2xl font-bold mb-6 text-[var(--color-content-default-primary)]">
Web Vitals Dashboard
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
{Object.entries(vitals).map(([metric, data]) => (
<div
key={metric}
className={`p-4 border rounded-lg ${getRatingColor(data.rating)}`}
>
<div className="flex items-center justify-between mb-2">
<h3 className="font-semibold text-lg">{metric.toUpperCase()}</h3>
<span className="text-2xl">{getRatingIcon(data.rating)}</span>
</div>
<div className="text-sm">
<div className="font-medium">
Value: {formatValue(metric, data.value)}
</div>
<div className="capitalize">
Rating: {data.rating.replace("-", " ")}
</div>
</div>
</div>
))}
</div>
{/* Historical Metrics */}
{Object.keys(metrics).length > 0 && (
<div className="mb-6">
<h3 className="text-lg font-semibold mb-4 text-[var(--color-content-default-primary)]">
Historical Metrics
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(metrics).map(([metric, data]) => (
<div
key={metric}
className="p-4 border rounded-lg bg-[var(--color-surface-default-secondary)]"
>
<h4 className="font-semibold mb-2">{metric.toUpperCase()}</h4>
<div className="text-sm space-y-1">
<div>Count: {data.count}</div>
<div>Average: {formatValue(metric, data.average)}</div>
<div>
Range: {formatValue(metric, data.min)} -{" "}
{formatValue(metric, data.max)}
</div>
<div className="flex gap-2 text-xs">
<span className="text-green-600">
Good: {data.goodCount}
</span>
<span className="text-yellow-600">
Needs Improvement: {data.needsImprovementCount}
</span>
<span className="text-red-600">Poor: {data.poorCount}</span>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Performance Guidelines */}
<div className="p-4 bg-[var(--color-surface-default-secondary)] rounded-lg">
<h3 className="font-semibold mb-2 text-[var(--color-content-default-primary)]">
Performance Guidelines
</h3>
<ul className="text-sm space-y-1 text-[var(--color-content-default-secondary)]">
<li>
<strong>LCP:</strong> Good &lt; 2.5s, Needs Improvement 2.5-4s,
Poor &gt; 4s
</li>
<li>
<strong>FID:</strong> Good &lt; 100ms, Needs Improvement
100-300ms, Poor &gt; 300ms
</li>
<li>
<strong>CLS:</strong> Good &lt; 0.1, Needs Improvement 0.1-0.25,
Poor &gt; 0.25
</li>
<li>
<strong>FCP:</strong> Good &lt; 1.8s, Needs Improvement 1.8-3s,
Poor &gt; 3s
</li>
<li>
<strong>TTFB:</strong> Good &lt; 800ms, Needs Improvement
800-1800ms, Poor &gt; 1800ms
</li>
</ul>
</div>
</div>
);
});
WebVitalsDashboard.displayName = "WebVitalsDashboard";
export default WebVitalsDashboard;
+160
View File
@@ -0,0 +1,160 @@
import React from "react";
import WebVitalsDashboard from "../components/WebVitalsDashboard";
import Header from "../components/Header";
import Footer from "../components/Footer";
export default function MonitorPage() {
return (
<div className="min-h-screen bg-[var(--color-surface-default-primary)]">
<Header />
<main className="container mx-auto px-[var(--spacing-scale-024)] py-[var(--spacing-scale-032)]">
<div className="max-w-6xl mx-auto">
<div className="mb-[var(--spacing-scale-032)]">
<h1 className="text-4xl font-bold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-016)]">
Performance Monitoring
</h1>
<p className="text-[var(--font-size-body-large)] text-[var(--color-content-default-secondary)]">
Real-time monitoring of Core Web Vitals and performance metrics
for Community Rule 3.0
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-[var(--spacing-scale-032)] mb-[var(--spacing-scale-032)]">
<div className="p-[var(--spacing-scale-024)] bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-measures-radius-medium)]">
<h2 className="text-2xl font-semibold mb-[var(--spacing-scale-016)] text-[var(--color-content-default-primary)]">
Performance Targets
</h2>
<div className="space-y-[var(--spacing-scale-012)]">
<div className="flex justify-between items-center">
<span className="text-[var(--font-size-body-medium)]">
Load Time
</span>
<span className="font-semibold text-green-600">
&lt; 2 seconds
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--font-size-body-medium)]">
LCP
</span>
<span className="font-semibold text-green-600">
&lt; 2.5s
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--font-size-body-medium)]">
FID
</span>
<span className="font-semibold text-green-600">
&lt; 100ms
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--font-size-body-medium)]">
CLS
</span>
<span className="font-semibold text-green-600">&lt; 0.1</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[var(--font-size-body-medium)]">
Lighthouse Score
</span>
<span className="font-semibold text-green-600">&gt; 90</span>
</div>
</div>
</div>
<div className="p-[var(--spacing-scale-024)] bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-measures-radius-medium)]">
<h2 className="text-2xl font-semibold mb-[var(--spacing-scale-016)] text-[var(--color-content-default-primary)]">
Optimization Status
</h2>
<div className="space-y-[var(--spacing-scale-012)]">
<div className="flex items-center gap-[var(--spacing-scale-008)]">
<span className="text-green-600"></span>
<span className="text-[var(--font-size-body-medium)]">
Code Splitting & Dynamic Imports
</span>
</div>
<div className="flex items-center gap-[var(--spacing-scale-008)]">
<span className="text-green-600"></span>
<span className="text-[var(--font-size-body-medium)]">
React.memo Optimizations
</span>
</div>
<div className="flex items-center gap-[var(--spacing-scale-008)]">
<span className="text-green-600"></span>
<span className="text-[var(--font-size-body-medium)]">
Image Optimization
</span>
</div>
<div className="flex items-center gap-[var(--spacing-scale-008)]">
<span className="text-green-600"></span>
<span className="text-[var(--font-size-body-medium)]">
Font Preloading
</span>
</div>
<div className="flex items-center gap-[var(--spacing-scale-008)]">
<span className="text-green-600"></span>
<span className="text-[var(--font-size-body-medium)]">
Bundle Analysis
</span>
</div>
<div className="flex items-center gap-[var(--spacing-scale-008)]">
<span className="text-green-600"></span>
<span className="text-[var(--font-size-body-medium)]">
Error Boundaries
</span>
</div>
</div>
</div>
</div>
<WebVitalsDashboard />
<div className="mt-[var(--spacing-scale-032)] p-[var(--spacing-scale-024)] bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-measures-radius-medium)]">
<h2 className="text-2xl font-semibold mb-[var(--spacing-scale-016)] text-[var(--color-content-default-primary)]">
Monitoring Commands
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-[var(--spacing-scale-016)]">
<div>
<h3 className="font-semibold mb-[var(--spacing-scale-008)] text-[var(--color-content-default-primary)]">
Bundle Analysis
</h3>
<code className="block p-[var(--spacing-scale-008)] bg-[var(--color-surface-inverse-brand-primary)] text-[var(--color-content-inverse-primary)] rounded-[var(--radius-measures-radius-small)] text-sm">
npm run bundle:analyze
</code>
</div>
<div>
<h3 className="font-semibold mb-[var(--spacing-scale-008)] text-[var(--color-content-default-primary)]">
Performance Monitor
</h3>
<code className="block p-[var(--spacing-scale-008)] bg-[var(--color-surface-inverse-brand-primary)] text-[var(--color-content-inverse-primary)] rounded-[var(--radius-measures-radius-small)] text-sm">
npm run performance:monitor
</code>
</div>
<div>
<h3 className="font-semibold mb-[var(--spacing-scale-008)] text-[var(--color-content-default-primary)]">
Web Vitals Tracking
</h3>
<code className="block p-[var(--spacing-scale-008)] bg-[var(--color-surface-inverse-brand-primary)] text-[var(--color-content-inverse-primary)] rounded-[var(--radius-measures-radius-small)] text-sm">
npm run web-vitals:track
</code>
</div>
<div>
<h3 className="font-semibold mb-[var(--spacing-scale-008)] text-[var(--color-content-default-primary)]">
All Monitoring
</h3>
<code className="block p-[var(--spacing-scale-008)] bg-[var(--color-surface-inverse-brand-primary)] text-[var(--color-content-inverse-primary)] rounded-[var(--radius-measures-radius-small)] text-sm">
npm run monitor:all
</code>
</div>
</div>
</div>
</div>
</main>
<Footer />
</div>
);
}
+19
View File
@@ -58,6 +58,25 @@ const nextConfig = {
use: ["@svgr/webpack"],
});
// Bundle analysis - only in production builds
if (process.env.ANALYZE === "true" && !dev) {
try {
const BundleAnalyzerPlugin =
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: "static",
openAnalyzer: false,
reportFilename: isServer
? "../analyze/server.html"
: "../analyze/client.html",
})
);
} catch (error) {
console.warn("Bundle analyzer not available:", error.message);
}
}
// Production optimizations
if (!dev && !isServer) {
// Tree shaking optimization
+153 -1
View File
@@ -60,7 +60,9 @@
"tailwindcss": "^4.0.0",
"typescript": "^5.9.2",
"vitest": "^3.2.4",
"wait-on": "^8.0.4"
"wait-on": "^8.0.4",
"web-vitals": "^4.2.4",
"webpack-bundle-analyzer": "^4.10.1"
}
},
"node_modules/@adobe/css-tools": {
@@ -2211,6 +2213,16 @@
"node": ">=18"
}
},
"node_modules/@discoveryjs/json-ext": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz",
@@ -8023,6 +8035,19 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -10155,6 +10180,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==",
"dev": true,
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -12843,6 +12875,22 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/gzip-size": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
"integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"duplexer": "^0.1.2"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/has-bigints": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -18629,6 +18677,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/opener": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
"dev": true,
"license": "(WTFPL OR MIT)",
"bin": {
"opener": "bin/opener-bin.js"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -23086,6 +23144,13 @@
"makeerror": "1.0.12"
}
},
"node_modules/web-vitals": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/webdriver-bidi-protocol": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.2.11.tgz",
@@ -23103,6 +23168,93 @@
"node": ">=12"
}
},
"node_modules/webpack-bundle-analyzer": {
"version": "4.10.2",
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz",
"integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@discoveryjs/json-ext": "0.5.7",
"acorn": "^8.0.4",
"acorn-walk": "^8.0.0",
"commander": "^7.2.0",
"debounce": "^1.2.1",
"escape-string-regexp": "^4.0.0",
"gzip-size": "^6.0.0",
"html-escaper": "^2.0.2",
"opener": "^1.5.2",
"picocolors": "^1.0.0",
"sirv": "^2.0.3",
"ws": "^7.3.1"
},
"bin": {
"webpack-bundle-analyzer": "lib/bin/analyzer.js"
},
"engines": {
"node": ">= 10.13.0"
}
},
"node_modules/webpack-bundle-analyzer/node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
}
},
"node_modules/webpack-bundle-analyzer/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/webpack-bundle-analyzer/node_modules/sirv": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
"integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@polka/url": "^1.0.0-next.24",
"mrmime": "^2.0.0",
"totalist": "^3.0.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/webpack-bundle-analyzer/node_modules/ws": {
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+10 -2
View File
@@ -33,7 +33,13 @@
"seed-snapshots:local": "PLAYWRIGHT_UPDATE_SNAPSHOTS=1 npx playwright test tests/e2e/visual-regression.spec.ts --project=chromium",
"visual:test": "npx playwright test tests/e2e/visual-regression.spec.ts",
"visual:update": "PLAYWRIGHT_UPDATE_SNAPSHOTS=1 npx playwright test tests/e2e/visual-regression.spec.ts",
"visual:ui": "npx playwright test tests/e2e/visual-regression.spec.ts --ui"
"visual:ui": "npx playwright test tests/e2e/visual-regression.spec.ts --ui",
"analyze": "npm run analyze:browser && npm run analyze:server",
"analyze:server": "ANALYZE=true npm run build",
"analyze:browser": "BUNDLE_ANALYZE=true npm run build",
"bundle:analyze": "node scripts/bundle-analyzer.js",
"web-vitals:track": "node scripts/web-vitals-tracker.js",
"monitor:all": "npm run bundle:analyze && npm run performance:monitor && npm run web-vitals:track"
},
"dependencies": {
"@mdx-js/loader": "^3.1.1",
@@ -87,6 +93,8 @@
"tailwindcss": "^4.0.0",
"typescript": "^5.9.2",
"vitest": "^3.2.4",
"wait-on": "^8.0.4"
"wait-on": "^8.0.4",
"web-vitals": "^4.2.4",
"webpack-bundle-analyzer": "^4.10.1"
}
}
+253
View File
@@ -0,0 +1,253 @@
#!/usr/bin/env node
/**
* Bundle Analysis Script
* Analyzes webpack bundles and provides detailed performance insights
*/
const { execSync } = require("child_process");
const fs = require("fs");
const path = require("path");
const BUNDLE_ANALYSIS_DIR = path.join(__dirname, "..", ".next", "analyze");
const PERFORMANCE_BUDGETS = require("../performance-budgets.json");
class BundleAnalyzer {
constructor() {
this.results = {
timestamp: new Date().toISOString(),
bundles: {},
recommendations: [],
budgetViolations: [],
};
}
/**
* Run bundle analysis using build output
*/
async analyzeBundles() {
console.log("🔍 Starting bundle analysis...");
try {
// Ensure analyze directory exists
if (!fs.existsSync(BUNDLE_ANALYSIS_DIR)) {
fs.mkdirSync(BUNDLE_ANALYSIS_DIR, { recursive: true });
}
// Build the project first
console.log("🏗️ Building project...");
execSync("npm run build", { stdio: "inherit" });
// Parse bundle stats from build output
await this.parseBundleStats();
// Check performance budgets
this.checkPerformanceBudgets();
// Generate recommendations
this.generateRecommendations();
// Save results
this.saveResults();
console.log("✅ Bundle analysis complete!");
console.log(`📁 Results saved to: ${BUNDLE_ANALYSIS_DIR}`);
} catch (error) {
console.error("❌ Bundle analysis failed:", error.message);
process.exit(1);
}
}
/**
* Parse bundle statistics from build output
*/
async parseBundleStats() {
const staticPath = path.join(__dirname, "..", ".next", "static");
const chunksPath = path.join(staticPath, "chunks");
// Analyze static assets
if (fs.existsSync(staticPath)) {
this.analyzeDirectory(staticPath, "static");
}
// Analyze chunks
if (fs.existsSync(chunksPath)) {
this.analyzeDirectory(chunksPath, "chunks");
}
// Analyze pages
const pagesPath = path.join(__dirname, "..", ".next", "server", "pages");
if (fs.existsSync(pagesPath)) {
this.analyzeDirectory(pagesPath, "pages");
}
}
/**
* Analyze directory for bundle sizes
*/
analyzeDirectory(dirPath, type) {
const files = fs.readdirSync(dirPath);
files.forEach((file) => {
const filePath = path.join(dirPath, file);
const stats = fs.statSync(filePath);
if (stats.isFile() && (file.endsWith(".js") || file.endsWith(".css"))) {
const key = `${type}/${file}`;
this.results.bundles[key] = {
size: stats.size,
sizeKB: Math.round(stats.size / 1024),
lastModified: stats.mtime,
type: file.endsWith(".css") ? "css" : "js",
};
} else if (stats.isDirectory()) {
this.analyzeDirectory(filePath, `${type}/${file}`);
}
});
}
/**
* Check against performance budgets
*/
checkPerformanceBudgets() {
const budgets = PERFORMANCE_BUDGETS.budgets || [];
Object.entries(this.results.bundles).forEach(([filename, bundle]) => {
const budget = budgets.find(
(b) => filename.includes(b.name) || b.name === "all"
);
if (budget) {
if (bundle.sizeKB > budget.maxSizeKB) {
this.results.budgetViolations.push({
file: filename,
currentSize: bundle.sizeKB,
maxSize: budget.maxSizeKB,
overage: bundle.sizeKB - budget.maxSizeKB,
severity:
bundle.sizeKB > budget.maxSizeKB * 1.2 ? "high" : "medium",
});
}
} else {
// Default budget check for large files
if (bundle.sizeKB > 500) {
this.results.budgetViolations.push({
file: filename,
currentSize: bundle.sizeKB,
maxSize: 500,
overage: bundle.sizeKB - 500,
severity: bundle.sizeKB > 600 ? "high" : "medium",
});
}
}
});
}
/**
* Generate optimization recommendations
*/
generateRecommendations() {
const recommendations = [];
// Check for large bundles
Object.entries(this.results.bundles).forEach(([filename, bundle]) => {
if (bundle.sizeKB > 500) {
recommendations.push({
type: "large-bundle",
file: filename,
size: bundle.sizeKB,
suggestion:
"Consider code splitting or dynamic imports for this bundle",
});
}
});
// Check for budget violations
if (this.results.budgetViolations.length > 0) {
recommendations.push({
type: "budget-violation",
count: this.results.budgetViolations.length,
suggestion:
"Review and optimize bundles that exceed performance budgets",
});
}
// General recommendations
const totalSize = Object.values(this.results.bundles).reduce(
(sum, bundle) => sum + bundle.sizeKB,
0
);
if (totalSize > 2000) {
recommendations.push({
type: "total-size",
size: totalSize,
suggestion: "Consider implementing more aggressive code splitting",
});
}
this.results.recommendations = recommendations;
}
/**
* Save analysis results
*/
saveResults() {
// Ensure directory exists
if (!fs.existsSync(BUNDLE_ANALYSIS_DIR)) {
fs.mkdirSync(BUNDLE_ANALYSIS_DIR, { recursive: true });
}
const resultsPath = path.join(BUNDLE_ANALYSIS_DIR, "bundle-analysis.json");
fs.writeFileSync(resultsPath, JSON.stringify(this.results, null, 2));
// Generate markdown report
this.generateMarkdownReport();
}
/**
* Generate markdown report
*/
generateMarkdownReport() {
const reportPath = path.join(BUNDLE_ANALYSIS_DIR, "bundle-report.md");
let report = `# Bundle Analysis Report\n\n`;
report += `**Generated:** ${this.results.timestamp}\n\n`;
// Bundle sizes
report += `## Bundle Sizes\n\n`;
report += `| File | Size (KB) | Status |\n`;
report += `|------|-----------|--------|\n`;
Object.entries(this.results.bundles).forEach(([filename, bundle]) => {
const status = bundle.sizeKB > 500 ? "⚠️ Large" : "✅ Good";
report += `| ${filename} | ${bundle.sizeKB} | ${status} |\n`;
});
// Budget violations
if (this.results.budgetViolations.length > 0) {
report += `\n## Budget Violations\n\n`;
this.results.budgetViolations.forEach((violation) => {
report += `- **${violation.file}**: ${violation.currentSize}KB (exceeds ${violation.maxSize}KB by ${violation.overage}KB)\n`;
});
}
// Recommendations
if (this.results.recommendations.length > 0) {
report += `\n## Recommendations\n\n`;
this.results.recommendations.forEach((rec) => {
report += `- ${rec.suggestion}\n`;
});
}
fs.writeFileSync(reportPath, report);
}
}
// Run analysis if called directly
if (require.main === module) {
const analyzer = new BundleAnalyzer();
analyzer.analyzeBundles().catch(console.error);
}
module.exports = BundleAnalyzer;
+265 -354
View File
@@ -2,386 +2,297 @@
/**
* Performance Monitoring Script
*
* This script provides comprehensive performance monitoring capabilities
* for the Community Rule application.
* Monitors Core Web Vitals and performance metrics
*/
const { spawn } = require("child_process");
const { execSync } = require("child_process");
const fs = require("fs");
const path = require("path");
// Performance budgets
const PERFORMANCE_BUDGETS = {
page_load_time: 3000,
first_contentful_paint: 2000,
largest_contentful_paint: 2500,
first_input_delay: 100,
dns_lookup: 100,
tcp_connection: 200,
ttfb: 600,
dom_content_loaded: 1500,
full_load: 3000,
component_render_time: 500,
interaction_time: 100,
scroll_performance: 50,
network_request_duration: 1000,
memory_usage_mb: 50,
};
const PERFORMANCE_BUDGETS = require("../performance-budgets.json");
const MONITORING_DIR = path.join(__dirname, "..", ".next", "monitoring");
// Baseline metrics for regression detection
const BASELINE_METRICS = {
page_load_time: 2000,
first_contentful_paint: 1500,
largest_contentful_paint: 2000,
first_input_delay: 50,
dns_lookup: 50,
tcp_connection: 100,
ttfb: 400,
dom_content_loaded: 1000,
full_load: 2000,
component_render_time: 300,
interaction_time: 50,
scroll_performance: 30,
network_request_duration: 500,
memory_usage_mb: 30,
};
class PerformanceMonitorScript {
class PerformanceMonitor {
constructor() {
this.metrics = new Map();
this.regressions = [];
this.warnings = [];
}
/**
* Run Lighthouse CI performance tests
*/
async runLighthouseCI() {
console.log("🚀 Running Lighthouse CI performance tests...");
return new Promise((resolve, reject) => {
const lhci = spawn("npx", ["lhci", "autorun"], {
stdio: "pipe",
shell: true,
});
let output = "";
let errorOutput = "";
lhci.stdout.on("data", (data) => {
output += data.toString();
console.log(data.toString());
});
lhci.stderr.on("data", (data) => {
errorOutput += data.toString();
console.error(data.toString());
});
lhci.on("close", (code) => {
if (code === 0) {
console.log("✅ Lighthouse CI tests completed successfully");
this.analyzeLighthouseResults(output);
resolve(output);
} else {
console.error("❌ Lighthouse CI tests failed");
reject(
new Error(`Lighthouse CI failed with code ${code}: ${errorOutput}`),
);
}
});
});
}
/**
* Run Playwright performance tests
*/
async runPlaywrightPerformanceTests() {
console.log("🎭 Running Playwright performance tests...");
return new Promise((resolve, reject) => {
const playwright = spawn(
"npx",
[
"playwright",
"test",
"tests/e2e/performance.spec.ts",
"--reporter=json",
],
{
stdio: "pipe",
shell: true,
},
);
let output = "";
let errorOutput = "";
playwright.stdout.on("data", (data) => {
output += data.toString();
});
playwright.stderr.on("data", (data) => {
errorOutput += data.toString();
});
playwright.on("close", (code) => {
if (code === 0) {
console.log("✅ Playwright performance tests completed successfully");
this.analyzePlaywrightResults(output);
resolve(output);
} else {
console.error("❌ Playwright performance tests failed");
reject(
new Error(
`Playwright tests failed with code ${code}: ${errorOutput}`,
),
);
}
});
});
}
/**
* Analyze Lighthouse CI results
*/
analyzeLighthouseResults(output) {
console.log("📊 Analyzing Lighthouse CI results...");
// Parse Lighthouse results
const lines = output.split("\n");
let currentMetric = null;
for (const line of lines) {
if (line.includes("Performance")) {
const scoreMatch = line.match(/(\d+)/);
if (scoreMatch) {
const score = parseInt(scoreMatch[1]);
this.recordMetric("lighthouse_performance_score", score);
if (score < 90) {
this.warnings.push(
`Performance score below threshold: ${score}/100`,
);
}
}
}
if (line.includes("First Contentful Paint")) {
const timeMatch = line.match(/(\d+(?:\.\d+)?)\s*ms/);
if (timeMatch) {
const time = parseFloat(timeMatch[1]);
this.recordMetric("first_contentful_paint", time);
if (time > PERFORMANCE_BUDGETS.first_contentful_paint) {
this.warnings.push(
`First Contentful Paint exceeded budget: ${time}ms`,
);
}
}
}
if (line.includes("Largest Contentful Paint")) {
const timeMatch = line.match(/(\d+(?:\.\d+)?)\s*ms/);
if (timeMatch) {
const time = parseFloat(timeMatch[1]);
this.recordMetric("largest_contentful_paint", time);
if (time > PERFORMANCE_BUDGETS.largest_contentful_paint) {
this.warnings.push(
`Largest Contentful Paint exceeded budget: ${time}ms`,
);
}
}
}
if (line.includes("Total Blocking Time")) {
const timeMatch = line.match(/(\d+(?:\.\d+)?)\s*ms/);
if (timeMatch) {
const time = parseFloat(timeMatch[1]);
this.recordMetric("total_blocking_time", time);
if (time > 300) {
this.warnings.push(
`Total Blocking Time exceeded budget: ${time}ms`,
);
}
}
}
if (line.includes("Cumulative Layout Shift")) {
const shiftMatch = line.match(/(\d+(?:\.\d+)?)/);
if (shiftMatch) {
const shift = parseFloat(shiftMatch[1]);
this.recordMetric("cumulative_layout_shift", shift);
if (shift > 0.1) {
this.warnings.push(
`Cumulative Layout Shift exceeded budget: ${shift}`,
);
}
}
}
}
}
/**
* Analyze Playwright test results
*/
analyzePlaywrightResults(output) {
console.log("📊 Analyzing Playwright test results...");
try {
const results = JSON.parse(output);
for (const result of results) {
if (result.status === "failed") {
this.warnings.push(`Test failed: ${result.title}`);
}
}
} catch (error) {
console.warn("Could not parse Playwright results as JSON");
}
}
/**
* Record a performance metric
*/
recordMetric(name, value) {
if (!this.metrics.has(name)) {
this.metrics.set(name, []);
}
this.metrics.get(name).push({
value,
timestamp: Date.now(),
});
// Check against baseline for regression detection
const baseline = BASELINE_METRICS[name];
if (baseline) {
const regressionThreshold = baseline * 1.2; // 20% regression threshold
if (value > regressionThreshold) {
this.regressions.push({
metric: name,
current: value,
baseline,
regression: (((value - baseline) / baseline) * 100).toFixed(1) + "%",
});
}
}
}
/**
* Generate performance report
*/
generateReport() {
console.log("\n📈 Performance Monitoring Report");
console.log("================================\n");
// Summary
console.log("📊 Summary:");
console.log(`- Total metrics recorded: ${this.metrics.size}`);
console.log(
`- Performance regressions detected: ${this.regressions.length}`,
);
console.log(`- Warnings: ${this.warnings.length}\n`);
// Performance regressions
if (this.regressions.length > 0) {
console.log("🚨 Performance Regressions:");
for (const regression of this.regressions) {
console.log(
` - ${regression.metric}: ${regression.current} (baseline: ${regression.baseline}, regression: ${regression.regression})`,
);
}
console.log("");
}
// Warnings
if (this.warnings.length > 0) {
console.log("⚠️ Warnings:");
for (const warning of this.warnings) {
console.log(` - ${warning}`);
}
console.log("");
}
// Metrics summary
console.log("📋 Metrics Summary:");
for (const [name, values] of this.metrics) {
const latest = values[values.length - 1];
const average =
values.reduce((sum, v) => sum + v.value, 0) / values.length;
const budget = PERFORMANCE_BUDGETS[name];
console.log(` - ${name}:`);
console.log(` Latest: ${latest.value}`);
console.log(` Average: ${average.toFixed(2)}`);
if (budget) {
const status = latest.value <= budget ? "✅" : "❌";
console.log(` Budget: ${budget} ${status}`);
}
}
// Save report to file
const report = {
this.metrics = {
timestamp: new Date().toISOString(),
summary: {
totalMetrics: this.metrics.size,
regressions: this.regressions.length,
warnings: this.warnings.length,
},
regressions: this.regressions,
warnings: this.warnings,
metrics: Object.fromEntries(this.metrics),
coreWebVitals: {},
bundleMetrics: {},
recommendations: [],
};
const reportPath = path.join(__dirname, "../performance-report.json");
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`\n📄 Report saved to: ${reportPath}`);
return report;
}
/**
* Run all performance monitoring
* Run comprehensive performance monitoring
*/
async run() {
console.log("🔍 Starting Performance Monitoring...\n");
async monitorPerformance() {
console.log("📊 Starting performance monitoring...");
try {
// Run Lighthouse CI tests
// Ensure monitoring directory exists
if (!fs.existsSync(MONITORING_DIR)) {
fs.mkdirSync(MONITORING_DIR, { recursive: true });
}
// Run Lighthouse CI for Core Web Vitals
await this.runLighthouseCI();
// Run Playwright performance tests
await this.runPlaywrightPerformanceTests();
// Analyze bundle performance
await this.analyzeBundlePerformance();
// Generate and display report
const report = this.generateReport();
// Check performance budgets
this.checkPerformanceBudgets();
// Exit with appropriate code
if (this.regressions.length > 0) {
console.log("❌ Performance regressions detected!");
process.exit(1);
} else if (this.warnings.length > 0) {
console.log("⚠️ Performance warnings detected.");
process.exit(0);
} else {
console.log("✅ All performance checks passed!");
process.exit(0);
}
// Generate performance report
this.generatePerformanceReport();
console.log("✅ Performance monitoring complete!");
console.log(`📁 Results saved to: ${MONITORING_DIR}`);
} catch (error) {
console.error("❌ Performance monitoring failed:", error.message);
process.exit(1);
}
}
/**
* Run Lighthouse CI for Core Web Vitals
*/
async runLighthouseCI() {
console.log("🔍 Running Lighthouse CI...");
try {
// Check if server is running
const { execSync } = require("child_process");
try {
execSync("curl -s http://localhost:3000 > /dev/null", {
stdio: "pipe",
});
} catch (error) {
console.warn(
"⚠️ Development server not running, skipping Lighthouse CI..."
);
return;
}
// Run Lighthouse CI with performance focus
execSync("npx lhci autorun --collect.url=http://localhost:3000", {
stdio: "inherit",
cwd: path.join(__dirname, ".."),
});
// Parse Lighthouse results
await this.parseLighthouseResults();
} catch (error) {
console.warn("⚠️ Lighthouse CI failed, continuing with other metrics...");
}
}
/**
* Parse Lighthouse CI results
*/
async parseLighthouseResults() {
const lhciResultsPath = path.join(__dirname, "..", ".lighthouseci");
if (fs.existsSync(lhciResultsPath)) {
const files = fs.readdirSync(lhciResultsPath);
const resultFile = files.find((f) => f.endsWith(".json"));
if (resultFile) {
const results = JSON.parse(
fs.readFileSync(path.join(lhciResultsPath, resultFile), "utf8")
);
if (results.lhr && results.lhr.audits) {
this.metrics.coreWebVitals = {
lcp: this.getAuditScore(
results.lhr.audits,
"largest-contentful-paint"
),
fid: this.getAuditScore(results.lhr.audits, "max-potential-fid"),
cls: this.getAuditScore(
results.lhr.audits,
"cumulative-layout-shift"
),
fcp: this.getAuditScore(
results.lhr.audits,
"first-contentful-paint"
),
tti: this.getAuditScore(results.lhr.audits, "interactive"),
performance: results.lhr.categories.performance?.score * 100 || 0,
};
}
}
}
}
/**
* Get audit score from Lighthouse results
*/
getAuditScore(audits, auditId) {
const audit = audits[auditId];
if (!audit) return null;
return {
score: audit.score * 100,
value: audit.numericValue,
displayValue: audit.displayValue,
};
}
/**
* Analyze bundle performance
*/
async analyzeBundlePerformance() {
console.log("📦 Analyzing bundle performance...");
const bundleStatsPath = path.join(
__dirname,
"..",
".next",
"static",
"chunks"
);
if (fs.existsSync(bundleStatsPath)) {
const files = fs.readdirSync(bundleStatsPath);
let totalSize = 0;
let jsFiles = 0;
files.forEach((file) => {
if (file.endsWith(".js")) {
const filePath = path.join(bundleStatsPath, file);
const stats = fs.statSync(filePath);
totalSize += stats.size;
jsFiles++;
}
});
this.metrics.bundleMetrics = {
totalSizeKB: Math.round(totalSize / 1024),
totalSizeMB: Math.round((totalSize / (1024 * 1024)) * 100) / 100,
fileCount: jsFiles,
averageSizeKB: Math.round(totalSize / jsFiles / 1024),
};
}
}
/**
* Check performance budgets
*/
checkPerformanceBudgets() {
const budgets = PERFORMANCE_BUDGETS.budgets;
const violations = [];
// Check Core Web Vitals
if (this.metrics.coreWebVitals.lcp) {
const lcpValue = this.metrics.coreWebVitals.lcp.value;
const lcpBudget = budgets.find((b) => b.name === "lcp")?.maxValue;
if (lcpBudget && lcpValue > lcpBudget) {
violations.push({
metric: "LCP",
current: lcpValue,
budget: lcpBudget,
severity: lcpValue > lcpBudget * 1.5 ? "high" : "medium",
});
}
}
// Check bundle size
if (this.metrics.bundleMetrics.totalSizeKB > 2000) {
violations.push({
metric: "Bundle Size",
current: this.metrics.bundleMetrics.totalSizeKB,
budget: 2000,
severity: "medium",
});
}
this.metrics.budgetViolations = violations;
}
/**
* Generate performance report
*/
generatePerformanceReport() {
const reportPath = path.join(MONITORING_DIR, "performance-report.json");
fs.writeFileSync(reportPath, JSON.stringify(this.metrics, null, 2));
// Generate markdown report
this.generateMarkdownReport();
}
/**
* Generate markdown performance report
*/
generateMarkdownReport() {
const reportPath = path.join(MONITORING_DIR, "performance-report.md");
let report = `# Performance Monitoring Report\n\n`;
report += `**Generated:** ${this.metrics.timestamp}\n\n`;
// Core Web Vitals
if (Object.keys(this.metrics.coreWebVitals).length > 0) {
report += `## Core Web Vitals\n\n`;
report += `| Metric | Score | Value | Status |\n`;
report += `|--------|-------|-------|--------|\n`;
Object.entries(this.metrics.coreWebVitals).forEach(([metric, data]) => {
if (data && typeof data === "object" && data.score !== undefined) {
const status = this.getMetricStatus(metric, data.score);
report += `| ${metric.toUpperCase()} | ${data.score} | ${
data.displayValue || "N/A"
} | ${status} |\n`;
}
});
}
// Bundle Metrics
if (Object.keys(this.metrics.bundleMetrics).length > 0) {
report += `\n## Bundle Metrics\n\n`;
report += `- **Total Size:** ${this.metrics.bundleMetrics.totalSizeMB}MB (${this.metrics.bundleMetrics.totalSizeKB}KB)\n`;
report += `- **File Count:** ${this.metrics.bundleMetrics.fileCount}\n`;
report += `- **Average Size:** ${this.metrics.bundleMetrics.averageSizeKB}KB per file\n`;
}
// Budget Violations
if (
this.metrics.budgetViolations &&
this.metrics.budgetViolations.length > 0
) {
report += `\n## Budget Violations\n\n`;
this.metrics.budgetViolations.forEach((violation) => {
report += `- **${violation.metric}**: ${violation.current} (exceeds ${
violation.budget
}) - ${violation.severity.toUpperCase()}\n`;
});
}
// Recommendations
report += `\n## Recommendations\n\n`;
report += `- Monitor Core Web Vitals regularly\n`;
report += `- Implement code splitting for large bundles\n`;
report += `- Use dynamic imports for non-critical components\n`;
report += `- Optimize images and fonts\n`;
report += `- Enable compression and caching\n`;
fs.writeFileSync(reportPath, report);
}
/**
* Get status emoji for metric score
*/
getMetricStatus(metric, score) {
if (score >= 90) return "✅ Good";
if (score >= 50) return "⚠️ Needs Improvement";
return "❌ Poor";
}
}
// Run the performance monitor if this script is executed directly
// Run monitoring if called directly
if (require.main === module) {
const monitor = new PerformanceMonitorScript();
monitor.run();
const monitor = new PerformanceMonitor();
monitor.monitorPerformance().catch(console.error);
}
module.exports = PerformanceMonitorScript;
module.exports = PerformanceMonitor;
+335
View File
@@ -0,0 +1,335 @@
#!/usr/bin/env node
/**
* Web Vitals Tracker
* Real-time monitoring of Core Web Vitals in production
*/
const fs = require("fs");
const path = require("path");
const WEB_VITALS_DIR = path.join(__dirname, "..", ".next", "web-vitals");
class WebVitalsTracker {
constructor() {
this.metrics = {
timestamp: new Date().toISOString(),
vitals: {
lcp: [],
fid: [],
cls: [],
fcp: [],
ttfb: [],
},
summary: {},
};
}
/**
* Track Web Vitals from client-side
*/
trackWebVitals() {
const trackingCode = `
// Web Vitals Tracking Script
(function() {
// Import web-vitals library
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
const vitals = {};
// Track Largest Contentful Paint
getLCP((metric) => {
vitals.lcp = {
value: metric.value,
rating: metric.rating,
delta: metric.delta,
timestamp: Date.now()
};
sendVitals('lcp', vitals.lcp);
});
// Track First Input Delay
getFID((metric) => {
vitals.fid = {
value: metric.value,
rating: metric.rating,
delta: metric.delta,
timestamp: Date.now()
};
sendVitals('fid', vitals.fid);
});
// Track Cumulative Layout Shift
getCLS((metric) => {
vitals.cls = {
value: metric.value,
rating: metric.rating,
delta: metric.delta,
timestamp: Date.now()
};
sendVitals('cls', vitals.cls);
});
// Track First Contentful Paint
getFCP((metric) => {
vitals.fcp = {
value: metric.value,
rating: metric.rating,
delta: metric.delta,
timestamp: Date.now()
};
sendVitals('fcp', vitals.fcp);
});
// Track Time to First Byte
getTTFB((metric) => {
vitals.ttfb = {
value: metric.value,
rating: metric.rating,
delta: metric.delta,
timestamp: Date.now()
};
sendVitals('ttfb', vitals.ttfb);
});
});
// Send vitals to server
function sendVitals(metric, data) {
if (typeof window !== 'undefined' && window.fetch) {
fetch('/api/web-vitals', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
metric,
data,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
})
}).catch(console.error);
}
}
})();
`;
return trackingCode;
}
/**
* Create API endpoint for receiving Web Vitals
*/
createAPIEndpoint() {
const apiCode = `
// API endpoint for Web Vitals tracking
export default function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { metric, data, url, userAgent, timestamp } = req.body;
// Store the metric data
const vitalsData = {
metric,
data,
url,
userAgent,
timestamp: new Date(timestamp).toISOString()
};
// In production, you would save this to a database
// For now, we'll log it
console.log('Web Vital received:', vitalsData);
res.status(200).json({ success: true });
} catch (error) {
console.error('Error processing web vital:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
`;
return apiCode;
}
/**
* Generate Web Vitals dashboard
*/
generateDashboard() {
const dashboardCode = `
import React, { useState, useEffect } from 'react';
const WebVitalsDashboard = () => {
const [vitals, setVitals] = useState({
lcp: { value: 0, rating: 'unknown' },
fid: { value: 0, rating: 'unknown' },
cls: { value: 0, rating: 'unknown' },
fcp: { value: 0, rating: 'unknown' },
ttfb: { value: 0, rating: 'unknown' }
});
useEffect(() => {
// In a real implementation, you would fetch from your database
// For now, we'll use localStorage for demo purposes
const storedVitals = localStorage.getItem('web-vitals');
if (storedVitals) {
setVitals(JSON.parse(storedVitals));
}
}, []);
const getRatingColor = (rating) => {
switch (rating) {
case 'good': return 'text-green-600';
case 'needs-improvement': return 'text-yellow-600';
case 'poor': return 'text-red-600';
default: return 'text-gray-600';
}
};
const getRatingIcon = (rating) => {
switch (rating) {
case 'good': return '✅';
case 'needs-improvement': return '⚠️';
case 'poor': return '❌';
default: return '❓';
}
};
return (
<div className="p-6 bg-white rounded-lg shadow-lg">
<h2 className="text-2xl font-bold mb-6">Web Vitals Dashboard</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(vitals).map(([metric, data]) => (
<div key={metric} className="p-4 border rounded-lg">
<div className="flex items-center justify-between mb-2">
<h3 className="font-semibold text-lg">{metric.toUpperCase()}</h3>
<span className="text-2xl">{getRatingIcon(data.rating)}</span>
</div>
<div className="text-sm text-gray-600">
<div>Value: {data.value}ms</div>
<div className={\`font-medium \${getRatingColor(data.rating)}\`}>
Rating: {data.rating.replace('-', ' ')}
</div>
</div>
</div>
))}
</div>
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
<h3 className="font-semibold mb-2">Performance Guidelines</h3>
<ul className="text-sm space-y-1">
<li>• <strong>LCP:</strong> Good &lt; 2.5s, Needs Improvement 2.5-4s, Poor &gt; 4s</li>
<li>• <strong>FID:</strong> Good &lt; 100ms, Needs Improvement 100-300ms, Poor &gt; 300ms</li>
<li>• <strong>CLS:</strong> Good &lt; 0.1, Needs Improvement 0.1-0.25, Poor &gt; 0.25</li>
<li>• <strong>FCP:</strong> Good &lt; 1.8s, Needs Improvement 1.8-3s, Poor &gt; 3s</li>
<li>• <strong>TTFB:</strong> Good &lt; 800ms, Needs Improvement 800-1800ms, Poor &gt; 1800ms</li>
</ul>
</div>
</div>
);
};
export default WebVitalsDashboard;
`;
return dashboardCode;
}
/**
* Save Web Vitals data
*/
saveVitalsData(metric, data) {
if (!fs.existsSync(WEB_VITALS_DIR)) {
fs.mkdirSync(WEB_VITALS_DIR, { recursive: true });
}
const filePath = path.join(WEB_VITALS_DIR, `${metric}.json`);
let existingData = [];
if (fs.existsSync(filePath)) {
try {
existingData = JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch (error) {
console.warn("Could not parse existing vitals data:", error.message);
}
}
existingData.push({
...data,
timestamp: new Date().toISOString(),
});
// Keep only last 100 entries
if (existingData.length > 100) {
existingData = existingData.slice(-100);
}
fs.writeFileSync(filePath, JSON.stringify(existingData, null, 2));
}
/**
* Generate Web Vitals report
*/
generateReport() {
if (!fs.existsSync(WEB_VITALS_DIR)) {
console.log("No Web Vitals data found");
return;
}
const files = fs.readdirSync(WEB_VITALS_DIR);
const report = {
timestamp: new Date().toISOString(),
metrics: {},
};
files.forEach((file) => {
if (file.endsWith(".json")) {
const metric = file.replace(".json", "");
const data = JSON.parse(
fs.readFileSync(path.join(WEB_VITALS_DIR, file), "utf8")
);
if (data.length > 0) {
const values = data
.map((d) => d.value)
.filter((v) => v !== undefined);
const ratings = data
.map((d) => d.rating)
.filter((r) => r !== undefined);
report.metrics[metric] = {
count: data.length,
average:
values.length > 0
? Math.round(values.reduce((a, b) => a + b, 0) / values.length)
: 0,
min: values.length > 0 ? Math.min(...values) : 0,
max: values.length > 0 ? Math.max(...values) : 0,
goodCount: ratings.filter((r) => r === "good").length,
needsImprovementCount: ratings.filter(
(r) => r === "needs-improvement"
).length,
poorCount: ratings.filter((r) => r === "poor").length,
};
}
}
});
const reportPath = path.join(WEB_VITALS_DIR, "report.json");
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log("📊 Web Vitals report generated:", reportPath);
return report;
}
}
// Run if called directly
if (require.main === module) {
const tracker = new WebVitalsTracker();
tracker.generateReport();
}
module.exports = WebVitalsTracker;