AI-Powered Performance Profiling and Optimization Recommendations
AI-Powered Performance Profiling and Optimization Recommendations
Real-World Problem Context
A React e-commerce application has degraded from a 2.1s Largest Contentful Paint (LCP) to 4.8s over six months of feature development. The team has 180 routes, 450 components, and a 2.8MB JavaScript bundle (gzipped). Performance profiling sessions reveal multiple issues: unnecessary re-renders in the product grid (rendering 200 cards when the viewport shows 12), a waterfall of sequential API calls on the product detail page, three competing analytics scripts blocking the main thread for 800ms, and layout shifts from dynamically loaded images. The team of 10 frontend developers runs Lighthouse audits, but the reports list 30+ recommendations without prioritization — nobody knows which fix gives the biggest improvement for the least effort. The team integrates AI at three points: (1) automated profiling that captures runtime performance data (React Profiler, PerformanceObserver, Long Tasks, bundle analysis) and builds a unified performance model, (2) root cause analysis using an LLM that correlates multiple signals to identify the true bottlenecks, and (3) prioritized fix generation that estimates impact, effort, and risk for each recommendation. This post covers how each pipeline works.
Problem Statements
-
Automated Performance Data Collection: How do you automatically capture a comprehensive performance profile — bundle composition, runtime rendering behavior, network waterfalls, main thread blocking, Core Web Vitals, memory patterns — and correlate them into a single diagnostic model?
-
AI Root Cause Analysis: How does an LLM analyze interleaved performance signals (React Profiler output, Long Task entries, network timing, Lighthouse scores) to identify the actual bottlenecks versus symptoms? How do you prevent the AI from recommending generic advice instead of specific, code-level fixes?
-
Impact-Effort Prioritization: How do you estimate the performance impact of each fix before implementing it? How does the system learn from historical fixes to improve future estimates?
Deep Dive: Internal Mechanisms
1. Comprehensive Performance Data Collection
/*
* Collecting performance data from MULTIPLE sources
* and correlating them into one diagnostic object.
*
* Data sources:
* ┌──────────────────────────────────────────────────┐
* │ 1. Core Web Vitals (CWV): │
* │ LCP, FID/INP, CLS — via web-vitals library │
* │ │
* │ 2. Long Tasks API: │
* │ Tasks blocking main thread > 50ms │
* │ │
* │ 3. Resource Timing API: │
* │ Network requests: URL, size, duration │
* │ │
* │ 4. React Profiler: │
* │ Component render times, re-render counts │
* │ │
* │ 5. Bundle analysis: │
* │ Chunk sizes, module composition, tree-shaking │
* │ │
* │ 6. Memory snapshots: │
* │ Heap size over time, detached DOM nodes │
* │ │
* │ 7. Layout shift entries: │
* │ Which elements shifted, when, by how much │
* └──────────────────────────────────────────────────┘
*/
class PerformanceCollector {
constructor() {
this.data = {
cwv: {},
longTasks: [],
resources: [],
reactProfiles: [],
layoutShifts: [],
memorySnapshots: [],
navigationTiming: null,
};
}
startCollection() {
this.collectCoreWebVitals();
this.collectLongTasks();
this.collectResources();
this.collectLayoutShifts();
this.collectMemory();
this.collectNavigationTiming();
}
collectCoreWebVitals() {
import('web-vitals').then(({ onLCP, onINP, onCLS, onFCP, onTTFB }) => {
onLCP(metric => { this.data.cwv.lcp = this.formatMetric(metric); });
onINP(metric => { this.data.cwv.inp = this.formatMetric(metric); });
onCLS(metric => { this.data.cwv.cls = this.formatMetric(metric); });
onFCP(metric => { this.data.cwv.fcp = this.formatMetric(metric); });
onTTFB(metric => { this.data.cwv.ttfb = this.formatMetric(metric); });
});
}
formatMetric(metric) {
return {
name: metric.name,
value: metric.value,
rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
entries: metric.entries?.map(e => ({
element: e.element?.tagName,
url: e.url,
startTime: e.startTime,
size: e.size,
})),
};
}
collectLongTasks() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.data.longTasks.push({
duration: entry.duration,
startTime: entry.startTime,
name: entry.name,
// Attribution tells us WHICH script caused it:
attribution: entry.attribution?.map(a => ({
containerType: a.containerType,
containerSrc: a.containerSrc,
containerId: a.containerId,
})),
});
}
});
observer.observe({ type: 'longtask', buffered: true });
}
collectResources() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.data.resources.push({
name: entry.name,
type: entry.initiatorType,
transferSize: entry.transferSize,
decodedBodySize: entry.decodedBodySize,
duration: entry.duration,
startTime: entry.startTime,
// Timing breakdown:
dns: entry.domainLookupEnd - entry.domainLookupStart,
tcp: entry.connectEnd - entry.connectStart,
tls: entry.secureConnectionStart > 0
? entry.connectEnd - entry.secureConnectionStart : 0,
ttfb: entry.responseStart - entry.requestStart,
download: entry.responseEnd - entry.responseStart,
// Was this blocking?
renderBlocking: entry.renderBlockingStatus,
});
}
});
observer.observe({ type: 'resource', buffered: true });
}
collectLayoutShifts() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) { // Only unexpected shifts
this.data.layoutShifts.push({
value: entry.value,
startTime: entry.startTime,
sources: entry.sources?.map(s => ({
node: s.node?.tagName,
previousRect: s.previousRect,
currentRect: s.currentRect,
})),
});
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });
}
collectMemory() {
if (performance.memory) {
const snapshot = () => {
this.data.memorySnapshots.push({
timestamp: Date.now(),
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
});
};
snapshot();
this.memoryInterval = setInterval(snapshot, 5000);
}
}
collectNavigationTiming() {
window.addEventListener('load', () => {
const nav = performance.getEntriesByType('navigation')[0];
if (nav) {
this.data.navigationTiming = {
dns: nav.domainLookupEnd - nav.domainLookupStart,
tcp: nav.connectEnd - nav.connectStart,
ttfb: nav.responseStart - nav.requestStart,
download: nav.responseEnd - nav.responseStart,
domParsing: nav.domInteractive - nav.responseEnd,
domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
load: nav.loadEventEnd - nav.startTime,
transferSize: nav.transferSize,
type: nav.type, // 'navigate' | 'reload' | 'back_forward'
};
}
});
}
getReport() {
if (this.memoryInterval) clearInterval(this.memoryInterval);
return { ...this.data };
}
}
2. React Component Profiling
/*
* React Profiler API captures RENDER-LEVEL performance:
* - Which components rendered
* - How long each render took
* - Why they re-rendered
*
* Combined with React DevTools' "why did this render" data,
* we can identify unnecessary re-renders.
*/
// Profiler wrapper that collects render data:
class ReactProfileCollector {
constructor() {
this.renders = [];
this.componentStats = new Map();
}
// onRender callback for <Profiler>:
onRender = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
const render = {
component: id,
phase, // 'mount' or 'update'
actualDuration, // Time spent rendering (ms)
baseDuration, // Estimated time without memoization
startTime,
commitTime,
timestamp: Date.now(),
};
this.renders.push(render);
// Aggregate per-component stats:
if (!this.componentStats.has(id)) {
this.componentStats.set(id, {
renderCount: 0,
totalActualDuration: 0,
totalBaseDuration: 0,
maxActualDuration: 0,
mountDuration: null,
});
}
const stats = this.componentStats.get(id);
stats.renderCount++;
stats.totalActualDuration += actualDuration;
stats.totalBaseDuration += baseDuration;
stats.maxActualDuration = Math.max(stats.maxActualDuration, actualDuration);
if (phase === 'mount') {
stats.mountDuration = actualDuration;
}
};
getTopRenderOffenders(limit = 20) {
return [...this.componentStats.entries()]
.map(([component, stats]) => ({
component,
renderCount: stats.renderCount,
totalTime: stats.totalActualDuration,
avgTime: stats.totalActualDuration / stats.renderCount,
maxTime: stats.maxActualDuration,
memoSavings: stats.totalBaseDuration - stats.totalActualDuration,
updateCount: stats.renderCount - (stats.mountDuration ? 1 : 0),
}))
.sort((a, b) => b.totalTime - a.totalTime)
.slice(0, limit);
}
getReport() {
return {
totalRenders: this.renders.length,
uniqueComponents: this.componentStats.size,
topOffenders: this.getTopRenderOffenders(),
renderTimeline: this.renders.slice(-100), // Last 100 renders
};
}
}
// HOC to track why a component re-rendered:
function withRenderTracking(Component, componentName) {
const TrackedComponent = React.memo(function TrackedComponent(props) {
const prevPropsRef = useRef(props);
useEffect(() => {
const prevProps = prevPropsRef.current;
const changes = {};
for (const key of new Set([
...Object.keys(prevProps),
...Object.keys(props)
])) {
if (!Object.is(prevProps[key], props[key])) {
changes[key] = {
from: typeof prevProps[key] === 'object'
? '(object)' : prevProps[key],
to: typeof props[key] === 'object'
? '(object)' : props[key],
referenceChanged: prevProps[key] !== props[key],
};
}
}
if (Object.keys(changes).length > 0) {
window.__renderReasons = window.__renderReasons || [];
window.__renderReasons.push({
component: componentName,
changes,
timestamp: Date.now(),
});
}
prevPropsRef.current = props;
});
return <Component {...props} />;
});
TrackedComponent.displayName = `Tracked(${componentName})`;
return TrackedComponent;
}
3. Bundle Analysis Integration
/*
* Bundle composition is a critical input for performance analysis.
* Webpack/Vite stats + source-map-explorer data tells us:
* - Which packages contribute the most bytes
* - Which chunks are loaded on initial page load vs lazy
* - Duplicate packages across chunks
* - Tree-shaking effectiveness
*/
async function analyzeBundleComposition(statsFile) {
const stats = JSON.parse(await fs.readFile(statsFile, 'utf-8'));
const analysis = {
chunks: [],
totalSize: 0,
initialSize: 0,
lazySize: 0,
duplicateModules: [],
largestModules: [],
};
// Analyze each chunk:
for (const chunk of stats.chunks) {
const chunkInfo = {
name: chunk.names[0] || chunk.id,
size: chunk.size,
isInitial: chunk.initial,
modules: [],
};
// Module breakdown:
for (const module of chunk.modules || []) {
chunkInfo.modules.push({
name: module.name,
size: module.size,
// Identify node_modules packages:
package: extractPackageName(module.name),
});
}
chunkInfo.modules.sort((a, b) => b.size - a.size);
analysis.chunks.push(chunkInfo);
analysis.totalSize += chunk.size;
if (chunk.initial) {
analysis.initialSize += chunk.size;
} else {
analysis.lazySize += chunk.size;
}
}
// Find duplicate packages across chunks:
const packageToChunks = new Map();
for (const chunk of analysis.chunks) {
for (const mod of chunk.modules) {
if (mod.package) {
if (!packageToChunks.has(mod.package)) {
packageToChunks.set(mod.package, []);
}
packageToChunks.get(mod.package).push(chunk.name);
}
}
}
analysis.duplicateModules = [...packageToChunks.entries()]
.filter(([, chunks]) => new Set(chunks).size > 1)
.map(([pkg, chunks]) => ({
package: pkg,
inChunks: [...new Set(chunks)],
duplicateCount: new Set(chunks).size,
}));
// Largest modules across all chunks:
analysis.largestModules = analysis.chunks
.flatMap(c => c.modules)
.sort((a, b) => b.size - a.size)
.slice(0, 30);
return analysis;
}
function extractPackageName(modulePath) {
const match = modulePath.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)/);
return match ? match[1] : null;
}
4. AI Root Cause Analysis Engine
/*
* The AI receives ALL collected data and identifies:
* 1. The PRIMARY bottleneck (not just symptoms)
* 2. Causal chains (X causes Y which causes Z)
* 3. Specific code locations to fix
*/
async function analyzePerformance(perfData, bundleData, reactProfile) {
const prompt = `You are a senior performance engineer analyzing a React application.
## CORE WEB VITALS
${Object.entries(perfData.cwv).map(([name, data]) =>
`${name.toUpperCase()}: ${data.value}${name === 'cls' ? '' : 'ms'} (${data.rating})`
).join('\n')}
## NAVIGATION TIMING
TTFB: ${perfData.navigationTiming?.ttfb}ms
DOM Content Loaded: ${perfData.navigationTiming?.domContentLoaded}ms
Page Load: ${perfData.navigationTiming?.load}ms
## LONG TASKS (${perfData.longTasks.length} total, showing top 10 by duration)
${perfData.longTasks
.sort((a, b) => b.duration - a.duration)
.slice(0, 10)
.map(lt => ` ${lt.duration.toFixed(0)}ms at ${lt.startTime.toFixed(0)}ms${lt.attribution?.[0]?.containerSrc ? ` (${lt.attribution[0].containerSrc})` : ''}`)
.join('\n')}
## NETWORK REQUESTS (${perfData.resources.length} total, showing slowest 10)
${perfData.resources
.sort((a, b) => b.duration - a.duration)
.slice(0, 10)
.map(r => ` ${r.duration.toFixed(0)}ms | ${(r.transferSize/1024).toFixed(0)}KB | ${r.type} | ${r.name.split('/').pop().slice(0, 50)}${r.renderBlocking === 'blocking' ? ' [RENDER BLOCKING]' : ''}`)
.join('\n')}
## BUNDLE COMPOSITION
Total: ${(bundleData.totalSize/1024).toFixed(0)}KB
Initial load: ${(bundleData.initialSize/1024).toFixed(0)}KB
Lazy-loaded: ${(bundleData.lazySize/1024).toFixed(0)}KB
Largest packages:
${bundleData.largestModules.slice(0, 10).map(m =>
` ${(m.size/1024).toFixed(0)}KB - ${m.name.slice(0, 60)}`
).join('\n')}
Duplicate packages: ${bundleData.duplicateModules.map(d => d.package).join(', ') || 'none'}
## REACT RENDER PROFILE (top re-rendering components)
${reactProfile.topOffenders.slice(0, 10).map(c =>
` ${c.component}: ${c.renderCount} renders, ${c.totalTime.toFixed(0)}ms total, ${c.avgTime.toFixed(1)}ms avg`
).join('\n')}
## LAYOUT SHIFTS (CLS sources)
${perfData.layoutShifts.slice(0, 5).map(ls =>
` Shift: ${ls.value.toFixed(4)} at ${ls.startTime.toFixed(0)}ms — ${ls.sources?.map(s => s.node).join(', ') || 'unknown element'}`
).join('\n')}
## MEMORY
${perfData.memorySnapshots.length > 0 ? `
Initial: ${(perfData.memorySnapshots[0].usedJSHeapSize / 1048576).toFixed(1)}MB
Latest: ${(perfData.memorySnapshots[perfData.memorySnapshots.length - 1].usedJSHeapSize / 1048576).toFixed(1)}MB
Trend: ${perfData.memorySnapshots[perfData.memorySnapshots.length - 1].usedJSHeapSize > perfData.memorySnapshots[0].usedJSHeapSize * 1.5 ? 'GROWING (possible leak)' : 'stable'}` : 'Not available'}
---
ANALYZE:
1. What is the PRIMARY performance bottleneck? (the one fix that would improve the most)
2. What is the CAUSAL CHAIN? (A → B → C, not just symptoms)
3. List ALL issues found, ranked by IMPACT (estimated ms/points improvement)
4. For each issue, provide:
- Category: bundle | rendering | network | layout | memory
- Specific fix (code-level, not generic advice)
- Estimated impact on Core Web Vitals
- Effort to fix: low (< 1 hour) | medium (1-4 hours) | high (4+ hours)
- Risk: low | medium | high
Format each issue as a structured recommendation.`;
const analysis = await callLLM(prompt, {
model: 'gpt-4o',
temperature: 0.1,
maxTokens: 2000,
});
return parsePerformanceAnalysis(analysis);
}
5. Automated Fix Generation
/*
* From analysis → specific code fixes:
*
* Common fix patterns the AI generates:
*
* 1. Bundle:
* - Dynamic import for large packages
* - Replace heavy library with lighter alternative
* - Tree-shake unused exports
*
* 2. Rendering:
* - Add React.memo to frequently re-rendering components
* - Add useMemo/useCallback for expensive computations
* - Virtualize long lists
*
* 3. Network:
* - Add preload/prefetch for critical resources
* - Parallelize sequential API calls
* - Add caching headers
*
* 4. Layout:
* - Add explicit dimensions to images/iframes
* - Reserve space for dynamic content
* - Use content-visibility for off-screen content
*/
async function generateFixes(analysis, sourceCode) {
const fixes = [];
for (const issue of analysis.issues) {
const fix = await generateFixForIssue(issue, sourceCode);
if (fix) {
fixes.push({
issue,
fix,
priority: calculatePriority(issue),
});
}
}
// Sort by priority (impact / effort):
fixes.sort((a, b) => b.priority - a.priority);
return fixes;
}
async function generateFixForIssue(issue, sourceCode) {
// Pattern-based fixes (no AI needed for common patterns):
switch (issue.category) {
case 'bundle': {
if (issue.type === 'large-package-on-initial') {
return generateDynamicImportFix(issue);
}
if (issue.type === 'duplicate-package') {
return generateDeduplicationFix(issue);
}
break;
}
case 'rendering': {
if (issue.type === 'excessive-rerenders') {
return generateMemoizationFix(issue, sourceCode);
}
if (issue.type === 'large-list-no-virtualization') {
return generateVirtualizationFix(issue, sourceCode);
}
break;
}
case 'network': {
if (issue.type === 'sequential-requests') {
return generateParallelRequestFix(issue, sourceCode);
}
if (issue.type === 'missing-preload') {
return generatePreloadFix(issue);
}
break;
}
case 'layout': {
if (issue.type === 'image-no-dimensions') {
return generateImageDimensionFix(issue, sourceCode);
}
break;
}
}
// AI-generated fix for non-standard issues:
return await generateAIFix(issue, sourceCode);
}
function generateDynamicImportFix(issue) {
// Example: Convert static import to dynamic:
return {
type: 'code-change',
description: `Lazy-load ${issue.package} (${(issue.size / 1024).toFixed(0)}KB) — not needed on initial render`,
before: `import { Chart } from '${issue.package}';`,
after: `const Chart = lazy(() => import('${issue.package}').then(m => ({ default: m.Chart })));`,
files: issue.files,
estimatedImpact: {
initialBundleReduction: issue.size,
lcpImprovement: Math.round(issue.size / 1024 * 10), // rough: 10ms per KB
},
};
}
function generateMemoizationFix(issue, sourceCode) {
const component = issue.component;
return {
type: 'code-change',
description: `Memoize ${component} — re-renders ${issue.renderCount} times (${issue.totalTime.toFixed(0)}ms total), most re-renders show same props`,
suggestion: `Wrap with React.memo and memoize callback props:
// Before:
function ${component}({ data, onSelect }) {
return <div>...</div>;
}
// After:
const ${component} = React.memo(function ${component}({ data, onSelect }) {
return <div>...</div>;
});
// In parent, memoize the callback:
const handleSelect = useCallback((item) => {
setSelected(item);
}, []);`,
files: issue.files,
estimatedImpact: {
renderTimeReduction: issue.totalTime * 0.7, // Assumes 70% of re-renders eliminated
},
};
}
function generateParallelRequestFix(issue) {
return {
type: 'code-change',
description: `Parallelize ${issue.requests.length} sequential API calls (current: ${issue.totalDuration}ms serial → estimated: ${issue.maxDuration}ms parallel)`,
suggestion: `// Before (sequential):
const user = await fetch('/api/user');
const posts = await fetch('/api/posts');
const comments = await fetch('/api/comments');
// After (parallel):
const [user, posts, comments] = await Promise.all([
fetch('/api/user'),
fetch('/api/posts'),
fetch('/api/comments'),
]);`,
estimatedImpact: {
timeReduction: issue.totalDuration - issue.maxDuration,
},
};
}
6. Network Waterfall Analysis
/*
* Network waterfall analysis identifies:
* 1. Request chains (A → B → C: each waits for the previous)
* 2. Render-blocking resources
* 3. Unnecessarily large responses
* 4. Missing compression, caching, or CDN
*/
function analyzeNetworkWaterfall(resources) {
const analysis = {
requestChains: [],
renderBlocking: [],
oversizedResponses: [],
compressionMissing: [],
cachingIssues: [],
};
// Sort by start time:
const sorted = [...resources].sort((a, b) => a.startTime - b.startTime);
// Detect request chains:
// A chain is when request B starts after request A completes,
// AND B depends on A's response (heuristic: B starts within 10ms of A ending).
for (let i = 0; i < sorted.length; i++) {
for (let j = i + 1; j < sorted.length; j++) {
const a = sorted[i];
const b = sorted[j];
const aEnd = a.startTime + a.duration;
const gap = b.startTime - aEnd;
if (gap >= 0 && gap < 10) { // B starts right after A
// Check if they're related (same domain or referrer):
const aHost = new URL(a.name).hostname;
const bHost = new URL(b.name).hostname;
if (aHost === bHost || gap < 5) {
analysis.requestChains.push({
chain: [a.name, b.name],
totalDuration: (b.startTime + b.duration) - a.startTime,
parallelizable: !isDependentRequest(a, b),
});
}
}
}
}
// Detect render-blocking resources:
analysis.renderBlocking = sorted.filter(r =>
r.renderBlocking === 'blocking' ||
(r.type === 'script' && !r.name.includes('async') && r.startTime < 1000)
);
// Detect oversized responses:
analysis.oversizedResponses = sorted.filter(r => {
if (r.type === 'img' && r.transferSize > 200 * 1024) return true;
if (r.type === 'script' && r.transferSize > 100 * 1024) return true;
if (r.type === 'css' && r.transferSize > 50 * 1024) return true;
if (r.type === 'fetch' && r.transferSize > 500 * 1024) return true;
return false;
});
// Detect missing compression:
analysis.compressionMissing = sorted.filter(r =>
r.decodedBodySize > 1024 &&
r.transferSize > 0 &&
r.decodedBodySize / r.transferSize < 1.5 // Should be at least 1.5x compression ratio
);
return analysis;
}
7. Performance Budget Monitoring
/*
* Performance budgets define LIMITS for key metrics.
* AI helps by:
* 1. Setting realistic budgets based on industry benchmarks
* 2. Predicting budget violations before they happen
* 3. Attributing budget violations to specific changes
*/
class PerformanceBudgetMonitor {
constructor(budgets) {
this.budgets = budgets;
// Example budgets:
// {
// lcp: { target: 2500, warning: 2000, unit: 'ms' },
// cls: { target: 0.1, warning: 0.05, unit: '' },
// inp: { target: 200, warning: 100, unit: 'ms' },
// initialBundle: { target: 200, warning: 150, unit: 'KB' },
// totalBundle: { target: 500, warning: 400, unit: 'KB' },
// thirdPartyScripts: { target: 3, warning: 2, unit: 'count' },
// imagePayload: { target: 500, warning: 300, unit: 'KB' },
// }
this.history = [];
}
check(currentMetrics) {
const results = [];
for (const [metric, budget] of Object.entries(this.budgets)) {
const current = currentMetrics[metric];
if (current === undefined) continue;
const status = current > budget.target ? 'over'
: current > budget.warning ? 'warning'
: 'ok';
results.push({
metric,
current,
target: budget.target,
warning: budget.warning,
status,
overBy: status === 'over' ? current - budget.target : 0,
unit: budget.unit,
});
}
return results;
}
// Track budget trends over time:
addSnapshot(metrics, metadata) {
this.history.push({
metrics,
metadata, // { commit, branch, timestamp, pr }
timestamp: Date.now(),
});
}
// Detect which commit caused a budget regression:
async findRegression(metric) {
const sorted = this.history
.filter(h => h.metrics[metric] !== undefined)
.sort((a, b) => a.timestamp - b.timestamp);
for (let i = 1; i < sorted.length; i++) {
const prev = sorted[i - 1];
const curr = sorted[i];
const diff = curr.metrics[metric] - prev.metrics[metric];
const budget = this.budgets[metric];
// Significant regression:
if (diff > 0 && prev.metrics[metric] <= budget.target &&
curr.metrics[metric] > budget.target) {
return {
metric,
regressionCommit: curr.metadata.commit,
regressionPR: curr.metadata.pr,
before: prev.metrics[metric],
after: curr.metrics[metric],
increase: diff,
timestamp: curr.timestamp,
};
}
}
return null;
}
}
// CI integration: check budgets on every PR
async function checkPerformanceBudgetsCI(prMetrics, budgets) {
const monitor = new PerformanceBudgetMonitor(budgets);
const results = monitor.check(prMetrics);
const violations = results.filter(r => r.status === 'over');
const warnings = results.filter(r => r.status === 'warning');
if (violations.length > 0) {
// Post PR comment with violations:
const comment = `## Performance Budget Violations
${violations.map(v =>
`- **${v.metric}**: ${v.current}${v.unit} (budget: ${v.target}${v.unit}, over by ${v.overBy}${v.unit})`
).join('\n')}
${warnings.length > 0 ? `\n### Warnings\n${warnings.map(w =>
`- **${w.metric}**: ${w.current}${w.unit} (warning at ${w.warning}${w.unit})`
).join('\n')}` : ''}`;
return { pass: false, comment };
}
return { pass: true };
}
8. Lighthouse CI with AI Interpretation
/*
* Lighthouse gives scores and recommendations,
* but doesn't prioritize or explain the relationships.
* AI interprets the raw Lighthouse data.
*/
async function interpretLighthouseResults(lighthouseReport) {
const audits = lighthouseReport.audits;
// Extract failing and warning audits:
const failing = Object.entries(audits)
.filter(([, a]) => a.score !== null && a.score < 0.5)
.map(([id, a]) => ({
id,
title: a.title,
score: a.score,
displayValue: a.displayValue,
description: a.description,
numericValue: a.numericValue,
details: a.details?.items?.slice(0, 5), // Top 5 items
}))
.sort((a, b) => a.score - b.score);
const prompt = `Analyze this Lighthouse report for a React e-commerce application.
SCORES:
Performance: ${lighthouseReport.categories.performance.score * 100}
Accessibility: ${lighthouseReport.categories.accessibility.score * 100}
Best Practices: ${lighthouseReport.categories['best-practices'].score * 100}
SEO: ${lighthouseReport.categories.seo.score * 100}
FAILING AUDITS (${failing.length}):
${failing.map(f => `
${f.title} (score: ${Math.round(f.score * 100)})
Value: ${f.displayValue}
${f.details ? `Top items: ${JSON.stringify(f.details).slice(0, 200)}` : ''}
`).join('\n')}
PROVIDE:
1. The #1 thing to fix first and WHY (the audit that would improve the score the most)
2. Which audits are RELATED (fixing one improves another)
3. Which audits are FALSE POSITIVES for a React SPA (if any)
4. A prioritized fix plan: order audits by (impact × ease), not by score
Keep recommendations specific to React/Next.js applications.`;
return await callLLM(prompt, {
model: 'gpt-4o',
temperature: 0.1,
maxTokens: 1000,
});
}
9. Continuous Performance Regression Detection
/*
* Track performance metrics per commit/PR to catch regressions
* before they accumulate.
*
* ┌──────────────────────────────────────────────────┐
* │ PR opened │
* │ │ │
* │ ▼ │
* │ Build preview deployment │
* │ │ │
* │ ▼ │
* │ Run Lighthouse CI on preview │
* │ Run bundle size check │
* │ │ │
* │ ▼ │
* │ Compare with main branch baseline │
* │ │ │
* │ ▼ │
* │ AI analysis: "This PR adds 45KB to initial │
* │ bundle from `chart.js`. LCP expected to │
* │ increase by ~200ms. Consider lazy-loading │
* │ the chart component." │
* │ │ │
* │ ▼ │
* │ Post PR comment with findings │
* └──────────────────────────────────────────────────┘
*/
async function analyzePRPerformanceImpact(prMetrics, baseMetrics, prChanges) {
const diffs = {};
for (const metric of Object.keys(prMetrics)) {
if (baseMetrics[metric] !== undefined) {
diffs[metric] = {
base: baseMetrics[metric],
pr: prMetrics[metric],
change: prMetrics[metric] - baseMetrics[metric],
changePercent: ((prMetrics[metric] - baseMetrics[metric]) /
baseMetrics[metric]) * 100,
};
}
}
// Identify significant changes:
const significantChanges = Object.entries(diffs)
.filter(([, d]) => Math.abs(d.changePercent) > 5) // >5% change
.map(([metric, d]) => ({ metric, ...d }));
if (significantChanges.length === 0) {
return { significant: false, message: 'No significant performance changes.' };
}
// AI analysis:
const prompt = `A PR made these performance changes in a React app.
FILES CHANGED:
${prChanges.files.slice(0, 20).map(f => ` ${f.status} ${f.filename} (+${f.additions} -${f.deletions})`).join('\n')}
PACKAGES ADDED/CHANGED:
${prChanges.packageChanges?.map(p => ` ${p.action} ${p.name}@${p.version} (${(p.size / 1024).toFixed(0)}KB)`).join('\n') || 'none'}
PERFORMANCE METRIC CHANGES:
${significantChanges.map(c =>
` ${c.metric}: ${c.base} → ${c.pr} (${c.change > 0 ? '+' : ''}${c.changePercent.toFixed(1)}%)`
).join('\n')}
EXPLAIN:
1. What in this PR caused each metric change?
2. Are these regressions acceptable given the feature being added?
3. Specific suggestions to mitigate any regressions
4. Overall recommendation: approve / request changes / investigate further`;
const analysis = await callLLM(prompt, {
model: 'gpt-4o',
temperature: 0.1,
maxTokens: 500,
});
return {
significant: true,
changes: significantChanges,
analysis,
};
}
10. Performance Optimization Dashboard
/*
* A React dashboard that visualizes:
* 1. Current CWV scores with trends
* 2. Bundle composition treemap
* 3. Top re-rendering components
* 4. AI-generated optimization recommendations
* 5. Historical performance over time
*/
function PerformanceDashboard({ projectId }) {
const cwvData = useCWVHistory(projectId);
const bundleData = useBundleAnalysis(projectId);
const recommendations = usePerformanceRecommendations(projectId);
return (
<div className="perf-dashboard">
{/* CWV Overview Cards */}
<div className="cwv-grid">
{['LCP', 'INP', 'CLS'].map(metric => (
<CWVCard
key={metric}
metric={metric}
current={cwvData.current?.[metric.toLowerCase()]}
trend={cwvData.trend?.[metric.toLowerCase()]}
target={cwvData.budgets?.[metric.toLowerCase()]}
/>
))}
</div>
{/* Bundle Treemap */}
<section className="bundle-section">
<h2>Bundle Composition ({(bundleData.totalSize / 1024).toFixed(0)}KB)</h2>
<BundleTreemap data={bundleData.chunks} />
<div className="bundle-alerts">
{bundleData.duplicateModules?.map(dup => (
<div key={dup.package} className="alert warning">
<strong>{dup.package}</strong> is duplicated in {dup.duplicateCount} chunks
</div>
))}
</div>
</section>
{/* AI Recommendations */}
<section className="recommendations">
<h2>AI Optimization Recommendations</h2>
{recommendations.map((rec, i) => (
<RecommendationCard
key={i}
rank={i + 1}
recommendation={rec}
/>
))}
</section>
{/* Historical Trend */}
<section className="trend-section">
<h2>Performance Over Time</h2>
<PerformanceTrendChart data={cwvData.history} />
</section>
</div>
);
}
function RecommendationCard({ rank, recommendation }) {
const [expanded, setExpanded] = useState(false);
const impactColor = recommendation.impact === 'high' ? 'red'
: recommendation.impact === 'medium' ? 'orange'
: 'green';
return (
<div className="rec-card">
<div className="rec-header">
<span className="rec-rank">#{rank}</span>
<div className="rec-title">
<h3>{recommendation.title}</h3>
<div className="rec-tags">
<span className={`tag impact-${impactColor}`}>
Impact: {recommendation.impact}
</span>
<span className="tag">
Effort: {recommendation.effort}
</span>
<span className="tag">
Category: {recommendation.category}
</span>
</div>
</div>
</div>
<p className="rec-summary">{recommendation.summary}</p>
{recommendation.estimatedImprovement && (
<div className="rec-improvement">
Estimated: {recommendation.estimatedImprovement}
</div>
)}
<button onClick={() => setExpanded(!expanded)}>
{expanded ? 'Hide details' : 'Show fix details'}
</button>
{expanded && (
<div className="rec-details">
<h4>Files to change:</h4>
<ul>
{recommendation.files?.map(f => (
<li key={f}><code>{f}</code></li>
))}
</ul>
<h4>Suggested fix:</h4>
<pre><code>{recommendation.codeChange}</code></pre>
</div>
)}
</div>
);
}
Trade-offs & Considerations
| Aspect | Manual Profiling | Lighthouse CI Only | AI Analysis | Full AI Pipeline |
|---|---|---|---|---|
| Time to diagnose | 2-4 hours | 5 min (report) | 5 min (analysis) | 5 min |
| Specificity | High (human) | Generic recs | Code-level fixes | Code-level fixes |
| Prioritization | Subjective | By score impact | By impact/effort | By impact/effort |
| Cross-signal correlation | Manual | None | Strong | Strong |
| False recommendations | Low | Medium | Low-Medium | Low-Medium |
| Historical tracking | Manual | Per-run | Automated trends | Automated trends |
| Cost | Engineer hours | Free (OSS) | ~$0.10/analysis | ~$0.10/analysis |
| React-specific insights | If expert | Generic web | React-aware | React-aware |
Best Practices
-
Collect data from multiple sources and correlate them — no single tool tells the full story — Core Web Vitals show WHAT is slow, React Profiler shows WHICH components, Long Tasks API shows WHAT blocks the main thread, Resource Timing shows network waterfalls, and bundle analysis shows WHY the initial load is heavy; feed all signals into the AI analysis together; an LCP regression might be caused by a render-blocking script (Long Tasks) that was added because a new package (bundle analysis) forces a synchronous re-render (React Profiler).
-
Use deterministic pattern matching for common optimizations and AI only for complex diagnosis — "this component re-renders 50 times but receives the same props" → add React.memo (deterministic rule); "three API calls are sequential but independent" → Promise.all (deterministic); "the bundle grew 200KB from adding chart.js but it's only used on one page" → lazy-load (deterministic); reserve AI for correlating multiple signals, explaining causal chains, and prioritizing across categories.
-
Track performance metrics per commit in CI and use AI to explain regressions — run Lighthouse CI and bundle analysis on every PR; compare with the main branch baseline; when a metric regresses >5%, use AI to correlate the regression with the specific files changed and packages added; post the analysis as a PR comment; this catches regressions before they accumulate into the "death by a thousand cuts" problem.
-
Set performance budgets with warning thresholds, not just hard limits — a hard budget of LCP < 2.5s with a warning at 2.0s gives the team time to react before violations; track budget trends — a metric that went from 1.2s to 2.3s over six months is more concerning than one that jumped from 1.5s to 2.6s in one PR (the PR is easier to revert); AI analysis should highlight trending metrics approaching their budgets.
-
Estimate fix impact before implementing — prioritize by impact-to-effort ratio — each AI recommendation should include an estimated improvement (ms, KB, or CLS score points) and an effort estimate; lazy-loading a 100KB package (effort: 15 min, impact: ~100ms LCP reduction) should rank higher than virtualizing a 50-item list (effort: 2 hours, impact: ~30ms INP improvement); track actual improvements after fixes to calibrate future estimates.
Conclusion
AI-powered performance profiling transforms optimization from a manual, expert-dependent process into a systematic pipeline. Data collection from multiple browser APIs — PerformanceObserver (Long Tasks, Layout Shifts, Resource Timing), web-vitals (CWV metrics), React Profiler (component render times and re-render counts), and webpack stats (bundle composition) — creates a comprehensive performance model that no single tool provides. The AI root cause analysis correlates these signals to identify causal chains: an LCP regression traced from a render-blocking script to a new package that forces synchronous initialization, identified by cross-referencing Long Task attribution with bundle analysis module sizes. Automated fix generation uses pattern matching for common optimizations (dynamic imports for large packages, React.memo for unnecessary re-renders, Promise.all for sequential requests) and LLM analysis for complex multi-signal issues. Performance budget monitoring tracks metrics per commit, uses warning thresholds to catch regressions before they violate budgets, and AI-powered PR analysis correlates metric changes with specific code changes. The prioritization engine ranks recommendations by impact-to-effort ratio — estimated improvement divided by implementation time — and tracks actual outcomes to calibrate future estimates. The dashboard visualizes CWV trends, bundle composition treemaps, top re-rendering components, and AI recommendations ranked by priority, giving the team a single view of what to fix next and why.
What did you think?