从 11 秒到 0 秒:一次扫码性能优化的实战记录
本文最后更新于 2026年7月25日 凌晨
从 11 秒到 0 秒:一次扫码性能优化的实战记录
如何让扫码页面从「扫一个等半分钟」变成「连续扫停不下来」
引子
用户扫了一个条码,页面卡住了。十秒后才反应过来——条码不在数据库里,后端逐个尝试了所有上游数据源,最终超时返回。
更糟的是,卡住的这十秒里,用户想扫下一个码,扫不进去。
这不是个例。扫码场景下,「查不到」是常态——用户扫的绝大多数商品数据库里都没有。但每次查不到都要等 10-20 秒,体验完全是灾难级的。
问题分析
先看原始的扫码流程:
1 | |
后端 /api/barcode/{code} 的逻辑是:先在本地数据库查,没找到就逐个尝试所有上游(Open Food Facts、UPCitemdb、Open Library、APIZero…),每个上游的超时是 5 秒,6 个上游串行跑下来,11-13 秒是常态。
后端改异步?当然可以,但那是大工程——需要消息队列、状态存储、WebSocket 推送。一个扫码页面没必要搞这么重。
前端的思路更简单:不让用户等就行了。

方案:Fire-and-Forget
核心思路就一句话:扫码后立即清空输入框,查询在后台跑,别阻塞用户。
改前代码
1 | |
问题一目了然:await fetch() 阻塞了后续所有操作。
改后代码
1 | |
关键变化:
handleScan()不再是async——它只做两件事:清空输入框 + 启动后台查询- 按钮不禁用,用户扫完一个码可以立即扫下一个
- 输入框立即清空并聚焦,准备接收下一个条码
- 后台查询完成后,自动更新结果区和历史列表
用户感知对比
| 时序 | 优化前 | 优化后 |
|---|---|---|
| 0s | 输入条码,点击查询 | 输入条码,点击查询 |
| 0.1s | 按钮变灰”查询中…” | 输入框清空,显示”查询中” |
| 0.5s | 页面卡住,无法操作 | 可以扫下一个码了 |
| 5s | 还在等 | 第二个码也开始查了 |
| 11s | 结果终于出来了 | 第一个码的结果出来了 |
| 12s | 可以扫下一个了 | 第二个码也快好了 |
实现细节
1. 占位行:让用户知道”码已录入”
1 | |
2. 脉冲动画:视觉上告诉用户”还在跑”
1 | |
3. 并发查询:扫多个码也不冲突
1 | |
用 Set 跟踪正在进行的查询,每个查询完成后自动从 Set 中移除。支持连续扫 3-5 个条码并行查询,互不干扰。
4. 一个坑:浏览器缓存
上线后发现用户还是卡。排查发现 scan.html 里引用 JS 没有版本号:
1 | |
浏览器缓存了旧版 JS,用户一直在跑旧代码。修复很简单:
1 | |
教训:纯前端优化改了 JS 文件,HTML 里的引用没加版本号,等于白改。
效果数据
以下数据来自 BarcodeApi 生产日志(运行于本地 GPU 服务器,2026 年 7 月实测):
| 条码 | 耗时 | 之前会怎样 |
|---|---|---|
| 9787513670869 | 11360ms | 页面卡 11 秒 |
| 9998887776665 | 7375ms | 页面卡 7 秒 |
| 6901294179608 | 2ms | 秒出(本地命中) |
关键指标不是后端耗时,而是「用户能扫下一个码的时间」——从 11 秒降到了 0 秒。
场景扩展
这个模式不止适用于扫码,任何需要等待后端查询的场景都可以用:
- 搜索建议:输入时立即显示”搜索中…”,同时发请求
- 批量导入:上传文件后立即返回”处理中”,后台跑完通知
- 报表生成:点击生成后立即显示”生成中”,结果出来再更新
核心模式就三步:
- 立即响应:用户操作后 UI 马上反馈
- 后台处理:把耗时操作丢到异步任务里
- 结果回来再更新:不阻塞,不等待
小结
这次优化最大的收获不是技术上的——fire-and-forget 不是什么新鲜概念。最大的收获是思维方式的转变:
前端性能优化的核心不是「让接口更快」,而是「让用户不等」。
11 秒的后端接口跑不了,但通过简单的 fire-and-forget 模式,让用户在 0 秒内就能继续操作。用户感知的速度,不是后端接口的响应时间,而是「我能做下一件事了」的时间。
这个道理说起来简单,但实际做的时候很容易陷进「把后端搞快」的思维定式里。有时候,不让用户等,比让后端快,更容易。