性能优化入手点清单(GitHub 文档翻译工具项目实战)

性能问题的账,最后都要算到吞吐和成本上。仓库扫描、文件拉取、模型调用全串在一起,翻译一份 README 平均要 8 分钟,单任务成本 0.2 美元,API 错误率还时不时冲到 1.5%。一开始我想的是换更大的模型、加机器,后来才发现瓶颈根本不在模型本身,而在任务编排和缓存策略。于是我把优化拆成 API、模型、数据库、前端、缓存、可观测六层,逐层定位、逐层改进,任务平均耗时从 8 分钟压到 3 分钟以内。下面把每层的做法、对比和踩过的坑写清楚。

一、优化指标

没有量化目标之前,改动全凭感觉:今天调数据库,明天改前端,谁也说不清哪个改动真正起了作用。我先把四个核心指标定下来,挂在显眼位置,后面每一层改动都对着它验收。指标选多了团队盯不过来,选少了又覆盖不住,最终只留耗时、成本、错误率、首屏四样。

指标 当前 目标
任务平均耗时 8 分钟 < 3 分钟
翻译成本/任务 $0.20 < $0.10
API 错误率 1.5% < 0.5%
LCP 2.6s < 1.5s

这套指标不是给团队看数字用的,而是给每处改动定一个回退标准:某项优化如果让成本升回去或者错误率上去了,立刻回滚重做,不用争论。

二、API 层

最初的代码是串行拉取:先扫仓库列表,再逐个文件拉取,最后逐个调翻译。仓库一多,单仓库耗时直接等于所有文件耗时的总和,这也是任务平均 8 分钟的主要来源。我对比过两种改法:给每个仓库起独立线程池,或者按文件分片并行。线程池方案连接数容易失控,最终选了分片并行,片内并发、片间用限流器兜底。

  1. 串行改并行:仓库扫描、文件拉取用 Promise.all 分片;
  2. 限流器:用 Installation 维度的令牌桶,避免 GitHub 429;
  3. 重试退避:指数 + 抖动,仅对 5xx/429;
  4. GraphQL 一次拿多文件元数据,减少请求数。

第一点落地就是这段代码,分片加限流器一起解决”并发提速但不被限速”:

const chunks = chunk(fileList, 10);
const results = await Promise.all(
  chunks.map(async (group) => {
    await limiter.wait(1); // 每片拿一个令牌
    return Promise.all(group.map(fetchFile));
  })
);

这样改完,文件拉取阶段从十几分钟降到两分钟以内。注意限流器要按 Installation 维度做键,别按仓库,否则同一个组织下多个仓库同时跑还是会撞 429。重试只对 5xx 和 429 生效,4xx 重试只会放大问题。

三、模型层

模型层的优化直接砍成本。当初我把所有段落都送进主模型,README 摘要、普通段落、代码注释一视同仁,单任务成本居高不下。我对比过按字符数分流和按段落类型分流两种思路,最终选了后者——字符数阈值对中文不友好,一句话可能 200 字,一段代码也可能只有 50 字,按字符数切不准。分级之后,普通段落走快模型,README 和 API 概念这类需要理解上下文的才走主模型。

  • 模型分级:普通段落快模型,README、API 概念用主模型;
  • 提示词缓存:把系统提示与术语表缓存到 provider;
  • 流式输出:前端边收边渲染;
  • 分段并发:5 段一组送模型,合并后渲染。

效果:单任务成本下降 45%。

这个数字是分级上线一周后对账出来的。流式输出是另一个加分项,前端边收边渲染,用户的等待体感从盯着转圈变成看着字一行行出来。分段并发要注意顺序问题:合并时按原始片段序号排序,不能依赖接口返回顺序。

四、数据库层

数据库层常见的坑是慢查询。任务详情页要按 jobId 查所有片段,一开始没加索引,数据量过了几万条之后页面明显变慢。我对比过加索引、加只读副本、上 Redis 缓存三套方案,最终选了索引加连接池调整——副本和缓存解决的是读压力,当时的瓶颈是慢查询本身,索引收益明显,成本也可控。

  • 常用查询加索引:TranslationSegment(jobId, status)、User(githubId);
  • 大字段单独存:原文与译文分离;
  • 事务短:避免长事务持锁;
  • Prisma connection pool 调到与 Postgres 最大连接匹配。

索引不是越多越好,写多读少的表加多了反而拖慢写入。大字段分离这个改动回报很直接:查询只扫片段元数据,不把整段译文拖出来,磁盘 IO 和网络开销都降了。

五、前端层

前端第一个暴露的问题是首屏加载。翻译工具是重列表、轻交互的页面,原本所有组件都走客户端渲染,首屏 JS 包很大。我对比过纯 CSR、SSR、服务端组件三种写法,最终选了 App Router 的服务端组件:列表页几乎不下载业务 JS,客户端组件按需水合。

  • App Router 默认服务端组件,列表页不下载 JS;
  • 客户端组件按需水合;
  • 图片走 next/image;
  • 字体 self-host;
  • 关键 CSS 内联,剩余 CSS 异步加载;
  • TanStack Query 缓存任务状态,避免重复请求。

图片交给 next/image 自动处理尺寸和懒加载,字体改成 self-host 后少了一次跨域请求,关键 CSS 内联、其余 CSS 异步加载。TanStack Query 缓存任务状态后,轮询接口不会因为组件重挂载而重复请求,LCP 从 2.6s 压到了 1.5s 以内。

六、缓存层

缓存是投入产出比很高的一层,但也容易做坏。我们按浏览器、CDN、Redis、数据库四个位置分别决定缓存什么、TTL 定多长。定 TTL 时踩过坑:最初翻译结果永久缓存,术语表一更新,老结果就出错。后来给翻译结果缓存加版本号,术语变更时主动失效。

层 缓存 TTL
浏览器 静态资源 7 天
CDN 静态资源 30 天
Redis Installation Token、术语表 50 分钟 / 24 小时
数据库 翻译结果 永久

这一层做完,重复任务的耗时从分钟级掉到秒级,成本几乎为零。浏览器和 CDN 的静态资源 TTL 可以拉长,因为文件名带 hash,变更后自然回源;Redis 里的 Installation Token 放短一点,术语表放长一点,前者失效代价是重新授权,后者只是多一次读取。

七、可观测层

没有埋点之前,出问题只能靠用户截图和日志大海捞针。我们把 trace_id 贯穿整个调用链,翻译任务的每个环节耗时、每段模型的 token 数都记录下来。慢调用和失败率告警按端点加用户维度区分,避免告警互相淹没;成本告警盯单日同比,超过 30% 就提醒——成本涨得太快,多半是死循环重试导致的。

  • 关键路径埋点:trace_id 贯穿;
  • 慢调用监控:超过阈值上报;
  • 失败率告警:按端点 + 用户;
  • 成本告警:单日成本同比 +30%。

八、增量与并发

并发策略决定系统上限。最初一个仓库一个任务,同一仓库短时间内多次 push 会重复翻译,白白烧钱。我们加了合并窗口:短时间内的多次 push 合并成一次任务。跨仓库并行默认 4 个,任务内片段并发默认 5 个,模型档位高的自动降低并发,避免把 provider 的配额打满。队列用 Redis Stream,消费成功 ACK 后才删消息,崩溃后还能重新消费,不会丢任务。

  • 同一仓库短时间多次 push:合并到一次任务;
  • 跨仓库并行:默认 4 个仓库并发;
  • 任务内片段:默认 5 并发,模型档位高的并发降低;
  • 队列用 Redis Stream,ACK 后再删。

九、内存与 CPU

超大仓库翻译时内存会涨。Node 默认堆只有 2GB,一次翻译上万文件的仓库,worker 进程会被 OOM 干掉。我对比过换 worker_threads 分担和调大堆内存两种方式,先用 --max-old-space-size=4096 顶住,同时把 Prisma client 做成全局单例,避免每个请求都新建连接。worker 进程数按 CPU 核数匹配,多了反而在上下文切换上浪费。

  • Node 默认堆 2GB,超大任务调 --max-old-space-size=4096;
  • Prisma client 全局单例,避免重复连接;
  • Worker 进程数与 CPU 核数匹配。

十、安全与性能的平衡

性能优化不能拿安全换。私钥解密是我纠结比较多的地方:缓存它性能好,但内存里多留一份私钥,就多一个泄露面,最终决定只在调用时解密、用完即弃。审计日志同步写会阻塞主流程,改成异步落盘。Webhook 校验一开始放在 Edge runtime,后来发现行为不稳定,挪回 Node runtime——校验逻辑一样,运行环境可控得多。

  • 私钥解密只在调用时进行,结果不缓存;
  • 审计日志异步落盘,避免阻塞主流程;
  • Webhook 校验在 Node runtime 中进行,避免 Edge runtime 性能差异。

十一、回归测试

优化的一大风险是回归。没有压测基线,任何一次改动的效果都说不清楚。我们用 k6 跑固定 QPS 看 P99,用 e2e 跑完整翻译任务对比耗时,每次 PR 检查 bundle size 和 Lighthouse 分数。这套流程不是可选项,它是守住前面所有优化成果的底线。

  • k6 跑固定 QPS,验证 P99;
  • e2e 跑完整翻译任务,对比耗时基线;
  • 每次 PR 检查 bundle size 与 Lighthouse。

十二、优化顺序

顺序错了,努力就白费。我们的顺序是监控先行、缓存次之、并行与限流、模型分级、前端体验、回归测试。理由很直接:没有数据时优化是猜测;缓存投入产出比高;并行与限流提升系统上限;模型分级砍成本;前端体验直接影响用户观感;最后用回归测试守住成果。

  1. 监控先行:没数据时优化是猜测;
  2. 缓存次之:投入产出比高;
  3. 并行与限流:提升系统上限;
  4. 模型分级:影响成本;
  5. 前端体验:用户感知最强;
  6. 回归测试:守住成果。

十三、给同类项目的建议

这几条建议是从返工里学来的。目标可量化,团队才知道改的是什么;监控先行,避免拍脑袋优化;优化是连续过程,突击式优化容易引入回归;改动前做基线、改动后做对比,才能量化收益。别的项目照搬不一定合适,但顺序大概率是通用的。

  1. 目标可量化,避免主观;
  2. 监控先行;
  3. 优化是连续过程,不要”突击”;
  4. 改动前做基线,改动后做对比。

常见问题(FAQ)

Q1:先优化 API 还是先优化模型?

看瓶颈在哪。用监控数据决定,不是猜。

Q2:缓存越多越好吗?

不是,缓存会带来一致性问题,按需配置 TTL。

Q3:性能优化要不要压测?

要。没有压测就没有 P99 数字。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
小码农的头像小码农认证作者

相关推荐

返回顶部