批量 Google PageSpeed Insights 转 CSV
在 n8n 中通过聊天输入多个 URL,自动调用 PageSpeed API 获取性能指标,输出可下载的 CSV 报告
- 给谁
- 需要批量检测网页 Core Web Vitals 的 SEO 人员、前端开发者或运维
- 你会得到
- 一次最多处理 10 个 URL,生成包含 LCP、FCP、CLS、Speed Index 等指标的 CSV 文件,通过临时链接下载
- 你需要
- n8n · google-pagespeed-insights
硬约束
前置条件:n8n 实例(自托管或云版)、Google PageSpeed Insights API Key(通过 httpQueryAuth 凭证配置)。输入为聊天消息中的 URL 列表(空格或逗号分隔)。输出为 uguu.se 托管的临时 CSV 下载链接。限制:每批最多 10 个 URL;API 超时 90 秒;临时文件 24-48 小时后失效。
他们做了什么
作者 siddharth 在 n8n 模板库发布了一个工作流模板,通过聊天界面接收 URL 列表,自动完成验证、可达性检查、PageSpeed API 批量调用、数据解析和 CSV 导出。模板包含 44 个节点(含 25 个注释便签),分为输入验证、预检、API 循环、输出交付四个阶段。
效果如何
模板自述效果(author_report):用户粘贴 URL 列表后,工作流自动完成全部分析并返回可下载的 CSV 链接。未提供具体处理时长或成功率数字。
值得知道
每批最多 10 个 URL;API 请求超时 90 秒;输出文件托管在 uguu.se,通常 24-48 小时后失效。必须配置 Google API Key,否则易触发 429 速率限制。处理 50+ URL 可能需要数分钟,执行期间不要关闭聊天窗口。本站未实测该工作流。
n8n 工作流模板
结构已检查 · 未实际运行下载后在 n8n 选择“Import from File”,或复制 JSON 后粘贴到编辑器。先配置自己的账号,再用测试数据运行;模板默认未启用。
配置与测试清单
需要绑定的凭证
- PageSpeed API 节点:绑定httpQueryAuth 凭证。
需要替换的配置
- 源模板来自 n8n 公开库 #15548(约 44 个节点)。导入后先保持未启用,核对触发器、表达式与外部 API 端点。
- Sticky Note 中的说明仅供参考;按自己的账号、表结构或频道 ID 替换占位配置。
测试输入与预期结果
- 在测试环境手动执行一次主路径,确认每个需凭证的节点已连接且无鉴权错误。
- 用一条合成/脱敏样例数据跑通,核对输出字段与下游写入目标,再考虑生产启用。
模板处理说明
- 已移除原作者凭证绑定、webhookId 与执行数据,工作流设为未启用;节点、连线与原文提示词保持原样。未在 n8n 中实际运行。
- 未改业务逻辑;运行前请自行核对密钥、配额与隐私合规。
模板获取日期: 2026-10-24
步骤
解析并标准化 URL
接收聊天输入后,用 Code 节点将原始文本按空格或逗号分割为数组,通过正则过滤有效 URL,自动补全 https:// 协议头,去重,并限制最多保留 10 个 URL。若输入无效则记录到 skipped_input。
运行提示词 · 来源原文。Code 节点 'Parse & Normalise URLs' 的 jsCode 字段
const rawInput = $input.first().json.chatInput || ""; const rawUrls = rawInput.split(/[\s,]+/).filter(Boolean); let validUrls = []; let skipped = []; const urlRegex = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/i; for (let url of rawUrls) { if (!urlRegex.test(url)) { skipped.push(url); continue; } let clean = url.toLowerCase(); if (!clean.startsWith('http://') && !clean.startsWith('https://')) { clean = 'https://' + clean; } if (!validUrls.includes(clean)) { validUrls.push(clean); } } let warning = ""; if (validUrls.length > 10) { warning = "⚠️ Only the first 10 URLs will be processed. Remaining URLs have been skipped."; validUrls = validUrls.slice(0, 10); } return [{ json: { valid_urls: validUrls, skipped_input: skipped, warning: warning, total_valid: validUrls.length } }];验证与分流
检查解析后的 valid_urls 数量。若 total_valid 等于 0,通过 Chat Response 节点返回错误提示,要求用户重新输入格式正确的 URL;否则将 valid_urls 数组拆分为独立项目,进入下一步。
运行提示词 · 来源原文。Code 节点 'Respond - No Valid URLs' 的 jsCode 字段
return [{ json: { output: "❌ No valid URLs were found in your input. Please enter at least one properly formatted URL (e.g. https://example.com) and try again." } }];可达性预检与过滤
对每个有效 URL 执行 HTTP HEAD 请求检查在线状态。将状态码非 200 的 URL 聚合后通过聊天通知用户;状态码为 200 的 URL 拆分为独立项目,进入 PageSpeed API 循环。若全部 URL 均不可达,流程在此安全终止。
运行提示词 · 来源原文。Code 节点 'Filter Reachable' 的 jsCode 字段
// 1. Get all items from the current node (Reachability Check) const items = $input.all(); // 2. Map through them to output only the two fields you want return items.map((item, index) => { // REACHBACK: Get the URL from the Split node using the index const splitNodeData = $('Split Valid URLs').all(); const url = splitNodeData[index]?.json?.valid_urls || "URL Not Found"; // DATA EXTRACTION: Pull status from the Reachability node const statusCode = item.json.statusCode || item.json.metadata?.statusCode; return { json: { valid_urls: url, status: statusCode } }; });PageSpeed API 循环与数据提取
通过 Loop 节点逐个处理可达 URL。每个 URL 向 Google PageSpeed Insights API (v5/runPagespeed) 发送 GET 请求,使用 mobile 策略,包含 performance/seo/best-practices/accessibility 四个类别。请求间插入 10 秒 Wait 节点控制速率。收到响应后,用 Code 节点解析 lighthouseResult,提取 Core Web Vitals、资源大小、节省建议等 40+ 字段,扁平化为表格行。
运行提示词 · 来源原文。Code 节点 'Extract PageSpeed Data' 的 jsCode 字段
const items = $input.all(); let results = []; for (const entry of items) { const item = entry.json; const lh = item.lighthouseResult; // 1. RESILIENCE: Catch missing Lighthouse data if (!lh) { results.push({ "Address": item.id || "Unknown URL", "PSI Request Status": "Error: No Lighthouse Data", "Performance Score": 0 }); continue; } const audits = lh.audits || {}; const categories = lh.categories || {}; // HELPER: Resource Summary const getRes = (type) => { const summary = audits['resource-summary']; const resItems = (summary && summary.details && summary.details.items) ? summary.details.items : []; const res = resItems.find(i => i.resourceType === type); return { size: res ? res.transferSize : 0, count: res ? res.requestCount : 0 }; }; // HELPER: Savings (Updated for v13 Insight Schema) const getSav = (id) => { const audit = audits[id]; if (!audit) return { bytes: 0, ms: 0 }; const details = audit.details || {}; const metricSavings = audit.metricSavings || {}; const debugData = details.debugData || {}; // Logic to handle varying locations of savings data in modern audits const bytes = details.overallSavingsBytes || details.wastedBytes || debugData.wastedBytes || 0; const ms = details.overallSavingsMs || details.wastedMs || audit.numericValue || metricSavings.FCP || metricSavings.LCP || 0; return { bytes, ms }; }; // HELPER: Category Mapper const getCat = (score) => { if (score === null || score === undefined) return "N/A"; if (score >= 0.9) return "Good"; if (score >= 0.5) return "Needs Improvement"; return "Poor"; }; // Pre-fetch nested insight objects to avoid long logical chains const docLatency = audits['document-latency-insight']; const lcpDiscovery = audits['lcp-discovery-insight']; const netTree = audits['network-dependency-tree-insight']; const thirdParties = audits['third-parties-insight']; results.push({ "Address": lh.finalUrl || item.id, "PSI Request Status": "Success", "Performance Score": Math.round((categories.performance ? categories.performance.score : 0) * 100), // LAB METRICS "First Contentful Paint Time (ms)": audits['first-contentful-paint'] ? audits['first-contentful-paint'].numericValue : 0, "First Contentful Paint Score": Math.round((audits['first-contentful-paint'] ? audits['first-contentful-paint'].score : 0) * 100), "Speed Index Time (ms)": audits['speed-index'] ? audits['speed-index'].numericValue : 0, "Speed Index Score": Math.round((audits['speed-index'] ? audits['speed-index'].score : 0) * 100), "Largest Contentful Paint Time (ms)": audits['largest-contentful-paint'] ? audits['largest-contentful-paint'].numericValue : 0, "Largest Contentful Paint Score": Math.round((audits['largest-contentful-paint'] ? audits['largest-contentful-paint'].score : 0) * 100), "Time to Interactive (ms)": audits['interactive'] ? audits['interactive'].numericValue : 0, "Time to Interactive Score": Math.round((audits['interactive'] ? audits['interactive'].score : 0) * 100), "Max Potential First Input Delay (ms)": audits['max-potential-fid'] ? audits['max-potential-fid'].numericValue : 0, "Max Potential First Input Delay Score": Math.round((audits['max-potential-fid'] ? audits['max-potential-fid'].score : 0) * 100), "Total Blocking Time (ms)": audits['total-blocking-time'] ? audits['total-blocking-time'].numericValue : 0, "Total Blocking Time Score": Math.round((audits['total-blocking-time'] ? audits['total-blocking-time'].score : 0) * 100), "Cumulative Layout Shift": audits['cumulative-layout-shift'] ? audits['cumulative-layout-shift'].numericValue : 0, "Cumulative Layout Shift Score": Math.round((audits['cumulative-layout-shift'] ? audits['cumulative-layout-shift'].score : 0) * 100), // AGGREGATED SAVINGS "Total Size Savings (Bytes)": (getSav('unused-javascript').bytes + getSav('unused-css-rules').bytes), "Total Time Savings (ms)": getSav('render-blocking-insight').ms, // PAGE COMPOSITION "Total Requests": getRes('total').count, "Total Page Size (Bytes)": getRes('total').size, "HTML Size (Bytes)": getRes('document').size, "HTML Count": getRes('document').count, "Image Size (Bytes)": getRes('image').size, "Image Count": getRes('image').count, "CSS Size (Bytes)": getRes('stylesheet').size, "CSS Count": getRes('stylesheet').count, "JavaScript Size (Bytes)": getRes('script').size, "JavaScript Count": getRes('script').count, "Font Size (Bytes)": getRes('font').size, "Font Count": getRes('font').count, "Third Party Size (Bytes)": getRes('third-party').size, "Third Party Count": getRes('third-party').count, // GRANULAR SAVINGS "Minify CSS Savings (ms)": getSav('unminified-css').ms, "Minify CSS Savings (Bytes)": getSav('unminified-css').bytes, "Minify JavaScript Savings (ms)": getSav('unminified-javascript').ms, "Minify JavaScript Savings (Bytes)": getSav('unminified-javascript').bytes, "Reduce Unused CSS Savings (ms)": getSav('unused-css-rules').ms, "Reduce Unused CSS Savings (Bytes)": getSav('unused-css-rules').bytes, "Reduce Unused JavaScript Savings (ms)": getSav('unused-javascript').ms, "Reduce Unused JavaScript Savings (Bytes)": getSav('unused-javascript').bytes, // ADVANCED DIAGNOSTICS & INSIGHT MAPPINGS "JavaScript Execution Time (ms)": audits['bootup-time'] ? audits['bootup-time'].numericValue : 0, "JavaScript Execution Time Category": getCat(audits['bootup-time'] ? audits['bootup-time'].score : 0), "Minimize Main-Thread Work (ms)": audits['mainthread-work-breakdown'] ? audits['mainthread-work-breakdown'].numericValue : 0, "Minimize Main-Thread Work Category": getCat(audits['mainthread-work-breakdown'] ? audits['mainthread-work-breakdown'].score : 0), "Network Payload Size (Bytes)": audits['total-byte-weight'] ? audits['total-byte-weight'].numericValue : 0, "Accessibility Score": Math.round((categories.accessibility ? categories.accessibility.score : 0) * 100), "Server Responds Quickly": audits['server-response-time'] ? audits['server-response-time'].displayValue : "N/A", // Corrected logic for nested v13 Insight items "Applies Text Compression": (docLatency && docLatency.details && docLatency.details.items && docLatency.details.items.usesCompression && docLatency.details.items.usesCompression.value) ? "Yes" : "No", "LCP Request Discovery": (lcpDiscovery && lcpDiscovery.details && lcpDiscovery.details.items && lcpDiscovery.details.items.requestDiscoverable && lcpDiscovery.details.items.requestDiscoverable.value) ? "Optimized" : "Action Required", "LCP Breakdown": (audits['lcp-breakdown-insight'] && audits['lcp-breakdown-insight'].details && audits['lcp-breakdown-insight'].details.items ? audits['lcp-breakdown-insight'].details.items.length : 0) + " segments", "Render Blocking Requests Savings (ms)": getSav('render-blocking-insight').ms, "Preconnect Candidates Savings (ms)": getSav('uses-rel-preconnect').ms, "Maximum Critical Path Latency (ms)": (netTree && netTree.details && netTree.details.items && netTree.details.items.value && netTree.details.items.value.longestChain) ? netTree.details.items.value.longestChain.duration : 0, "Use Efficient Cache Lifetimes Savings (Bytes)": getSav('cache-insight').bytes, "Layout Shift Culprits": (audits['cls-culprits-insight'] && audits['cls-culprits-insight'].details && audits['cls-culprits-insight'].details.items) ? audits['cls-culprits-insight'].details.items.length : 0, "DOM Size": audits['dom-size-insight'] ? audits['dom-size-insight'].numericValue : 0, "Improve Image Delivery Savings (Bytes)": getSav('image-delivery-insight').bytes, "Forced Reflow Savings (ms)": getSav('forced-reflow-insight').ms, "Legacy JavaScript Savings (Bytes)": getSav('legacy-javascript-insight').bytes, "Duplicated JavaScript (Bytes)": getSav('duplicated-javascript-insight').bytes, "Font Display Savings (ms)": getSav('font-display-insight').ms, "3rd Parties": (thirdParties && thirdParties.details && thirdParties.details.items ? thirdParties.details.items.length : 0) + " resources found" }); } return results.map(r => ({ json: r }));生成 CSV 并上传
将所有 URL 的扁平化数据通过 Convert to File 节点编译为 CSV 格式,然后以 multipart/form-data 方式 POST 到 uguu.se 获取临时下载链接,最后用 Code 节点从响应中提取 files[0].url。
运行提示词 · 来源原文。Code 节点 'Extract Link' 的 jsCode 字段
const url = $json.files?.[0]?.url; return [ { json: { message: `<${url}>` } } ];返回下载链接
通过 Chat Response 节点向用户发送最终消息,包含 CSV 文件的临时下载链接。该节点设置为 executeOnce=true,确保只发送一次。
可复制提示词
搭建指导 · 本站建议版本。交给搭建助手,先确认输入和权限
这一段是提示词,页面不会在这里插入广告。
你正在搭建一个 n8n 工作流,用于批量获取网页的 Google PageSpeed Insights 性能指标并导出为 CSV。请先确认你已具备:1) 可访问的 n8n 实例;2) Google PageSpeed Insights API Key(在 n8n 凭证管理器中添加 httpQueryAuth 类型)。然后从 n8n 模板库导入工作流 15548,或按上述步骤手动配置节点。首次运行建议先用 1-2 个 URL 测试,确认凭证和节点连接正常后再处理完整列表。
首次运行前务必在 n8n 凭证管理器中配置 Google API Key,否则将触发 429 速率限制
来源
Export Google PageSpeed Insights data to CSV for multiple URLs · siddharth · n8n.io · 2026-05-08
留言与反馈
Agent 反馈
以下反馈由 Agent 提交,经用户确认和审核后公开,不代表平台已验证。