
一句话:场景设计题考的不是「你会不会写代码」,而是「你能不能在信息不全时先澄清、再把问题拆成「已有知识的组合」、然后给出「有取舍的方案」」——所以答题的骨架永远是五步:澄清约束 → 拆成子问题 → 给决策与理由 → 说落地细节 → 主动讲权衡。
一、答题骨架(⭐ 五步,适用于任何场景题)
⭐⭐⭐ 「场景设计题」的五步骨架:
① 【澄清(⭐ 最关键、90% 的人会跳过)】
问清「约束」:规模(多少数据/多少用户)、频率(多久更新一次)、
环境(移动端?弱网?老浏览器?)、团队(几人?能否维护复杂方案)、
「成功的定义」(⭐ 「快」是指「首屏」还是「交互」?)
⭐ 为什么要澄清:「场景题的答案完全取决于约束」——
「无限滚动」在「100 条」和「100 万条」是「两种实现」。
② 【拆成子问题】
⭐ 「渲染层(怎么画)」「数据层(怎么取/存)」「交互层(怎么响应)」
「性能层(瓶颈在哪)」「工程层(怎么扩展/维护)」
⭐ 好处:每个子问题「都能落到你已有的知识上」(⭐ 这就是「考察点」)。
③ 【给决策 + 理由 + 替代方案】
⭐ 「我选 A,因为…;B 也能做,但代价是…」——
⭐ **「有取舍」才是「设计」,只说一种方案是「背 API」**。
④ 【落地细节(⭐ 展示「真做过」)】
「具体到哪个 API、哪个参数、哪个坑」
⑤ 【主动讲权衡与风险(⭐ 加分项)】
「这个方案在「X 情况」下会退化,那时我会…」
⚠️ 三个「不要」:
❌ 「一上来就写代码」(⭐ 没澄清就写 = 「赌需求」)
❌ 「只给一种方案」(⭐ 无取舍 = 无设计)
❌ 「不提「不可行的情况」」(⭐ 显得「没踩过坑」)
二、场景 1:无限滚动的信息流
【澄清清单】
· 数据量级?(⭐ 100 条 vs 100 万条 → 完全不同的实现)
· 单条内容的高度「固定吗」?(⭐ 决定「能否用 getItemLayout / 精确虚拟化」)
· 需要「回到之前的位置」吗?(⭐ 决定「要不要缓存已渲染项」)
· 交互频率?(⭐ 「每屏都要请求」还是「预加载 3 屏」)
· 移动端?(⭐ 「滚动性能 + 内存」更敏感)
· 需要「实时插入」(如「新消息进来」)吗?(⭐ 会「打断滚动」)
【拆解与决策】
① 渲染层:
· ⭐ **虚拟滚动**(只渲染视口附近的 N 项)—— ⭐ 这是「1000+ 条」的唯一解
(⭐ Web 用 `react-window`/`@tanstack/virtual`;RN 用 `FlatList` 的 `windowSize`,
见 10.15)
· ⚠️ 「不定高」时虚拟滚动的成本高(⭐ 要「测量 + 缓存」)
→ ⭐ 折中:「先估高 → 渲染后测量 → 修正」(⭐ 会「跳一下」但可接受)
· ⭐ **`content-visibility: auto` + `contain-intrinsic-size`** 是「轻量版」
(⭐ 「DOM 不多」时可以用,见 5.17)—— ⚠️ 它「不省内存」
· ⭐ 「过渡动画」只改 `transform`/`opacity`(见 5.17)
② 数据层:
· ⭐ **游标分页(cursor-based)而不是 offset**——
因为「信息流会插入新内容」→ ⚠️ offset 分页会「重复/跳过」(见 7.10 的分页讨论)
· ⭐ **`useInfiniteQuery`**(React Query / tRPC)—— 内建「下一页 + 缓存 + 去重」(见 7.4)
· ⭐ **预加载**:用 `IntersectionObserver` 在「距底 2 屏」时请求下一页(见 1.14)
· ⭐ 「新内容插入」用「顶部提示条(『有 5 条新内容』)」而不是「直接插进去并跳滚动」(⚠️ 会打断用户)
③ 交互层:
· ⭐ **滚动位置恢复**(见 6.13)—— 「返回列表时回到原位」
· ⭐ 「图片」用 `loading="lazy"` + 明确的尺寸(⭐ 防 CLS,见 5.15)
· ⭐ 「回到顶部」按钮(⭐ 长列表必备)
④ 性能层:
· ⭐ 「滚动时不做「同步布局读取」」(⚠️ 布局抖动,见 1.14)
· ⭐ 「列表项内联函数/对象」要留意(⭐ 对「虚拟列表」影响较小,因为「渲染项少」)
【权衡】
⭐ 「虚拟滚动」的代价:「Ctrl+F 搜不到未渲染的内容」「打印不全」
→ ⭐ 需要「可搜索」的场景要「额外的处理」(如「搜索走服务端」)
⭐ 「不定高 + 虚拟滚动」在「快速滚动」时可能「白屏一下」(⭐ 因为「测量还没回来」)
三、场景 2:大型表单(100+ 字段)
【澄清清单】
· 字段类型?(原生输入 vs 复杂控件如「日期/富文本/上传」)
· 需要「实时校验」吗?有「跨字段校验」吗?(⭐ 决定规模)
· 需要「分步」吗?(⭐ 多步表单的「状态保留」问题,见 1.13)
· 需要「草稿保存」吗?(⭐ 本地持久化,见 3.15)
· 提交是「一次性」还是「逐字段保存」?(⭐ 完全不同)
· 用户会「输入很慢」吗?(⭐ 「性能敏感度」)
【拆解与决策】
① 数据层(⭐ 核心决定):
· ⭐⭐ **非受控 + 字段级订阅**(react-hook-form 的思路,见 1.13)——
⚠️ **「受控 + useState」在 100 字段时会「输入一个字符重渲染整表单」**
· ⭐ 或「受控但字段级订阅」(自研 / Jotai 的「对象 atom + 字段级派生」,见 3.11)
· ⭐ **校验用 schema(zod)**:一处定义「类型 + 规则」(见 1.13)
· ⭐ 「跨字段校验」放「表单级 resolver」(⭐ 字段级校验器「拿不到别的字段」)
② 渲染层:
· ⭐ **字段组件要「只订阅自己」**——「改 A 字段只有 A 的输入框与它的错误提示重渲染」
(⭐ 这是「大表单性能」的唯一解,见 11.14)
· ⭐ 「分步(wizard)」用「`shouldUnregister: false` 保値」(见 1.13)
· ⭐ 「字段很多」时「滚动性能」要注意(但「字段数」通常不至于要虚拟滚动)
③ 体验层:
· ⭐ 「错误显示时机」——「touched 之后才显示」(⭐ 避免「一打开满屏红字」,见 1.13/11.14)
· ⭐ 「提交失败要「把用户送回出错的那一步/那个字段」」
· ⭐ 「草稿自动保存」(⭐ 本地持久化 + 节流,见 3.15)
· ⭐ 「提交中禁用 + 防重复提交」(⭐ 并做「服务端幂等」,见 9.10)
④ 工程层:
· ⭐ 「字段配置驱动」——用「schema/配置」生成表单(⭐ 而不是「手写 100 个 input」)
⚠️ 但「配置驱动」的代价是「复杂布局/联动会很难表达」
→ ⭐ 折中:「常规字段配置驱动 + 特殊字段手写」
· ⭐ 「嵌套/数组字段」用「路径(`items[0].name`)」,⭐ 且「写入只复制路径上的层」(见 11.14)
· ⭐ 「数组字段的 key 用稳定 id(不是 index)」(⭐ 否则「删中间项时状态错位」,见 1.13)
【权衡】
⭐ 「非受控」的代价:「程序化改值要「手动同步 DOM」」(⭐ 见 11.14 的坑)
⭐ 「配置驱动」的代价:「复杂联动」表达力不足 → 「最终会「配置 + 代码混用」」
四、场景 3:实时协作文档
【澄清清单】
· 并发规模?(⭐ 「2 人」和「50 人同编一段」完全不同)
· 冲突处理要求?(⭐ 「最后写入赢」够用吗,还是要「不丢字符」?)
· 离线要求?(⭐ 「离线编辑 + 合并」= 「CRDT 的领域」,见 7.12)
· 延迟要求?(⭐ 「光标协同」要「毫秒级」)
· 「谁在编辑哪一段」要显示吗?(⭐ 这需要「额外协议」)
【拆解与决策】
① 冲突模型(⭐ 这是「全场最硬的决策」):
· ⭐ **「文本并发编辑」必须用「操作型 CRDT」**(如 Yjs / Automerge)——
⚠️ **不要自己手写文本 CRDT**(⭐ 这是「少数「不该自研」的领域」)
· ⭐ 「结构化数据(字段级)」→ 可以「按字段 LWW 或字段级合并」(见 7.12)
· ⚠️ 「最后写入赢」用在文本上 = 「必然丢字符」
② 传输层(⭐ 与 7.14 同源):
· ⭐ **WebSocket**(双向、毫秒级)—— ⭐ 这是「真双向」的场景,**不该用 SSE**
· ⭐ **二进制编码**(Yjs 传的是「update 的二进制」)—— 比 JSON 小很多
· ⭐ **`ArrayBuffer` + Transferable**(⭐ 如果「大 update」)
· ⭐ 「断线补偿」——CRDT 天生「可合并任意顺序的 update」,
所以「重连后「同步双方的全部 update」」即可(⭐ 比「消息补发」更简单,见 7.14)
③ 状态模型:
· ⭐ **文档状态「不完全放 React state」**——因为「每次击键都是「一次 update」」→
⚠️ 「进 React state 会「每键一次重渲染」」
→ ⭐ 做法:「Yjs 的 doc 在外面」+「用「绑定」(`y-textarea` 等)或
「把「渲染的文本」用「非 React 的方式更新」」
⭐ 这与「拖拽用 ref + 直接改 DOM」是**同一个思路**(见 12.13)
④ 协作感知(presence):
· ⭐ 「谁在线/谁的光标在哪」用「独立的、「低频、可丢失」的通道」——
⭐ **它不需要「可靠」**(丢了「下一次心跳会补上」)
· ⭐ 「光标位置」用「相对位置(CRDT 的 relative position)」而不是「绝对 offset」
(⚠️ 否则「别人插入文字后你的光标会「漂移」」)
【权衡】
⭐ 「CRDT」的代价:「文档体积随「历史」增长」→ 需要「压缩/快照」(⭐ Yjs 有 GC)
⭐ 「协同编辑」的代价:「撤销/重做」要「按「用户」而不是「按「全局」」(⭐ 有额外设计)
⭐ 「服务端」要「持久化 update + 定期快照」(⚠️ 「只存最终文本」会「丢失历史」)
五、场景 4:数据看板(大量图表 + 实时刷新)
【澄清清单】
· 图表数量?(⭐ 「5 个」和「50 个同屏」完全不同)
· 刷新频率?(⭐ 「每分钟」和「每秒」完全不同)
· 「实时」的定义?(⭐ 「接近实时(1s)」还是「流式(100ms)」)
· 需要「下钻/联动」吗?(⭐ 联动会「触发连锁更新」)
· 有多少人在看?(⭐ 决定「推送方式」与服务端成本,见 7.14)
【拆解与决策】
① 渲染层:
· ⭐ **图表用 Canvas/SVG 库(ECharts/Chart.js/D3/Visx)**,⭐ 且「按需引入」
(⚠️ ECharts 全量很大,见 5.14)
· ⭐ **图表容器「尺寸稳定」**(⭐ 否则「ResizeObserver 循环 + 布局抖动」,见 1.14)
· ⭐ **`IntersectionObserver` 让「屏外图表「不渲染」**(⭐ 见 5.5 的思路)
· ⭐ 「图表实例」用「`ref` + 命令式更新」(⭐ 不要「每次数据变都「重建实例」」)
② 数据层:
· ⭐ **合并降采样**:⚠️ 「服务端每秒推 100 条,前端画 100 次」= 灾难
→ ⭐ 「客户端「按帧合并」(如「每 500ms 用「最新值」更新一次」)——
**与 7.14 的「背压与合并」是同一件事**
· ⭐ 「不重要的图表」降频/暂停(⭐ 屏外的不刷、后台标签页不刷,见 7.14)
· ⭐ 「同一个数据被多个图表用」→ 「取一次、分发」(⭐ 别每个图表各请求一次)
③ 性能层:
· ⭐ 「主线程」是「共同的瓶颈」:
· 「大数据集的聚合计算」→ ⭐ **Web Worker**(见 8.10)
· 「图表库自身的渲染」→ 「降采样 + 「只在可见时渲染」」
· ⭐ 「高频更新」不要「走 React state 逐点更新」——
⭐ 「用「图表实例的命令式 API」(`chart.setOption`)」(⭐ 同「ref 直接改 DOM」的思路)
④ 「实时」的传输(⭐ 见 7.14):
· ⭐ 「看板上百人」→ ⭐ **SSE**(只推送)通常比 WebSocket 更合适
(⭐ 简单、内建重连、网关友好)
· ⭐ 「推送「只带「哪个指标变了」」→ 客户端「拉详情」(⭐ 见 7.14 的「推送只作提示」)
【权衡】
⭐ 「Canvas 图表」的代价:「无障碍差」(⭐ 要「提供表格/文本替代」,见 10.12)
⭐ 「实时」的代价:「服务端成本 ×观看人数」→ ⭐ 可能「改「轮询 + 缓存」更划算」
(⭐ 「真的需要「秒级」吗」——这是必须澄清的)
六、场景 5:离线可用的现场作业 App
【澄清清单】
· 离线多久?(⭐ 「几分钟」和「一整天」不同)
· 「离线期间」允许写吗?(⭐ 允许 → 必须有「队列 + 冲突处理」,见 7.12)
· 数据量?(⭐ 「本地库要多大」)
· 设备?(⭐ 「低端安卓机 + 弱网」很常见)
· 「数据一致性」要求?(⭐ 「库存/金额」要强一致 → 可能「不允许离线写」)
【拆解与决策】(⭐ 这一场景在 7.12 已有系统展开,这里给「决策清单」)
① ⭐ **本地库是「唯一读写入口」**(见 7.12)
· 「UI 只读本地库」+「网络同步是后台过程」→ 「UI 逻辑只有一条路径」
② ⭐ **写操作「先落本地 + 入队」**(⭐ 且「同一事务」,见 7.12)
· 「乐观 UI」+「队列 + 幂等键」+「指数退避重试」+「死信可见可处理」
③ ⭐ **冲突解决**(⭐ 别指望「不冲突」)
· 「字段级合并」优先(⭐ 多数冲突是「改了不同字段」)
· 「真要「同段文本并发」→ CRDT 库」
④ ⭐ **同步状态必须可见**(⭐ 「每条记录的状态 + 全局待同步数 + 离线条」,见 7.12)
· ⚠️ 「用户以为保存了但丢了」→ **信任崩塌**(⭐ 这是「体验红线」)
⑤ ⭐ **存储选型**(⭐ 见 3.15)
· 「IndexedDB(Web)/ SQLite(RN)」—— ⚠️ 「localStorage 只有 5MB 且同步阻塞」
· ⭐ 「版本迁移」从第一天就要做(⭐ 见 3.15)
⑥ ⭐ **「离线壳」**(⭐ 让「App 能打开」)
· Web:Service Worker 缓存壳(⭐ 见 10.11)
· ⭐ 「不这么做 = 「离线时白屏」」—— 连「提示用户」的机会都没有
【权衡】
⭐ 「离线优先」的代价:「复杂度大幅上升」(本地库 + 队列 + 冲突 + 同步可见)
⭐ ⭐ **一个重要判断**:「**不是所有数据都该支持离线写**」——
「强一致」(库存扣减、金额)应该「在线才允许」,并「明确告诉用户」
(⭐ 「让用户填完才发现「不能提交」」比「一开始就禁用」更糟)
七、通用「决策清单」(⭐ 场景题万能检查表)
⭐⭐⭐ 任何场景题都可以按这九个问题过一遍(⭐ 「不漏项」比「答得深」更重要):
① ⭐ 【数据量级与频率】「多少条/多久一次」→ 决定「要不要虚拟化/合并/降采样」
② ⭐ 【延迟与一致性要求】「能容忍多少陈旧」→ 决定「缓存策略」与「推送方式」
③ ⭐ 【离线与弱网】「要不要离线可用」→ 决定「本地库/队列/冲突」
④ ⭐ 【渲染瓶颈】「DOM 数量?重渲染范围?合成层?」→ 决定「虚拟化/订阅粒度/走合成」
⑤ ⭐ 【计算瓶颈】「有没有「超过 16ms 的同步计算」」→ 决定「Worker」
⑥ ⭐ 【状态归属】「服务端状态 vs 客户端状态 vs URL 状态」→ 决定「用什么工具」(见 3.10)
⑦ ⭐ 【可观测性】「上线后「怎么知道它好/坏」」→ 决定「监控/埋点/预算」
⑧ ⭐ 【团队与维护】「团队多大、能不能维护复杂方案」→ 决定「方案的复杂度上限」
⑨ ⭐ 【失败的降级】「这个方案在「X 情况」下会退化吗」→ 决定「兜底与开关」(见 8.9)
八、面试延伸:怎么答这类问题(⭐ 一句话模板)
⭐⭐⭐ 一个「万能开场 + 收尾」(⭐ 面试里非常好用):
【开场(⭐ 澄清)】
「在设计之前我想先确认几个约束:
**数据量级、更新频率、要不要离线、有没有现成的数据层、
以及团队能接受的复杂度上限** —— 因为这几个「会直接改变方案」。」
【收尾(⭐ 权衡与降级)】
「这个方案的核心取舍是「**用 X 换 Y**」;
它在「**情况 A**」下会退化,那时我会「**降级到 B**」。
另外「**上线后要看「指标 C」**」来判断「它是否真的解决了问题」。」
⭐ 三个「让答案像「真做过」」的细节:
① ⭐ 提「具体的 API/参数/数值阈值」
「用 `IntersectionObserver` 提前 400px 触发」「时间片 5ms」「可见比例 ≥ 50%」
(⭐ 而不是「用个观察器」「分片渲染」)
② ⭐ 提「具体的坑」
「⚠️ `ArrayBuffer` 转移后会 detach」「⚠️ tag 命名不一致会导致「失效了但不更新」」
「⚠️ 嵌套点击不阻止冒泡会导致埋点数据翻倍」
③ ⭐ 提「怎么验证」
「用 Profiler 对比优化前后」「看 RUM 的 P75」(⭐ 见 5.12/7.13)
九、一句话速记
场景设计题的骨架是五步:澄清约束(⭐ 90% 的人跳过,而「答案完全取决于约束」)→ 拆成子问题(渲染/数据/交互/性能/工程)→ 给决策 + 理由 + **替代方案(⭐ 「有取舍才是设计」)→ 落地细节(具体 API/参数)→ 主动讲权衡与降级;五个高频场景的核心决策:① 无限滚动 → 虚拟滚动(⚠️ 不定高有代价)+ 游标分页(不用 offset,因为会插新内容)+ 预加载 + 顶部「有 N 条新内容」提示(不打断滚动);② 100+ 字段表单 → 非受控 + 字段级订阅(⭐ 否则「一个字符重渲染整表单」)+ schema 校验 +
touched才显示错误 + 草稿持久化 + 数组字段用稳定 id 做 key;③ 实时协作 → 文本冲突必须用 CRDT 库(⭐ 少数「不该自研」的领域)+ WebSocket + 二进制 update(Transferable)+ ⭐ 文档状态不进 React state(同「ref 直接改 DOM」的思路)+ 光标用「相对位置」;④ 数据看板 → 图表按需引入 + 尺寸稳定 + 屏外不渲染 + ⭐ 客户端「按帧合并」高频数据(否则「每秒推 100 条画 100 次」)+ 大数据集聚合放 Worker + 高频更新走「图表实例的命令式 API」;⑤ 离线现场作业 → 本地库是唯一读写入口 + 写操作先落本地并入同一事务 + 队列(幂等/退避/死信)+ 字段级合并 + ⭐ 同步状态必须可见 + IndexedDB/SQLite + 离线壳;⭐ **万能检查表九个问题:数据量与频率 / 延迟与一致性 / 离线弱网 / 渲染瓶颈 / 计算瓶颈 / 状态归属 / 可观测性 / 团队维护能力 / 失败降级;⭐ 让答案像「真做过」的三个细节:提「具体 API 与阈值」、提「具体的坑」、提「怎么验证」。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!