用 AI 优化关键渲染路径:自动识别与消除阻塞资源
用 AI 优化关键渲染路径:自动识别与消除阻塞资源
一、Lighthouse 报告里的"Eliminate render-blocking resources"和你的无力感
Lighthouse 报告说"消除阻塞渲染的资源可节省 2.3 秒"。点开详情——3 个 CSS 文件(Google Fonts 1 个、Ant Design CSS 1 个、自定义样式 1 个)和 2 个 JS 文件在阻塞 FCP。理想方案是:内联首屏关键 CSS、异步加载非关键 CSS、对 JS 添加async/defer。但"首屏关键 CSS"包含哪几条规则?Ant Design 的全局 CSS 中有 87% 的规则在首屏根本不使用。谁来判断"该内联哪些"、"该延迟哪些"?
这就是 Critical CSS 提取的问题。手动提取"首屏需要的 CSS 规则"需要对每个页面做"在 375×812 视口下,哪些 CSS 规则被应用了?"的分析。AI 的自动化方案是通过 Puppeteer + Coverage API 收集首屏实际使用的 CSS 规则,然后自动分割为 Critical(首屏内联)和 Non-Critical(异步加载)。
二、渲染阻塞的资源分类与处理策略
flowchart TD A["HTML 解析] --> B{"遇到 <link rel=stylesheet>"} B --> C["暂停解析 → 下载 CSS → 构建 CSSOM → 恢复解析"] A --> D{"遇到 <script src> (无 async/defer)"} D --> E["暂停解析 → 下载 JS → 执行 JS → 恢复解析"] C --> F["内联 Critical CSS 消除此阻塞"] E --> G["async/defer 消除此阻塞"] subgraph 阻塞CSS处理 F1["Puppeteer 收集首屏 Coverage"] F1 --> F2["提取被应用的 CSS 规则"] F2 --> F3["内联到 <head> 的 <style> 中"] F1 --> F4["未应用的规则 → 异步加载"] end subgraph 阻塞JS处理 G1["识别首屏非必需的 JS"] G1 --> G2["添加 async / defer 属性"] G2 --> G3["确保无 DOM 依赖的脚本用 async<br/>需要 DOM 的用 defer"] end三、自动提取 Critical CSS 的实现
/** * Critical CSS 自动提取器 * * 工作流: * 1. Puppeteer 打开目标页面 * 2. 使用 CSS Coverage API 收集所有样式表的使用情况 * 3. 筛选出首屏实际应用的 CSS 规则(覆盖率 > 0%) * 4. 输出为单独的内联样式字符串 * * 依赖:puppeteer */ import puppeteer, { Browser, Page } from 'puppeteer'; interface CriticalCSSResult { /** 内联到 <head> 的首屏关键 CSS */ critical: string; /** 未使用规则百分比 */ unusedPercent: string; /** 节省的阻塞 CSS 体积 */ savedBytes: number; } async function extractCriticalCSS( url: string, viewport: { width: number; height: number } = { width: 375, height: 812 } ): Promise<CriticalCSSResult> { const browser = await puppeteer.launch(); const page = await browser.newPage(); // 设置移动端视口(匹配大多数用户的首屏尺寸) await page.setViewport(viewport); // 关键:启用 CSS Coverage await Promise.all([ page.coverage.startCSSCoverage(), page.goto(url, { waitUntil: 'networkidle0' }), ]); // 获取页面中所有样式表的使用情况 // 返回数组:[{ url, text, ranges }] // ranges: [{ start, end }] → 被实际使用的字符范围 const coverage = await page.coverage.stopCSSCoverage(); const criticalParts: string[] = []; let totalBytes = 0; let usedBytes = 0; for (const entry of coverage) { totalBytes += entry.text.length; // 跳过 data: 和 inline 样式(已经内联,无需处理) if (entry.url.startsWith('data:')) continue; // ranges 为使用了哪些字符区间 // { start: 0, end: 100 } → text[0:100] 这段 CSS 被使用了 const usedRanges = entry.ranges; for (const range of usedRanges) { const usedCode = entry.text.slice(range.start, range.end); usedBytes += usedCode.length; criticalParts.push(usedCode); } } const unusedPercent = (((totalBytes - usedBytes) / totalBytes) * 100).toFixed(1); await browser.close(); return { // 拼接所有被使用的 CSS 代码段 // 生产环境需要 postcss 处理合并后的代码(去重、压缩) critical: criticalParts.join('\n'), unusedPercent: `${unusedPercent}%`, savedBytes: totalBytes - usedBytes, }; } /** * 将 Critical CSS 嵌入 HTML * * 策略: * 1. 将 Critical CSS 内联到 <head> 的 <style> 标签中 * 2. 将原始外部 CSS 链接改为 preload(异步加载,不阻塞渲染) * 3. 在 onload 事件中切换为 stylesheet(保证后续页面有完整样式) */ function embedCriticalCSS(html: string, criticalCSS: string): string { // 提取原始的外部 CSS 链接 const linkRegex = /<link\s+[^>]*rel=["']stylesheet["'][^>]*>/gi; const links = html.match(linkRegex) || []; // 构建内联 Critical CSS 标签 const criticalStyleTag = `<style>