从 11 秒到 0 秒:一次扫码性能优化的实战记录

本文最后更新于 2026年7月25日 凌晨

从 11 秒到 0 秒:一次扫码性能优化的实战记录

如何让扫码页面从「扫一个等半分钟」变成「连续扫停不下来」

引子

用户扫了一个条码,页面卡住了。十秒后才反应过来——条码不在数据库里,后端逐个尝试了所有上游数据源,最终超时返回。

更糟的是,卡住的这十秒里,用户想扫下一个码,扫不进去。

这不是个例。扫码场景下,「查不到」是常态——用户扫的绝大多数商品数据库里都没有。但每次查不到都要等 10-20 秒,体验完全是灾难级的。

问题分析

先看原始的扫码流程:

1
用户输入条码 → 点击查询 → 按钮禁用 → fetch API → 等待10-20秒 → 显示结果 → 按钮恢复

后端 /api/barcode/{code} 的逻辑是:先在本地数据库查,没找到就逐个尝试所有上游(Open Food Facts、UPCitemdb、Open Library、APIZero…),每个上游的超时是 5 秒,6 个上游串行跑下来,11-13 秒是常态。

后端改异步?当然可以,但那是大工程——需要消息队列、状态存储、WebSocket 推送。一个扫码页面没必要搞这么重。

前端的思路更简单:不让用户等就行了。

优化前后对比

方案:Fire-and-Forget

核心思路就一句话:扫码后立即清空输入框,查询在后台跑,别阻塞用户。

改前代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function handleScan() {
const barcode = scanInput.value.trim();
if (!barcode) return;
scanBtn.disabled = true; // 按钮禁用
scanBtn.textContent = '查询中...';
try {
const res = await fetch(`/api/barcode/${barcode}`);
// ... 等待 10-20 秒 ...
} finally {
scanBtn.disabled = false; // 10 秒后才恢复
scanInput.value = '';
scanInput.focus();
}
}

问题一目了然:await fetch() 阻塞了后续所有操作。

改后代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function handleScan() {
const barcode = scanInput.value.trim();
if (!barcode) return;

// 立即清空输入框,保持聚焦,不禁用按钮
scanInput.value = '';
scanInput.focus();

// 立即在历史列表顶部插入"查询中"占位行
insertPendingRow(barcode);

// fire-and-forget:异步查询,不 await
queryBarcodeAsync(barcode);
}

async function queryBarcodeAsync(barcode) {
pendingQueries.add(barcode);
try {
const res = await fetch(`/api/barcode/${barcode}`);
const result = await res.json();
showResult(barcode, result);
} catch (error) {
updatePendingRow(barcode, 'error');
} finally {
pendingQueries.delete(barcode);
loadHistory();
}
}

关键变化:

  • handleScan() 不再是 async——它只做两件事:清空输入框 + 启动后台查询
  • 按钮不禁用,用户扫完一个码可以立即扫下一个
  • 输入框立即清空并聚焦,准备接收下一个条码
  • 后台查询完成后,自动更新结果区和历史列表

用户感知对比

时序 优化前 优化后
0s 输入条码,点击查询 输入条码,点击查询
0.1s 按钮变灰”查询中…” 输入框清空,显示”查询中”
0.5s 页面卡住,无法操作 可以扫下一个码了
5s 还在等 第二个码也开始查了
11s 结果终于出来了 第一个码的结果出来了
12s 可以扫下一个了 第二个码也快好了

实现细节

1. 占位行:让用户知道”码已录入”

1
2
3
4
5
6
7
8
9
10
11
12
function insertPendingRow(barcode) {
const container = document.getElementById('history-list');
const row = document.createElement('tr');
row.id = `pending-${barcode}`;
row.className = 'history-row pending-query';
row.innerHTML = `
<td class="history-barcode">${barcode}</td>
<td><span class="status-dot blue"></span>
<span class="history-status">查询中</span></td>
<td>-</td><td>-</td><td>刚刚</td>`;
container.insertBefore(row, container.firstChild);
}

2. 脉冲动画:视觉上告诉用户”还在跑”

1
2
3
4
5
6
7
8
9
.pending-query {
opacity: 0.7;
animation: pulse-fade 1.5s ease-in-out infinite;
}
.status-dot.blue { background: #3b82f6; }
@keyframes pulse-fade {
0%, 100% { opacity: 0.7; }
50% { opacity: 0.4; }
}

3. 并发查询:扫多个码也不冲突

1
let pendingQueries = new Set();

用 Set 跟踪正在进行的查询,每个查询完成后自动从 Set 中移除。支持连续扫 3-5 个条码并行查询,互不干扰。

4. 一个坑:浏览器缓存

上线后发现用户还是卡。排查发现 scan.html 里引用 JS 没有版本号:

1
<script src="js/scan.js">  <!-- 没有 ?v=2 -->

浏览器缓存了旧版 JS,用户一直在跑旧代码。修复很简单:

1
<script src="js/scan.js?v=2">

教训:纯前端优化改了 JS 文件,HTML 里的引用没加版本号,等于白改。

效果数据

以下数据来自 BarcodeApi 生产日志(运行于本地 GPU 服务器,2026 年 7 月实测):

条码 耗时 之前会怎样
9787513670869 11360ms 页面卡 11 秒
9998887776665 7375ms 页面卡 7 秒
6901294179608 2ms 秒出(本地命中)

关键指标不是后端耗时,而是「用户能扫下一个码的时间」——从 11 秒降到了 0 秒。

场景扩展

这个模式不止适用于扫码,任何需要等待后端查询的场景都可以用:

  • 搜索建议:输入时立即显示”搜索中…”,同时发请求
  • 批量导入:上传文件后立即返回”处理中”,后台跑完通知
  • 报表生成:点击生成后立即显示”生成中”,结果出来再更新

核心模式就三步:

  1. 立即响应:用户操作后 UI 马上反馈
  2. 后台处理:把耗时操作丢到异步任务里
  3. 结果回来再更新:不阻塞,不等待

小结

这次优化最大的收获不是技术上的——fire-and-forget 不是什么新鲜概念。最大的收获是思维方式的转变:

前端性能优化的核心不是「让接口更快」,而是「让用户不等」。

11 秒的后端接口跑不了,但通过简单的 fire-and-forget 模式,让用户在 0 秒内就能继续操作。用户感知的速度,不是后端接口的响应时间,而是「我能做下一件事了」的时间。

这个道理说起来简单,但实际做的时候很容易陷进「把后端搞快」的思维定式里。有时候,不让用户等,比让后端快,更容易。


从 11 秒到 0 秒:一次扫码性能优化的实战记录
https://normdist.com/2026/07/24/ND-20260724-001-scan-optimization-fire-and-forget/
作者
小瑞
发布于
2026年7月24日
许可协议