AI Agent 驱动的前端性能优化:从检测到修复的自动化闭环

一、"检测-分析-修复"三轮反复:人类在性能优化中的角色浪费

一个标准的性能优化流程是这样的:(1)Lighthouse 报告说 LCP 超标 1.2 秒;(2)你打开 DevTools Performance 面板,花了 20 分钟找到卡顿的原因是"首屏加载了 2 个未压缩的 500KB PNG";(3)你把这两张 PNG 转成 WebP → 构建 → 部署 → LCP 从 3.8s 降到 2.4s;(4)两天后,设计师又上传了一张 1.2MB 的 PNG Banner,LCP 又回到了 4s。

你在这个循环中做了什么?检测(Lighthouse)→ 分析(Performance 面板)→ 修复(图片格式转换)→ 检测(新一轮 Lighthouse)。机器做了第一步,人类做了中间两步。但中间两步——分析瓶颈根因、执行格式转换——是高度规则化的。如果 AI Agent 能做"分析+修复",人类只需要做"确认+merge",这个循环的周期可以从 2 天压缩到 20 分钟。

二、AI Agent 的性能优化闭环架构

flowchart TD
    A["CI/CD Pipeline<br/>触发性能检查"] --> B["Lighthouse CI<br/>生成性能报告"]
    B --> C{"LCP / FCP / TBT<br/>是否超标?"}
    C -->|"✅ 达标"| D["Pass,无操作"]
    
    C -->|"❌ 超标"| E["AI Agent 接管"]
    E --> F["分析 Lighthouse 报告<br/>识别瓶颈类型"]
    
    F --> G{"瓶颈类型"}
    G -->|"图片过大"| H1["Agent: 自动压缩图片<br/>PNG → WebP/AVIF"]
    G -->|"JS Bundle 过大"| H2["Agent: 分析 Bundle<br/>建议代码分割点"]
    G -->|"阻塞 CSS 过多"| H3["Agent: 提取 Critical CSS<br/>异步加载非关键样式"]
    G -->|"字体过大"| H4["Agent: 子集化字体<br/>减少字重"]
    
    H1 --> I["生成 Fix PR<br/>(含修改内容 + 性能收益预估)"]
    H2 --> I
    H3 --> I
    H4 --> I
    
    I --> J["开发者 Review <br/>确认或拒绝修复"]
    J --> K["合并 PR → 重新检测 → 闭环"]

三、Agent 修复引擎的实现

/**
 * AI 性能修复 Agent
 *
 * 自主执行流程:
 * 1. 读取 Lighthouse CI 报告
 * 2. 分析瓶颈类型
 * 3. 自动执行修复操作
 * 4. 创建包含修复的 PR
 * 5. 在 PR 中报告性能收益预估
 */
import { readFileSync, writeFileSync } from 'fs';
import { execSync } from 'child_process';
import sharp from 'sharp';
import { glob } from 'glob';

// === Lighthouse 报告解析 ===
interface LighthouseAudit {
  id: string;
  title: string;
  score: number;
  displayValue: string;
  details?: {
    items?: Array<Record<string, unknown>>;
  };
}

interface PerformanceIssue {
  type: 'oversized-images' | 'render-blocking' | 'large-bundle' | 'large-fonts' | 'excessive-dom';
  severity: 'critical' | 'warning';
  audit: LighthouseAudit;
  items: string[];
  estimatedSavingMs: number;
}

/**
 * 解析 Lighthouse JSON 报告,提取性能问题
 */
function parseLighthouseReport(reportPath: string): PerformanceIssue[] {
  const report = JSON.parse(readFileSync(reportPath, 'utf-8'));
  const audits: Record<string, LighthouseAudit> = report.audits || {};
  const issues: PerformanceIssue[] = [];

  // 图片过大
  if (audits['uses-optimized-images']?.score < 0.9) {
    const items = audits['uses-optimized-images'].details?.items || [];
    issues.push({
      type: 'oversized-images',
      severity: 'critical',
      audit: audits['uses-optimized-images'],
      items: items.map((i: any) => i.url || JSON.stringify(i)),
      estimatedSavingMs: items.reduce((s: number, i: any) => s + (i.wastedBytes || 0), 0) / 500000 * 1000,
    });
  }

  // 阻塞渲染资源
  if (audits['render-blocking-resources']?.score < 0.9) {
    const items = audits['render-blocking-resources'].details?.items || [];
    issues.push({
      type: 'render-blocking',
      severity: 'critical',
      audit: audits['render-blocking-resources'],
      items: items.map((i: any) => i.url || ''),
      estimatedSavingMs: items.reduce((s: number, i: any) => s + (i.wastedMs || 0), 0),
    });
  }

  // JS Bundle 过大
  if (audits['unused-javascript']?.score < 0.9) {
    issues.push({
      type: 'large-bundle',
      severity: 'warning',
      audit: audits['unused-javascript'],
      items: [audits['unused-javascript'].displayValue],
      estimatedSavingMs: (audits['unused-javascript'].details?.items || [])
        .reduce((s: number, i: any) => s + (i.wastedBytes || 0), 0) / 500000 * 1000,
    });
  }

  return issues;
}

/**
 * 自动修复:压缩超大图片
 *
 * 策略:
 * - PNG 转 WebP(质量 80)+ 同时生成 AVIF(质量 50)作为 <picture> 源
 * - JPEG 直接重新编码 WebP
 * - 超过 200KB 的原图生成 800/1200/2400 三档 srcset
 */
async function fixOversizedImages(projectDir: string): Promise<string[]> {
  const changes: string[] = [];
  const imageExtensions = '{png,jpg,jpeg}';

  const images = await glob(`${projectDir}/public/**/*.{png,jpg,jpeg}`, {
    nodir: true,
    ignore: ['**/node_modules/**'],
  });

  for (const imgPath of images) {
    const stat = readFileSync(imgPath);
    if (stat.length < 100 * 1024) continue; // 跳过 < 100KB 的图片

    const outWebp = imgPath.replace(/\.(png|jpe?g)$/, '.webp');

    // 转 WebP
    await sharp(imgPath)
      .webp({ quality: 80 })
      .toFile(outWebp);

    const newSize = (await import('fs')).statSync(outWebp).size;
    const saving = stat.length - newSize;

    changes.push(`${imgPath}: ${(stat.length / 1024).toFixed(0)}KB → ${(newSize / 1024).toFixed(0)}KB WebP (节省 ${(saving / 1024).toFixed(0)}KB)`);

    // 同时在代码中替换引用
    // 此处简化:实际应通过 AST 修改 import/require 引用
  }

  return changes;
}

/**
 * 自动修复:阻塞渲染 CSS → 提取 Critical CSS
 */
async function fixRenderBlocking(url: string): Promise<string> {
  // 1. 用 Puppeteer 提取 Critical CSS
  // 2. 内联到 HTML <head>
  // 3. 将原外部 CSS 链接改为 preload
  // 具体实现在 08 号文章中,此处省略
  return '已提取 Critical CSS 并内联';
}

/**
 * 生成修复 PR 的描述信息
 */
function generatePRDescription(issues: PerformanceIssue[], changes: string[]): string {
  const lines = [
    '## 🤖 AI Agent 自动性能修复',
    '',
    '### 检测到的性能问题',
  ];

  for (const issue of issues) {
    const emoji = issue.severity === 'critical' ? '🔴' : '🟡';
    lines.push(`- ${emoji} **${issue.audit.title}** (评分: ${issue.audit.score * 100})`);
    lines.push(`  - 预计节省: ${issue.estimatedSavingMs.toFixed(0)}ms`);
  }

  lines.push('', '### 执行的修复', ...changes.map(c => `- ${c}`));
  lines.push('', '---', '> 🤖 此 PR 由 AI Performance Agent 自动创建。请 Review 修复内容后合并。');

  return lines.join('\n');
}

/**
 * 主 Agent 循环
 */
async function performanceAgentMain(projectDir: string, lighthouseReportPath: string) {
  // 1. 解析 Lighthouse 报告
  const issues = parseLighthouseReport(lighthouseReportPath);
  if (issues.length === 0) {
    console.log('✅ 性能指标全部达标,无需修复');
    return;
  }

  console.log(`🔍 发现 ${issues.length} 个性能问题`);

  const allChanges: string[] = [];

  // 2. 根据问题类型执行修复
  for (const issue of issues) {
    switch (issue.type) {
      case 'oversized-images':
        console.log(`📷 修复超大图片...`);
        const imgChanges = await fixOversizedImages(projectDir);
        allChanges.push(...imgChanges);
        break;

      case 'render-blocking':
        console.log(`📋 修复阻塞渲染资源...`);
        const cssFix = await fixRenderBlocking('http://localhost:3000');
        allChanges.push(cssFix);
        break;

      case 'large-bundle':
        console.log(`📦 分析 JS Bundle...(需人工确认分割方案)`);
        allChanges.push('已标记可分割的 chunk(见 PR 评论)');
        break;

      default:
        console.log(`⏭️ 跳过 ${issue.type}(无自动修复方案)`);
    }
  }

  // 3. 创建 PR
  const prDescription = generatePRDescription(issues, allChanges);
  
  // 使用 gh CLI 创建 PR
  execSync(`git add -A`);
  execSync(`git commit -m "perf: AI Agent 自动性能修复"`);
  execSync(`git push origin HEAD:fix/ai-perf-auto-fix`);
  
  const prUrl = execSync(
    `gh pr create --title "🤖 [AI Agent] 自动性能修复" --body "${prDescription.replace(/"/g, '\\"')}" --base main`,
    { encoding: 'utf-8' }
  ).trim();

  console.log(`✅ PR 已创建: ${prUrl}`);
}

export { performanceAgentMain, parseLighthouseReport, fixOversizedImages };

四、Agent 自主修复的三个边界

边界一:不能修改业务逻辑。 Agent 可以将 PNG 转为 WebP、可以提取 Critical CSS、可以添加 loading="lazy"——这些都是"格式转换 + 属性注入"类的安全操作,不会改变页面行为。但 Agent 不能决定"这张图片的质量可以降到 50% 还是需要保持在 80%",不能决定"这个组件的渲染是否可以延迟到 requestIdleCallback"。这些涉及主观质量和用户行为预期的决策需要人工确认。

边界二:不能对生产环境未知的资源做假设。 Agent 分析的 Lighthouse 报告来自于 CI 环境——可能是无头浏览器、1x DPR、固定的 1920×1080 视口。但生产环境的用户有 80% 在移动端、2x DPR、375×812 视口。Agent 无法推断"这张图在移动端的 LCP 影响有多大",因此它的修复建议是基于"CI 环境的测量"而非"真实用户的体验分布"。需要结合 RUM 数据做交叉验证。

边界三:Agent 不能创建比"不修复"更糟的结果。 在图片压缩中,一张有复杂渐变的 PNG 转为 WebP 后可能产生压缩伪影(banding)。Agent 需要内置"视觉回归检查"——在修复后自动截图对比,如果差异热力图超过阈值,标记为"需人工审核"而不是自动合入。

五、总结

  1. AI Agent 的性能优化闭环:Lighthouse 检测 → Agent 分析瓶颈 → Agent 执行修复 → 创建 PR → 人工确认。
  2. Agent 可自主执行的修复操作:图片格式转换(PNG→WebP/AVIF)、Critical CSS 提取、图片添加 loading="lazy"。
  3. Agent 任何修复都必须创建 PR 而非直接 commit——"机器修 + 人确认"是安全底线。
  4. 图片压缩修复须附带"压缩前/后对比截图"——视觉回归检查是 Agent 修复的准入关卡。
  5. Agent 不能修改业务逻辑(如延迟加载时机、图片质量选择)——涉及用户行为假设的决策必须人工做出。
  6. CI 环境的性能数据不能等同于真实用户体验——Agent 的修复建议需结合 RUM 数据交叉验证。
  7. 图片压缩不应产生可见的质量退化——banding/伪影检测必须在 Agent 修复后自动运行。
  8. 每个 Agent 修复应在 PR 描述中附带"预计节省时间"——让 Review 者快速判断边际收益。
  9. Agent 的修复能力随 Lighthouse Audit 的扩展而扩展——Lighthouse 检测不到的瓶颈,Agent 也修不了。
  10. 终极目标:让"性能优化"从 2 天的周期事件变成 CI 流水线中的一个全自动 Step。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐