Community研究与数据分析github.com

overfetch:请求审计

审计前端应用与后端之间的通信,生成一份带有证据的排序报告,指出请求放大问题并提供具体修复方案——包括逐行的N+1读取、重复获取、堆叠的轮询定时器、零新鲜度、全局无效化、串行瀑布流、全量获取的分页、过度获取、重试浪费。当有人询问为何应用发出过多请求、需要进行网络/请求/数据获取/API调用审计、或提及N+1、瀑布流、轮询、重新获取风暴、速率限制时,都可使用此技能。

overfetch:请求审计 是什么?

overfetch 是一个专门用于审计前端应用后端请求效率的技能。它通过结合静态代码扫描和动态运行捕获,自动生成一份有证据支持的排序报告,明确指出请求放大问题的根因、具体修改方案以及预期效果。该技能支持 TanStack Query、SWR、Apollo 和 RTK Query 等主流数据获取库,并针对每种库提供了详细的语义检查参考。工作流程包括:通过覆盖率地图(coverage_map)识别潜在问题区域、使用静态扫描(static_scan)生成候选问题、基于捕获数据(capture)验证实际行为,最终按照增长等级而非绝对数量对问题进行排序(P0 表示随行数、挂载组件或时间无限增长的问题)。此外,它还能识别扫描无法发现的跨切面问题,例如查询键的唯一性、轮询所有权、并发天花板和重试策略等,并提供详细的非发现记录以避免重复调查。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/alii13/overfetch/tree/HEAD

Installed? Explore more 研究与数据分析 skills: affaan-m/uspto-database, affaan-m/scholar-evaluation, affaan-m/literature-review · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

overfetch:请求审计 是做什么的?

你正在生成一份可供他人操作执行的报告:一份排序列表,列出该应用相对于屏幕实际需求发出更多请求的方式,每条附带证据、修复方案以及预期减少的数量。报告只有在每项发现都能经受住怀疑读者的审视时才是有用的,因此以下方法将大部分精力投入到验证上,并明确说明哪些已检查、哪些未检查。

交付内容

一份 Markdown 报告(模板见 references/report-template.md),包含:

  1. 范围和证据:哪个代码树、哪个提交、哪些捕获文件。
  2. 覆盖率:每条路由、每个懒加载块和每个门控数据站点,注明哪些仅在代码中读取,哪些在捕获中已执行。
  3. 发现项:按请求数量增长等级排序,每项附带 file:line 或捕获证据、根本原因、修复模式、预期变化、工作量。
  4. 非发现项:已检查过并排除的模式及原因。这些可避免其他人再次重复调查。
  5. 开放的产品决策和后端需求:需要人工决策或 API 变更(而非客户端补丁)的事项。
  6. 限制:审计无法衡量的内容。

审计完成后可提供修复实施服务,每个发现单独一个 PR,按严重级别从高到低处理。审计过程中不开始修复。

原则

正则匹配结果是候选,不是发现。 scripts/static_scan.py 读取文本。它无法区分 3 行的标签页条和 3000 行的表格,也无法区分批量导入后故意的根无效化与懒惰的无效化。阅读每个你打算报告的候选背后的代码。说明你阅读了哪些内容。

实测优于声称。 运行中应用的捕获是请求数量的真相;代码是原因真相。注释、文档和旧审计记录是声称。当文档说“60 秒内新鲜”而代码说 staleTime: Infinity 时,报告代码,指出矛盾,并记录你读取的提交哈希。

在归咎于库之前,先了解其真实语义。 代价最高的错误是自信但错误的声称:“缓存去重了”但实际上键不同,“轮询在空闲时暂停”但实际上仅隐藏时暂停,“三个调用者,每分钟三次获取”但实际上库会重新同步同键的定时器并共享获取。references/library-facts.md 包含 TanStack Query、SWR、Apollo 和 RTK Query 的事实,并附有检查来源路径。在编写任何关于缓存或轮询的句子之前,请阅读所使用的库的相关部分;当一项判断取决于对库的声称时,请打开已安装库的源代码并确认。

按增长排序,而非按数量排序。 随行数、挂载组件或时间增长的读取比固定重复更严重。下面的严重性评分标准是关于增长类别的。

覆盖率有分母。 捕获只看到运行时加载的代码:有人访问的路由、有人打开的对话框和标签页、已启用的标志、后端恰好返回的状态。懒加载块、门控屏和错误路径正是请求 Bug 隐藏的地方,因为在开发过程中没人关注它们。因此每次审计都从完整表面开始(coverage_map.py),每个“无问题”都限定在已覆盖的范围,报告会说明哪些部分已实际执行、哪些仅被读取。

说明什么是正确的。 每行一个去重预取的悬停预取是非发现项。把它记录下来。只列出问题的报告会让读者怀疑还遗漏了什么。

数字来自某处。 报告中的每个数量都应追溯到捕获文件或代码路径。不要凭直觉编造“6+”或“约 100”;如果无法测量,就说“未测量”,并给出公式(行数 × 每行读取次数)。

工作流程

事先告知用户所需时间。中型应用的静态审计需 15 到 30 分钟阅读。添加实时捕获会增加 15 到 45 分钟,具体取决于需要测试的重要路径数量。

1. 确定范围

  • 从 package.json 中找到应用根目录和数据请求库(扫描会同时输出两者)。Monorepo:分别扫描每个应用。
  • 找到数据层:查询键工厂、传输/服务模块、QueryClient 构造(或 SWR 配置、Apollo 客户端、RTK createApi)。首先读取默认值;它们设定了每项发现衡量的基线。
  • 列出重要的界面:通常是登录后的着陆路由、最繁忙的列表,以及任何长时间保持打开状态的界面(shell、监控页面)。若不明确则询问用户;否则默认选择以上三类并说明。
  • 记录提交:git rev-parse HEAD。报告每一行都引用此版本。
  • 确定是否可以进行实时捕获:正在运行的开发服务器加可操作的浏览器,或用户已有的 HAR 文件。如果两者都不可用,审计仅限静态,报告必须在第一段中说明。

2. 覆盖率地图

python3 <skill-dir>/scripts/coverage_map.py <app-root> --format md --out coverage.md

这是分母。它列出每条路由(针对 TanStack Router、Next.js、Remix、SvelteKit 和 SolidStart 使用基于文件的路由器;代码定义的路由器使用路径字面量;其他情况使用入口文件)、每个动态 import() 边界及其后的读取、每个由标志、平台、权限或环境检查门控的数据模块、源中的每个端点路径字面量,以及无路由可达的数据模块。在 2000 个文件的应用上几秒钟内即可运行完成。

三种使用方式:

  • 规划。底部的捕获计划命名了读取站点最多的路由、要触发的懒加载块以及要切换的门控。用它来选择第 6 步中的路径,而非猜测。
  • 限定扫描范围。如果某个发现在无路由可达的模块中,则可能是死代码或由地图无法解析的方式访问。在判定其为活跃代码前先检查。
  • 闭环。捕获后,结合捕获文件重新运行地图;它会标记访问了哪些路由、加载了哪些模块和懒加载块(对于开发服务器捕获来说是精确的)、运行了哪些门控模块、出现了哪些端点路径:
python3 <skill-dir>/scripts/coverage_map.py <app-root> --har home.har --har library.har --visited home.har.visited.txt --format md

重复捕获直到覆盖大多数读取站点的路由和懒加载块,否则记录未覆盖的原因(无测试数据、缺少所需角色、无法运行的平台)。

3. 静态扫描

python3 <skill-dir>/scripts/static_scan.py <app-root> --format md --out scan.md
python3 <skill-dir>/scripts/static_scan.py <app-root> --format json --out scan.json

扫描速度很快(2000 个文件约需十秒),仅需 Python 3.9+。它输出候选问题,附带严重性、置信度、触发的检测器、file:line、重要性、修复模式以及“首先验证”说明。完整阅读 scan.md;JSON 用于脚本和评估框架。

对默认未覆盖的生成树添加 --exclude;当列表过长且希望首轮更紧凑时添加 --min-confidence medium。

4. 验证候选

按以下顺序处理候选:按置信度 P0,然后 P1,其余在后。对于每个保留的候选,必须已打开文件并能够回答:

  • 每行或每个观察者的乘数是什么,以及它的边界是什么?(固定枚举上的 .map 不是 N+1。分页列表的行数是。)
  • 缓存是否已经吸收了它?(同键、热数据、enabled: false 读取、父组件已获取的对象。)
  • 是否存在客户端可用的批量或投影端点?读取 API 客户端或 OpenAPI 规范。如果不存在,修复是后端需求,发现项保留并标记为“backend-blocked”。
  • 修复模式是什么(按名称,来自 references/catalog.md)?

剔除未通过这些检查的候选,并保留那些读者可能会合理提出的被剔除候选:它们将被放入非发现表,附上一行原因。

每个检测器的目录条目列出了特定的非发现条件。针对扫描中出现的每个检测器 ID 阅读该条目;它按相同 ID 组织。

5. 扫描器无法完成的跨切面检查

这些需要阅读而非搜索。全部完成;大多数审计至少会发现一个。

  • 键的唯一性。选择五个最常用的端点,并找到读取每个端点的所有查询键。一个端点的两个键意味着两个缓存条目和两次获取。特别注意不同投影(全字段 vs 仅名称)下的列表读取,以及范围差异(组织 vs 工作空间)实际上并非不同数据的情况。
  • 共享的进行中读取。同一帧内挂载的两个组件且键相同,仅当键在该时刻完全相同时才共享一个请求。键基于稍后才会稳定的状态(来自另一个查询的 ID)构建会产生两次获取:一次带 undefined,一次带真实值。检查依赖键上的 enabled 门控。
  • 每个界面的新鲜度。制作一个表,列出每个数据族(标识、列表、详情、实时状态)的 staleTime,并与每个数据实际变化的速度进行比较。常见的错误是单个全局值对所有数据都不合适。
  • 轮询所有权。对于共享钩子或选项工厂中的每个轮询选项,列出可能同时挂载的组件以及每个组件使用的键。在 SWR 和 Apollo 中,每个实例运行自己的定时器,获取次数会乘数增长,除非落入去重窗口。在 TanStack Query 中,定时器在每次查询更新时重新启动并共享一次获取,因此同键观察者按最短间隔计算一次获取;堆积的问题体现在不同键或客户端之间,发现项是节奏所有权。在 RTK Query 中,每个缓存条目有一个定时器,按最短间隔运行。
  • 焦点和可见性。在此平台上什么会触发焦点重新获取?桌面 shell(Tauri、Electron)通常连接 OS 窗口焦点,触发频率远高于标签页可见性。哪些轮询在隐藏时暂停,哪些在用户空闲于可见屏幕时继续运行?
  • 并发上限。是否存在扇出读取的限制器,以及它是什么?注意,6 个并行请求的限制器不等于每秒 6 个请求的限制;不要混淆两者。
  • 分页契约。对于未知总数的集合,客户端是在页面返回较短结果时停止,还是继续请求直到空页?检查页面大小与屏幕可显示的内容。
  • 重试。客户端在 403/404/422 时如何处理?库默认重试三次;一个包含被禁止块的 100 行 N+1 将变成 400 个请求。
  • 持久化。如果缓存被持久化(IndexedDB、localStorage、原生存储),maxAge 是什么,什么会在部署时清除它,存储是否按平台区分?持久化缓存会改变捕获中“冷加载”的含义;说明你测量了哪种。
  • 变更操作。对于最常用的五个写入操作,哪些被无效化了?小写入后的根无效化是常见的发现。

6. 实时捕获

如果能够运行应用或用户提供了捕获文件,获取数字。references/capture.md 包含要运行的路径、状态矩阵(空、有数据、错误、慢、离线、空闲、隐藏、门控、冷和热缓存)、捕获方式(DevTools HAR、Playwright、捆绑的 scripts/capture.mjs 或应用自身的请求日志),以及如何保持捕获的可比性。

根据覆盖率地图的计划选择路径,而非凭习惯。优先使用可运行的开发服务器:其捕获会命名加载的每个模块,因此地图可以精确说明哪些懒加载块和门控模块运行了。然后结合捕获文件重新运行地图(第 2 步),并要么捕获缺失部分,要么将其列入限制部分。

捕获显示发生了什么,而不解释原因。在仅捕获的审计中,在每个因果从句处添加条件性措辞(“与两个查询键一致”、“可能是重新挂载”),而不是在开头一次性添加免责声明:读者会引用单个句子,一个未加条件性措辞的“因为”会绕过它的免责声明传播。

当只有别人制作的捕获时,你无法仅从捕获中看出遗漏了什么。说明这一点,列出单个会话通常遗漏的内容(未访问的路由、未打开的对话框和标签页、标志和角色变体、错误和空状态、空闲和隐藏标签页行为),并推荐可弥补缺失的捕获。

python3 <skill-dir>/scripts/har_analyze.py capture.har --format md --out capture.md

它报告每个页面的 API 请求数、基于相同路径模板的突发行、包含每个键堆叠定时器数量的轮询、窗口内的重复(POST 使用 body 哈希)、串行链、重复错误以及最大的响应。静态资产被排除;使用 --ignore 忽略遥测主机。

阅读时注意两件事:

  • 同一 URL 的两次 POST 仅在 body 哈希匹配时才是重复。如果未捕获 body,工具会标记“未验证”;请按此方式报告。
  • 串行链只有在后续请求不需要前一个请求的任何数据时才构成瀑布流。工具发现形状;你在代码中确认依赖关系。

7. 调和

将每个静态发现与捕获该界面的捕获证据配对。当它们不一致时,以捕获为准确定数量,以代码为准确定原因。当捕获显示放大但无候选解释时,继续阅读代码直到找到原因;这通常是第 4 步中键的唯一性或依赖键问题。

如果仓库包含先前的审计或性能文档,请对照此提交的代码检查其中与你发现相关的主张,并在报告中列出矛盾之处。过时的主张是同一 Bug 被“修复”两次的原因。

8. 排序与编写

使用以下评分标准:

严重性增长类别典型形态
P0无上限增长:随行数、挂载的观察者或时间增长分页列表上的每行查询、共享钩子中每个实例的轮询定时器(SWR、Apollo、自定义)、每次按键无效化、未清除的间隔
P1热路径上的固定乘数每个界面读取的列表的 staleTime 0、每次焦点重新获取、无缓存的效果驱动获取、未绑定……

…(内容被截断)

相关技能