AI 参与说明(Agent:
/root、/root/case_outline、/root/convex_docs_style、/root/install_command_verify):本文根据 Convex Stack 案例、Convex 官方文档与官方仓库协助整理,重点核对实时订阅、单次读取、索引、CLI Insights 与 Agent Skills 的当前用法。资料核验于 2026-08-21;案例中的流量和成本数据由原作者报告,未在本文独立复现。Convex CLI、平台行为与 Skills 会持续演进,实施前应以目标 deployment、当前 CLI 帮助和文末一手资料为准。
适用范围:本文面向已经上线、出现明显读取带宽、订阅重跑或 OCC(Optimistic Concurrency Control,乐观并发控制)信号的 Convex 应用。它不是“所有项目一开始就应采用”的架构清单;小表、低流量和尚未验证需求的功能,通常应优先保持简单。
先说结论#
ClawHub 是 OpenClaw 的 Skill 仓库。Convex 团队在优化案例中报告:它在约 120 万周活用户的负载下,将数据库流量从 9 TB/日 降到约 600 GB/日。这不是“换一个 API”就能得到的收益,而是反复把读取与写入形状调整为符合实际访问模式的结果。
案例中的指标衡量对象不同,不能混成同一个 benchmark:
| 指标(均为原文报告) | 优化前 | 优化后 | 含义 |
|---|---|---|---|
| 全站数据库流量 | 9 TB/日 | 600 GB/日 | 整体读写与订阅成本约降 93%。 |
| 某个 browse query | 17 MB/次 | 约 20 KB/次 | 单条公开目录读取的带宽变化。 |
| 25 项列表及关联读取 | 约 195 KB/页 | 约 20 KB/页 | digest table 消除跨表 join 后的页面读取变化。 |
| 优化过程 | 8 轮、4 天 | — | 原文称期间没有停机。 |
可复用的结论是:优化重点不是让某个 query 的 CPU 更低,而是降低这两类乘积:
单次读取字节 × 调用量
订阅数量 × 失效频率 × 单次 query 成本因此应遵循一个小闭环:测量最贵路径 → 只修一个根因 → 部署并验证结果等价 → 回到测量数据。理解 Convex 的 read set、订阅失效和 OCC 原理,可先阅读Convex 源码导读。
1. 先找出最贵的真实路径#
案例以生产 Insights 作为起点。当前 Convex CLI 可这样查看默认 production deployment 过去 72 小时的健康信号:
# 汇总查看
npx convex insights --prod
# 显示各项 Insight 的近期事件
npx convex insights --prod --details本文以 convex@latest 的 CLI 帮助验证过上述参数。该命令仅适用于已登录用户可访问的 Convex Cloud deployment;没有足够真实流量时,结果为空并不等于路径没有风险。先运行 npx convex insights --help 核对本机版本。
| 观察到的信号 | 常见根因 | 优先检查 |
|---|---|---|
| bytes/documents read 很高 | document 过大、循环 ctx.db.get()、无界读取或读后过滤 | query 返回值、join、.collect()、.filter() 和索引 |
| 高流量页面反复重跑 query | 不必要的 reactive subscription,或 read set 被高频写入触碰 | useQuery / usePaginatedQuery 的调用点和写入源 |
| OCC retry 或 conflict | mutation 竞争同一 document 或同一小组 document | 共享计数器、状态机和 read-modify-write 热点 |
| backfill 时突发带宽或延迟 | 批量写入使大量订阅同时失效 | batch 大小、调度间隔、派生表和停止开关 |
不要从全仓库泛搜“性能坏味道”开始。先选一条排名靠前的用户路径,读清它的 query、调用页和写入源,再决定是否改变数据模型。
2. 实时不是默认目标:订阅与单次读取要匹配#
useQuery 会建立 reactive subscription:相关数据变化后,query 重新执行,React 随之更新。这非常适合协作文档、聊天室、presence 和实时看板等“旧结果会误导用户”的界面。
但公开目录、搜索结果页或下载排行榜通常是“很多读者、允许页面加载后短暂不刷新”的场景。ClawHub 的案例将此类读取从分页订阅改为单次 convex.query(),避免每一次背景写入都放大为大量重跑。
下面是一个可复现的最小示例。前提是已配置 Convex React Provider,并至少运行一次 npx convex dev 生成 convex/_generated 文件。先定义一个有界、按索引读取的公开目录 query:
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
catalogItems: defineTable({
category: v.string(),
title: v.string(),
createdAt: v.number(),
}).index("by_category_createdAt", ["category", "createdAt"]),
});// convex/catalog.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
export const listPublic = query({
args: { category: v.string(), limit: v.number() },
handler: async (ctx, args) => {
if (!Number.isInteger(args.limit) || args.limit < 1 || args.limit > 100) {
throw new Error("limit must be an integer between 1 and 100");
}
return await ctx.db
.query("catalogItems")
.withIndex("by_category_createdAt", (q) => q.eq("category", args.category))
.order("desc")
.take(args.limit);
},
});如果用户点击刷新时才需要新数据,可以用 useConvex() 进行 one-shot read:
import { useState } from "react";
import { useConvex } from "convex/react";
import { api } from "../convex/_generated/api";
type CatalogListItem = { _id: string; title: string };
export function CatalogSnapshot() {
const convex = useConvex();
const [items, setItems] = useState<CatalogListItem[]>([]);
const [loading, setLoading] = useState(false);
async function refresh() {
setLoading(true);
try {
const next = await convex.query(api.catalog.listPublic, {
category: "public",
limit: 25,
});
setItems(next);
} finally {
setLoading(false);
}
}
return (
<section>
<button disabled={loading} onClick={refresh}>
{loading ? "Loading…" : "Refresh catalog"}
</button>
<ul>{items.map((item) => <li key={item._id}>{item.title}</li>)}</ul>
</section>
);
}预期行为是:点击按钮只执行一次 api.catalog.listPublic;之后的 database 写入不会自动刷新这个组件。若 UI 必须持续接收更新,应保留 useQuery,而不是为了节省带宽牺牲产品正确性。Convex React 文档
3. 热路径只读取 UI 需要的数据#
Convex query 读取的是完整 document。若列表只显示标题、摘要、作者名和计数,却在每一行读取大版本 document、用户资料和其它关联数据,读取量与 reactive read set 都会被不必要地放大。
ClawHub 的做法是维护一个轻量的 digest table(读模型):它只保留列表页需要的字段,并把稳定的作者显示名、最新版本摘要等字段反规范化到同一条记录。这样列表 query 不需要在循环中跨表 ctx.db.get()。案例报告该页面从约 195 KB/25 条降到约 20 KB/25 条。
这里有一个常见误区:在函数返回前用 map() 选择字段,确实会缩小传给客户端的 payload,但不会减少数据库已经读取的大 source document。想降低 DB I/O,需要改变热路径读的 document 本身,例如引入 digest table、拆分高频更新字段,或消除 join。
digest table 有明确代价:写入链路必须维护它与源数据的一致性,还可能需要 schema 扩展、backfill 和未回填数据的回源 fallback。只有当观测数据证明这是一条热点、且 UI 所需字段稳定而有限时,才值得引入;不要为了“看起来更规范”给每张表复制一份读模型。
4. 用 Index 和分页消除无界扫描#
上例中的 by_category_createdAt 把筛选条件放进 .withIndex(),再用 .take() 限制结果数量。这避免了“先读全表,再在 JavaScript 中过滤”的路径。
审计时优先搜索:
- 无界的
.collect(); - query 后的
.filter(),或循环中的if (...) continue; - 需要分页的页面把多页结果长期保持为 subscription;
- 每条列表项重复执行
ctx.db.get()的 join。
对会增长的集合使用 .take() 或 .paginate();只有结果规模已知且很小的路径才适合 .collect()。复合 index 的字段顺序应服务于实际 query 的约束和排序;索引也有存储、写维护和构建成本,不是越多越好。Convex Indexes 与 Query Performance Convex Best Practices
5. 写入不仅是写入,也是订阅失效的触发源#
在 ClawHub 案例中,定时更新约 500 个 Skill 的统计数据会触发 digest 同步。即使多数统计值没有变化,重复写入仍可能让订阅者反复读取页面;案例作者报告这曾形成约 55 GB 的带宽尖峰。其修复是在更新 digest 前先比较真正会影响列表的字段,未变化就跳过写入。
应把原则与具体实现分开:
- 原则:不要让派生数据、cron 或 backfill 在没有可观察业务变化时制造大面积 invalidation。
- 实现:是否需要手写字段比较、如何组织 trigger、相同值写入在目标版本中的行为,都要以当前 deployment 的 Insights 和实际代码为准。
也就是说,不应机械复制案例中的比较逻辑;应该先证明某次写入确实触发了昂贵的订阅重跑,再用最小改动消除它。对下载计数、lastSeen 等高频字段,也要评估能否从被广泛读取的主 document 中拆出,缩小无关页面的失效范围。
6. 把 backfill 和重 mutation 当作生产操作#
新建 digest table 后的 backfill 不应一次性冲进生产。案例将 batch 分散到多个调度周期,并建议准备 stop flag,以便异常时停止后续 batch。对于会读写大量数据的工作,可以由 Action 编排“读取 → 计算 → 写入”的小步骤,避免单个 mutation 撞上事务读取限额。
代价是原子性边界会改变:一个 mutation 内的写入仍是原子的,但一个 Action 调用的多个 mutation 并不会整体作为同一事务提交。因此,涉及余额、库存、权限或其它必须一起成立的不变量时,不能只为性能随意拆分;需要先定义补偿、幂等和旧新数据共存策略。批处理的持久化边界可结合Convex Workflow:接口、事件与恢复 API 实战进一步设计。
7. 用小循环验证,而不是“优化完再看”#
案例的操作循环可以直接复用:
1. npx convex insights --prod 找到带宽或 OCC 排名最高的函数
2. 阅读这条函数及其调用页面,确认实际读写形状
3. 只修改一个根因:订阅方式、索引、读模型或写入条件
4. npx convex deploy 部署并完成既有测试
5. 回到 Insights / Dashboard 确认指标变化,再处理下一项把部署放在循环中不代表跳过审核。npx convex deploy 会 typecheck、生成代码并推送函数、索引和 schema;生产变更前仍应评估 migration 影响、准备回滚方案,并在低风险环境先验证。Convex CLI 文档
可选:把性能检查交给 Agent,但不要交出决策权#
原案例最后将经验沉淀为 Agent Skill。原来的 convex-performance-audit 已被官方替换为:
convex-advisor:读取近期 Insights,并把运行时事件对应回代码根因。convex-optimize:把评估、升级、可观测性与整改计划编排起来。
若要把当前路由入口与这两个性能相关 Skill 装到 OpenClaw 项目的 skills/ 目录,可运行:
npx skills add get-convex/agent-skills \
--skill convex convex-advisor convex-optimize \
--agent openclaw \
--yes加上 --global 会安装到 OpenClaw 的全局 Skill 目录;先运行 npx skills add get-convex/agent-skills --list 可以检查当前可用名称。不要使用 --all,除非确实要把仓库内所有 Skills 安装给所有受支持 Agent。无论使用哪种 Skill,生产 deployment 的写入、迁移和删除操作都应保留人工确认。Skills CLI 选项