平台工程

前端投票场景的乐观更新实践

适用场景:需要即时反馈的投票/点赞/收藏等交互,API 调用有延迟,用户期望点击后立即看到状态变化。

问题

投票页点红心 → 调 API → 等返回 → 更新 UI。API 延迟 300-500ms 时,用户双击红心会感觉「没反应」,可能重复点击。

方案:乐观更新 + 失败回退

核心逻辑

async function selectVote(submissionId) {
  const previousVote = state.vote;       // ① 保存旧值
  state.vote = submissionId;             // ② 乐观更新
  renderVoteWorks();                     // ③ 立即渲染
  try {
    const result = await api.castVote(submissionId);
    state.vote = result.battle.myVoteSubmissionId; // ④ 以后端为准
  } catch (error) {
    state.vote = previousVote;           // ⑤ 失败回退
    renderVoteWorks();
    showToast(error.message);
  }
}

关键设计决策

  1. 保存旧值而非硬编码 null:如果用户之前已投过作品 A,现在改投作品 B 但失败,应该恢复到 A 而不是清空。previousVote 保存的是”点击之前的状态”。

  2. 以后端返回值为准:即使本地乐观更新成功,服务端返回的 myVoteSubmissionId 可能因竞态条件与本地不同(比如另一个标签页同时改了投票)。第④步用后端值覆盖本地值,保证最终一致性。

  3. 失败回退不是「静默失败」:catch 里同时调了 renderVoteWorks() 回退 UI 和 showToast() 告知用户。只回退不提示,用户不知道操作失败了;只提示不回退,UI 和实际状态不一致。

配合服务端持久化

乐观更新只能解决「点击 → 看到反馈」的延迟,不能解决「刷新 → 状态丢失」。需要服务端同时支持:

这样刷新后红心仍然亮着,乐观更新的体验闭环才算完整。

边界

来源

PB Arena BUG-03 投票阶段乐观更新实践,commit 1b0a90e,2026-08-11。