帆软前端二面三面面经
- 轮次
- 二面、三面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
二面(约 55 分钟)
- 描述用户从说话到文字呈现到屏幕的完整流程,你负责哪部分?
- revision 快照覆盖是什么?为什么用覆盖而不是增量同步?
- 缓冲调度解决什么问题?
- 断线重连时,断线期间正在说的语音怎么处理?
- renderToPipeableStream 和 renderToString、renderToStream 有什么区别?
- Tauri 项目用 ready/get 事件暂存加顺序补发解决初始化竞态,是什么场景?怎么解的?
- Tauri 通信哪些走 command、哪些走 event?返回值上有什么差别?
- 虚拟滚动的数据量有多大?怎么实现?定高改成不定高怎么办?
- 十万行乘两百列的大表格怎么做?合并单元格对不上怎么办?
- 分片上传的 hash 在哪里算?大文件怎么避免卡页面?内网改公有云要做什么调整?
- 现在大部分代码由 AI 写,你们怎么 review AI 代码?
- AI 写代码又写单测,怎么确定测试不是在迎合代码?
- 有没有遇到 AI 怎么改都改不对的情况?
- 场景题:BI 仪表板有几十个图表加筛选器,怎么设计数据请求和渲染?
- 追问:筛选器只影响部分图表(按字段联动),React 下怎么处理?
- 追问:同一个图表三次修改、后台并行返回乱序,怎么保证最终渲染是最后一次的结果?
三面(约 20 分钟)
- 为什么做前端?什么时候开始接触的?
- JSBridge 的原理是什么?H5 怎么调到原生能力?
- 混合开发常见的技术选型有哪些?RN 的原理了解吗?
- 平时学习前端的渠道有哪些?
- 出于兴趣做过什么项目?3D 项目是需求还是兴趣?用了什么框架?
- WebGL 和 WebGPU 的差别是什么?用过 WebGPU 吗?
- AI coding 占比多少?举一个 AI 方案不满意、你去纠正的案例,你是怎么发现问题的?
- 用 AI 做过最大的项目是什么?
- 你的主要工作是什么?最难的问题是什么?内存问题从现象到定位到大对象再到优化,内存降了多少?
- 找工作看重哪些点?城市意向是什么?
- 在校期间最有成就感的一件事是什么?
- 有直接跟用户沟通需求吗?是你主导还是导师带着?有没有达不成一致的矛盾?
《参考解析》
语音链路的端到端时序:快照覆盖、缓冲调度与断线期间的音频。 从说话到屏幕出字要经过采集、降噪与分帧、识别(流式返回中间结果与最终结果)、文本整理、渲染几段,容易出问题的是状态对齐而不是单点性能。中间识别结果会不断修正前面已出的字,如果按增量追加就会越写越乱,所以用「以 revision 为单位的快照覆盖」:服务端为每次修正生成新版本号,前端整段替换对应范围的文本,渲染结果与最新版本一致;代价是传输量比增量大一点,但换来的是幂等——同一 revision 重复到达、乱序到达都不会产生脏数据,这个收益远大于带宽成本。缓冲调度解决的是「识别产出速率与渲染节奏不匹配」:字来得太密会让 React 每收到一个 token 就重渲染一次,用 requestAnimationFrame 或固定时间窗把多次更新合并成一次提交,既压掉抖动也避免布局反复计算。断线期间的音频是最容易被追问的点:纯前端的做法是本地缓存未确认的音频分片,重连后按序号补发,服务端按会话与序号去重拼接;更稳的做法是前端先本地出临时占位文本,重连拿到最终识别结果后整体覆盖,让用户立刻看到反馈又不留下错误内容,同时明确告知「这段可能不完整」。整体设计原则是:任何一段都可以重试,靠序号与版本号收敛,而不是靠「不丢包」。
renderToPipeableStream 与另外两个 API 的差别在「流什么时候能用」。 renderToString 是同步的:它必须等整棵树算完才返回完整字符串,期间不释放任何东西,所以无法配合 Suspense 做流式吐字,适合小页面或非关键场景。renderToNodeStream 返回一个可读流,能边渲染边输出,但它不支持 Suspense,也没有续接脚本的机制,遇到异步数据只能干等。renderToPipeableStream 是当前推荐的方案:它把渲染分成壳与内容两段,先吐出包含 fallback 的页面骨架让浏览器尽早上屏,异步数据就绪后把真实内容连同补丁脚本一起追加到流尾,由 React 在客户端接管替换;onShellReady 适合需要精细控制响应头(状态码、爬虫可见性)的场景,onAllReady 适合爬虫或静态生成,因为要等全部内容才好给完整 HTML。工程取舍很清楚:要首字节快、要 SEO 的页面用 pipeable 并暴露 onShellReady 尽早开流;只需要字符串的小页面用 renderToString 更简单;注意流一旦开始写就不能再改状态码,所以错误边界和状态码决策要在开流前做完。
初始化竞态与事件顺序补发。 桌面 WebView 里,前端脚本开始执行和原生侧准备就绪是两个并行的时序,谁先到不确定,于是常出现「前端订阅事件时,原生已经把事件发完了」的丢消息问题。解法是给事件加一个暂存层:原生侧在启动阶段产生的 ready 事件既推送也写入一个可查询的缓冲(内存队列或状态字段),前端挂载后先主动 get 一次当前状态,再把缓冲里未消费的事件按顺序补发,之后的实时事件才走推送;前端对同一事件做幂等处理(按序号或状态位去重),补发与推送重叠也不会重复生效。command 与 event 的分工是另一条常考题:command 是前端主动发起、有请求响应语义的调用(返回 Promise 或结果值,失败可抛错,适合查询与写操作),event 是后端主动广播、单向无返回值(适合状态变化通知、进度与日志),选错了会导致「用事件做请求响应」这种脆弱结构。判断标准很简单:谁发起、要不要拿到结果——要结果就用 command,只是通知就用 event,跨进程调用的参数与返回值都要能被序列化。
主线程预算:大表格、虚拟滚动与分片 hash 是同一类问题。 浏览器只有一条主线程,任何超过 16ms 的同步任务都会掉帧,所以这几道题共用一个答案:把工作切小、把计算搬走。十万行乘两百列直接渲染 DOM 是不可能的(节点数以百万计),标准方案是 Canvas 绘制可视区加脏区检测与局部重绘:按行列位置映射到像素,只重画滚动或数据变化涉及的矩形区域,把一帧的绘制量控制在预算内;合并单元格对不上通常是因为「视觉合并」和「数据模型」没对齐,正确做法是维护一份合并区间索引(按锚点行列存跨度),绘制时先按锚点合并再裁剪,命中的单元格直接跳过,命中判断用二分而不是逐个比较。虚拟滚动在定高时只是滚动位置的除法,不定高则要用已测量高度的前缀和加二分定位,未测量项先估算、量到真实高度后回写缓存并修正偏移,代价是可能出现滚动抖动和快速滚动白屏,靠提高缓冲区和骨架屏兜。分片上传的 hash 是纯粹的 CPU 密集计算,大文件在 Worker 里分片计算(或抽样计算文件头尾加大小做快速校验),主线程只做索引登记与上传调度,这样滚动和点击不会被打断;从内网迁到公有云要应对的是更高的网络抖动与失败率——分片调小、并发上传分级、断点续传记录已完成分片,以及失败退避重试,这些在内网环境下往往被忽略,一换环境就集中暴露。
BI 仪表板:请求编排与乱序响应收敛。 几十个图表加筛选器如果各自独立发请求,会出现请求风暴、筛选条件不一致、以及旧响应覆盖新响应三类问题。请求侧的做法是分层:以筛选器状态作为一个「查询描述对象」统一生成请求,相同数据源的图表合并成一个批量接口或按数据集分组,对请求做去重(相同参数在飞请求复用 Promise)与取消(参数变化时 abort 掉旧请求),并给筛选加防抖避免连续选择时打满后端。渲染侧的关键是让每个图表独立处理自己的加载与错误状态,别让一个慢接口把整页按住——可以用 Suspense 边界加骨架屏,或者图表各自维护 loading。至于三次修改乱序返回,靠「请求序号 + 结果集比对」解决:每次发请求递增一个版本号,响应回来只接受版本号等于当前最新值的结果,旧的直接丢弃;更严谨的做法是把「当前生效的查询条件」作为响应的一部分回传,前端比对不一致就丢弃,这能覆盖跨组件与服务端缓存带来的隐性乱序。补充一点:筛选器按字段联动时的正确姿势是先算依赖图(哪些图表依赖哪些字段),只有依赖变化的图表才重新请求,而不是全量刷新。
AI 写的代码怎么 review、怎么写不迎合实现的测试。 review AI 产出的代码,重心要往「意图是否被满足」和「边界是否被处理」挪,而不是纠缠格式——格式交给 lint 和格式化工具自动收敛。有效的几条:改动必须能对应到一个明确的输入输出与验收标准,先看测试是否真的断言业务语义;重点关注 AI 最容易漏的地方——错误分支、并发与幂等、空值与越界、资源释放、权限与输入校验;对可疑的小块让其解释「为什么这么写、替代方案是什么」,解释不通的部分人工重写;把「代码里有没有偷偷改掉调用方契约」当成必查项。至于测试迎合代码,本质是同一份上下文里生成实现和断言导致的共谋,破法有三条:断言写到行为层而不是实现细节(不 mock 到函数级、不测内部方法调用次数),否则测试只是把实现抄了一遍;先写失败用例再让 AI 实现(或至少让另一轮会话只看需求写测试);用变异测试或刻意改坏实现来验证测试会不会红——红不了说明测试没在断言真正的东西。最后把高风险路径的用例沉淀成回归集,让它成为后续自动改动的护栏。