Optimize Mermaid and blog component loading with lazy imports

This commit is contained in:
Michael Freno
2026-01-04 11:24:14 -05:00
parent 7e89e6dda2
commit 68073b4f17
9 changed files with 3300 additions and 30 deletions

1137
perf-results-2026-01-04.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

391
scripts/perf-compare.ts Normal file
View File

@@ -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 <baseline.json> <optimized.json>");
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();

586
scripts/perf-test.ts Normal file
View File

@@ -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<PerformanceMetrics> {
// 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<TestResult> {
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);

View File

@@ -1,7 +1,15 @@
import { onMount } from "solid-js";
import mermaid from "mermaid";
mermaid.initialize({
export default function MermaidRenderer() {
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",
@@ -15,11 +23,7 @@ mermaid.initialize({
secondaryColor: "#3e4255",
tertiaryColor: "#505469"
}
});
export default function MermaidRenderer() {
onMount(() => {
const mermaidPres = document.querySelectorAll('pre[data-type="mermaid"]');
});
mermaidPres.forEach(async (pre, index) => {
const code = pre.querySelector("code");

View File

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

View File

@@ -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<typeof MermaidType | null>(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);

View File

@@ -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."
/>
<Show when={authState()?.userID}>
<Show when={authState()?.userID} fallback={<Spinner />}>
<PostForm mode="create" userID={authState()!.userID} />
</Show>
</>

View File

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