1. 投递这一面之前我做了哪些准备先交代一下背景。我是社招主攻方向是中后台复杂前端应用之前做过在线文档、低代码平台这类偏“重型”的项目。投飞书前端理由其实很直接飞书是典型的 To B 协作工具文档、表格、会议、审批这些场景天然就是前端的深水区——长文档渲染、多人实时协同、复杂表格交互、消息流性能哪一个拿出来都能挖得很深。这正好是我这几年一直在积累的东西所以我判断这个岗位的面试官大概率会贴着项目问而不是纯考八股。简历我改了三天。一开始我写的是“负责 xx 模块开发提升 xx 效率”后面发现这种写法根本没法打。后来我把每一段项目经历都改成“面对什么问题我做了哪些方案对比最终选择什么拿到什么可量化的收益”。举一个我改完的例子以前写“实现了大文件上传功能”改成“视频素材上传场景下原方案在 2GB 文件时主线程阻塞约 4 秒我改造成基于 Web Worker 的分片上传 断点续传方案上传耗时降低 35%整体内存峰值下降 40%”。为什么这么改因为社招一面基本不会对着简历逐字问面试官只会挑你写出来的技术点顺着往下挖。你写得越具体他越容易找到切入点你也越容易把话题引到自己的强项上。如果你只写“负责 xx”面试官就只能从常见八股题库里随便抽题考你那场面就不可控了。技术准备这块我没有按照传统“前端面试题”的清单死背而是按底层原理重新过了一遍。我当时的准备清单大概是这样的方向核心知识点我用的练习方式JavaScript事件循环、闭包、原型链、垃圾回收、Promise/A手写实现 给代码猜输出浏览器渲染流程、合成层、重排重绘、性能指标用 Performance 面板实测自己的项目框架React 渲染流程、Fiber、diff 策略Vue 响应式原理画执行流程图 手写简化版工程化Webpack/Vite 构建流程、微前端沙箱、monorepo搭一个最小可运行的微前端 Demo场景题大文件上传、协同编辑、消息推送、离线缓存每周写一个设计思路文档另外飞书这类业务对“实时协作”和“复杂编辑器”的知识特别看重所以我把之前研究过的 OT 算法、CRDT 思路、WebSocket 重连机制、SSE 推送、IndexedDB 缓存这些内容都重新整理了一遍。还有一个容易被忽略的准备面试前把飞书的产品打开认真用一下。我在面试前一周每天会用飞书写文档、做表格感受它们的交互细节。比如飞书文档的光标位置处理、多人头像展示、表格冻结行列的交互这些看起来只是功能但如果你能说出“这个功能的难点在哪里”面试官会眼前一亮。提示社招一面和校招最大的区别是——面试官默认你是有实际项目经验的所以他问的每一个基础题最终都会落到“你在项目中怎么用它解决了问题”上。准备时一定要给每个知识点准备一个项目案例。2. 开场自我介绍与项目深挖这一问就聊了 25 分钟一面开头照例是自我介绍。我的表达在 2 分钟左右结构是“过去做了什么 目前最擅长的方向 为什么来面这个岗位”。建议大家不要花太多时间讲经历面试官真正想听到的是你的技术标签是什么、和这个岗位匹配度有多高。我给自己定的标签是“复杂前端应用和高性能渲染”。因为我做过的在线文档和低代码平台都涉及长列表渲染、协同编辑、复杂状态管理这和飞书文档、表格的场景非常匹配。所以自我介绍里我就直接说“我近几年主要在做复杂前端应用涉及编辑器渲染、多人协同和性能优化飞书文档类业务应该是我最能发挥价值的方向。”这句话相当于主动递了一个钩子。结果不出所料面试官接着就问“那你挑一个最能代表你能力的项目讲讲里面最复杂的技术点。”我讲的是之前做过的在线文档表格协同项目然后就是长达 25 分钟的连环追问。我当时讲的是一个自研的表格渲染和协同模块但面试官的追问风格不是“你做了什么”而是反复问“你怎么证明你的方案是对的”。整个深挖过程我整理了下面几个核心问题大家可以感受一下考察逻辑2.1 协同冲突是怎么处理的客户端还是服务端合并我遇到过这类问题所以干脆提前讲清楚我们用的是基于操作转换的思路客户端的每一次编辑都生成一个操作描述发送给服务端。服务端维护一个操作序列当不同客户端的操作发生冲突时通过转换算法让后到的操作相较于先到的操作做一次调整然后再广播给所有客户端。面试官追问“你这里为什么用操作转换而不是 CRDT有对比过吗”这是很关键的问题。我的回答是表格场景的数据结构比较结构化操作转换的效率更高且实现逻辑与现有编辑器的数据结构更匹配CRDT 的优势在于天然适合离线合并和去中心化但对表格这类“位置频繁移动”的场景元数据膨胀问题很难接受。面试官听到这个对比明显是满意的因为他说“看来你确实把两种方案都研究过”。这里我给自己提了个醒项目讲述不能只讲结果要把“方案选型的对比过程”说出来。面试官真正想看的不是你知道 OT 还是 CRDT而是你有没有能力在多个可行方案里做合理决策。2.2 Web Worker 解决了什么不用行不行我提到过用 Web Worker 做表格的渲染调度和计算任务分离。面试官直接问了一个很实在的问题“你这里不用 Web Worker 会怎样”这个问题的考察点在于你是否真实验证过性能瓶颈还是跟风用了个热门技术。我的回答是我们在一个 5000 行的表格上做过压测不做调度优化时主线程的长任务会阻塞页面交互输入延迟达到 300 毫秒以上下拉滚动有明显的空白闪烁。用 Worker 把行高计算和渲染指令生成移出去之后主线程的长任务从 180 毫秒降到了 20 毫秒左右。我特意提了一句“交互输入延迟降低到 50 毫秒以内用户体感基本无感了”因为这些都是实测数据。面试官没有继续纠细节但追了一句“Worker 和主线程之间的通信本身也有开销你怎么平衡”我说我们的原则是只把“计算密集且不需要频繁回传”的任务放到 Worker 里比如行高批量计算而用户输入响应这类高频、低计算量的任务保持在主线程避免序列化和消息传递的开销。简单说就是“重计算、轻交互”的划分。2.3 如果让你设计一个多人同时编辑的表格模块怎么拆分这是一个典型的设计题。面试官问这个问题的潜台词是你已经做过了那你有没有总结出可复用的架构模式我是这样拆的渲染层负责可视区的表格渲染和交互、操作管理层负责本地操作的生成、应用和 undo/redo、同步层负责与 WebSocket 通信、操作序列的处理和冲突解决、状态层维护最终一致的数据模型。同时我提到本地状态会做备份和持久化断网时先走本地缓存恢复连接后再做增量补偿。这个设计也呼应了飞书文档离线编辑的场景。这一段聊下来我总结出一个很重要的经验项目介绍时不要平铺直叙地罗列功能而是要“制造钩子”每讲一个技术点就留下一个面试官可能追问的线索。比如我提到 Worker 时故意说“性能数据从 180 降到 20 毫秒”这比单说“用了 Worker”更有被追问的余地。3. 前端基础与原理考察看起来是八股其实都在考场景项目深挖结束面试官切换到基础考察环节。这个环节大概是 15 分钟面试官没有一上来就让我背概念而是丢出来几段代码和一些场景问题。我印象比较深的几个点如下。3.1 事件循环不是背输出而是讲调度过程面试官给了一段代码要求说出输出顺序console.log(script start); setTimeout(() { console.log(timeout1); }, 0); Promise.resolve().then(() { console.log(promise1); }); queueMicrotask(() { console.log(microtask1); }); console.log(script end);这个输出是script start、script end、promise1、microtask1、timeout1。关键是面试官没有在我说出答案后就放过我而是追问“为什么 Promise 回调会在setTimeout之前执行如果 setTimeout 嵌在两个 Promise 里执行顺序会怎样”我当时的回答思路是每一轮事件循环里入栈的逻辑先全部执行完然后处理微任务队列微任务队列清空后再从宏任务队列取下一个任务。所以script end输出完后先清空微任务再执行宏任务。而setTimeout即使延迟是 0也会在下一轮宏任务执行。这类题很多人是背口诀“先微后宏”但面试官真正想验证的是你有没有理解“队列的调度优先级”和“为什么微任务需要被一次性清空”。如果只是背口诀遇到嵌套 Promise 和嵌套 setTimeout 的变种题就会慌。我建议准备面试时把事件循环的这个调度过程用“生产者和消费者”的模型理解一遍JS 引擎是一个消费者任务队列是缓冲区微任务优先级更高只有微任务清空才会消费下一个宏任务。3.2 闭包从一道循环输出题展开紧接着是经典的循环输出for (var i 0; i 5; i) { setTimeout(() { console.log(i); }, 0); }输出全是 5。原因是var声明的i是函数级作用域循环结束后i已经变成 5五个定时器回调共用同一个变量。面试官的追问是“如果不用 let你有哪些方式让输出变成 0、1、2、3、4”我给出了两种IIFE 传参以及把setTimeout放到一个函数工厂里。前一种方式还衍生出一个问题为什么 IIFE 里能保留每次循环的i值核心是“每次迭代都会创建一个独立函数作用域参数被复制到当前作用域里”。面试官又问“闭包会导致内存泄漏吗你在项目里遇到过吗”这个问题我很有感触因为我真的排查过一次一个地图应用里事件回调函数引用了大量 DOM 节点和业务对象页面切换时没有主动解绑导致内存持续上涨最后整个页面在移动端直接被系统杀掉。我用 Chrome DevTools 的 Heap Snapshot 对比了操作前后的内存变化定位到是闭包引用链没有释放。这种“真实经历过”的回答比任何标准答案都有说服力。3.3 浏览器渲染与性能优化从 URL 输入到页面显示面试官问了一段很经典的问题“在地址栏输入 URL 到页面显示经历了哪些过程”这种题大家都会答但我要提醒一句社招不能只答那几个大步骤面试官一定会挑一两个细节深挖。我回答时重点展开了两个阶段HTML 解析和渲染合成。HTML 解析阶段我提到了 CSS 和 JS 对渲染的阻塞关系CSS 会阻塞渲染树的构建而普通脚本会阻塞解析。然后面试官顺着问了一个实际的优化问题“如果你有一个 2000 行的表格加载时会出现长时间白屏你怎么排查和优化”我的回答是先确认瓶颈在哪个阶段——如果网络请求时间占比高就要做数据分片或骨架屏如果 JS 执行时间长就要考虑任务拆解、懒加载、虚拟滚动如果渲染阶段时间长就要看是不是表格单元格样式太复杂触发了大量重排。针对表格场景我实际做过只渲染可视区域的行列滚动时通过绝对定位保持视图稳定使用will-change提升合成层减少重绘面积。面试官追问“虚拟滚动的空白区域怎么处理”我说用一个占位块把整个滚动高度撑起来并维护可视区到数据索引的映射这样滚动条的真实高度不会失真。3.4 微前端与工程化为什么很多团队都在做飞书的中后台业务里微前端是常见的工程化方案。面试官问了一个很实际的问题“如果一个老项目要从单体应用拆成微前端你会怎么做主应用和子应用之间怎么通信”我提到了两种主流的隔离方式JavaScript 沙箱和样式隔离。沙箱上我解释过快照沙箱和代理沙箱的区别——快照沙箱适合运行前全局变量不复杂的场景代理沙箱可以在运行时拦截全局对象的读写实现更细粒度的隔离。样式隔离则分为每个子应用都挂载在自己的容器节点下配合 CSS Modules 或者scoped属性来避免串样式。我还主动提了一句“子应用加载完成前怎么避免主应用的白屏”这是我没踩过的坑但确实是经常会遇到的实际问题。我建议的方案是主应用在路由切换时保留旧的子应用界面拿到新子应用的加载状态后再渲染 Loading 态同时注意预加载脚本资源让切换更顺滑。这块我没有说得太深但面试官显然认可因为工程化问题背后其实是在考察“你是否理解大型团队协作时的架构治理”而不是会不会用某个框架。3.5 框架原理React 与 Vue至少要能讲清一条链路飞书前端的组件库以 React 为主但面试官没有限制我技术栈问的是“你在项目里用过 React那 React 的 setState 之后发生了什么能不能说清楚从调用到界面更新的完整链路。”我回答的思路是setState首先会触发一次更新标记React 会把该组件标记为需要更新然后把更新加入调度器。调度器根据当前优先级决定是同步执行还是进入异步调度。渲染阶段从根节点开始React 会通过 diff 算法对比新旧虚拟 DOM生成需要变更的最小操作集合提交阶段把这些操作同步到真实 DOM 上。注意在并发模式下渲染阶段是可以被打断的但提交阶段必须同步完成。面试官又追了一个问题“那 React 的事件系统和原生事件有什么区别”我举了一个项目里的实际例子React 17 之前事件是委托到 document 上的所以如果多个子应用微前端混用就很容易出现事件对象被其他应用干扰的情况React 17 之后改为委托到根容器上这种冲突就明显减少。这又是一个“我实际遇到并解决过”的回答。4. 手写题与场景设计题两道题不是考代码是考思考路径大概面试进行到第 35 分钟面试官进入了手写题环节。这个环节一共两道题一道是代码实现一道是场景设计。我这两道题加起来花了差不多 15 分钟。4.1 手写题带并发限制的任务调度器面试官原话是“实现一个 TaskScheduler它有一个 add 方法调用 add 时传入一个返回 Promise 的函数。要求同时最多只能执行两个任务并且要能拿到任务的返回结果。”我先把需求重复了一遍确认了几个关键点同时执行的任务数不能超过 limit任务执行完毕之后自动从等待队列取下一个任务执行需要支持拿到每个任务的结果和错误。然后我写了一个实现class Scheduler { constructor(limit 2) { this.limit limit; this.activeCount 0; this.queue []; } add(task) { return new Promise((resolve, reject) { const run async () { this.activeCount; try { const result await task(); resolve(result); } catch (error) { reject(error); } finally { this.activeCount--; this.next(); } }; if (this.activeCount this.limit) { run(); } else { this.queue.push(run); } }); } next() { if (this.activeCount this.limit this.queue.length 0) { const run this.queue.shift(); run(); } } }面试官看了一会儿没急着说对不对而是出了一个变种“如果任务之间有依赖关系比如第二个任务必须等第一个任务完成才能执行你会怎么改”我回答这种情况下就不能纯粹靠并发调度器来解决了需要在上层根据依赖关系构建一个任务拓扑图或把依赖任务组合成一个 Promise 链后作为一个整体任务丢给调度器。具体到代码可以先把有依赖的任务通过taskA().then(() taskB())组装成一个新任务再交给调度器统一调度。面试官又追了一个性能问题“网络请求类的任务并行数和排队数怎么平衡”这其实考察的是工程判断不是代码能力。我说并发数不能设置得太高要结合服务端的处理能力和业务场景一般 2 到 4 个就比较合理同时要设置超时和重试机制避免一个慢请求占住并发坑位。4.2 场景题大文件分片上传的客户端核心流程这个场景题其实和我简历里的项目是重合的面试官应该是特意挑了一个“你可能做过”的题来验证我的项目真实性。题目是“如果要上传一个超大文件比如 5GB 的视频前端要怎么设计上传方案”我的思路分成四步第一文件分片。通过File.slice(start, end)把文件切成固定大小的分片。分片大小不是随便定的我一般取 1MB 到 5MB 之间。为什么这么定分片太小会导致请求数量太多网络往返时间占比高分片太大会导致单次请求失败后重新传输的成本变高。我比较常用的是 2MB这样 5GB 的文件大概分成 2560 个分片数量上可接受单分片体积也合适。第二计算文件指纹。用文件内容生成一个唯一标识比如读取前几 MB 数据计算哈希或者直接对整个文件做抽样哈希。上传前先给服务端发送一个“查询”请求如果服务端说文件已经存在且完整那就是“秒传”如果服务端已经接收了一部分分片就只需要上传剩余分片这是“断点续传”。第三并发控制。不能把所有分片一次性发出去需要通过一个并发调度器控制上传数量。这里我就直接复用了刚才第一道手写题的思路设置并发数通常是 3 到 5 个。每个分片上传失败时要做指数退避重试比如第一次失败等 1 秒第二次等 2 秒第三次等 4 秒最多重试 3 次。第四进度计算。注意不能只看已上传分片数除以总分片数因为每个分片大小可能不一致。更准确的是累计已上传字节数除以文件总字节数。我还提到一个细节上传计算可以放到 Web Worker 里因为 Hash 计算、分片切片这些操作都比较消耗 CPU放在主线程会卡住页面交互。如果还要实现上传进度条就通过 Worker 的postMessage把进度信息回传主线程。热词里提到的“水波纹进度条”其实就是拿到真实的进度比例后用 CSS 动画做一个视觉反馈原理并不复杂但不能让动画进度脱离真实进度否则用户会后知后觉。面试官这时补了一句“如果用户在上传到一半时切换了网络比如从 WiFi 切到 4G你会怎么做”我的回答是监听navigator.onLine事件和visibilitychange一旦检测到断网立即暂停所有未完成的分片上传请求保存当前的分片上传状态到本地恢复网络后从本地状态读取已上传的分片列表只上传缺失的部分。这正好呼应了飞书文档在弱网环境下的离线恢复体验。4.3 场景题进阶多人实时协作文档的前端架构大文件上传聊完面试官把场景升级了一下“你做过在线文档假设现在让你给飞书文档设计一个多人同时编辑的前端架构你会怎么做”我给出的设计方案是分层架构渲染层负责文档内容的展示和编辑操作捕获包括光标位置、选区、输入、删除、格式变化。这部分要尽量轻量不做任何数据存储。状态层维护一个本地文档模型所有操作先应用在本地模型上实现“乐观更新”。用户无感知地看到自己的操作立即生效。通信层与服务器保持 WebSocket 长连接。操作发生时把操作序列发送到服务端服务端广播其他客户端操作本地收到后应用。为了让消息更实时可以用 SSE 做单向通知用 WebSocket 做双向实时操作。冲突处理层收到远端操作后如果和本地操作冲突通过 OT 算法进行转换保证每个客户端最终看到一致的结果。离线缓存层本地用 IndexedDB 持久化操作日志断开连接时操作先记到本地重连后按时间戳和序号重放。我还加了一个扩展点如果编辑器是纯文本CRDT 可能是更容易实现的方案因为它的数据结构天然支持无冲突合并但如果是富文本、表格这类结构化数据OT 更合适因为富文本的格式边界和嵌套结构非常复杂CRDT 的元数据膨胀和 GC 压力会变成一个很大的负担。面试官这里问了一个很细的点“本地缓存用 IndexedDB 还是其他方案有没有考虑过 RxDB”我承认我之前在一个低代码项目里用过 RxDB它本质是把 IndexedDB 封装成了响应式数据库适合做离线优先的本地状态管理。但在这里我的考量是它引入的依赖和心智负担比较大而文档协同场景的核心是操作日志的顺序和同步不是复杂的查询所以直接维护一个操作列表就够了。面试官听完没有反驳他只说了一句“你用一下就知道这个场景的边界在哪里了”这句算是给了一个经验性的提示。场景题答完之后我最大的感受是面试官并不是想要一个完美的标准设计而是想看到你如何拆解问题、如何权衡取舍。你每做一个技术选型都要能说出理由否则方案再漂亮也只是空中楼阁。5. 反问环节与一面节奏复盘手写题和场景题结束后面试官很自然地说“你有什么想问我的”我当时准备的问题有三个都围绕岗位实际价值。反问内容我觉得有必要写出来因为很多同学容易在这个环节“送分”或“送命”。我问了三个问题团队的技术栈和前端规模是怎样的协作上有什么比较棘手的挑战你们现在做文档协同底层渲染是自研的还是基于开源框架二次封装的这个岗位的业务交付节奏如何前端在跨端和性能优化上有什么比较硬的目标这三个问题的共同点是“把面试官当成行业内的人而不是考官”。我问技术栈和渲染方案其实是想评估这个岗位的技术深度问业务交付和目标是想了解我入职后的成长空间和真实挑战。有一个问题我原本想问但觉得不妥就没有问出口提醒大家注意“公司加班多不多”“这个岗位来了之后是不是很卷”这类问题在一面阶段不要问。一面是技术考察为主这类问题可以等到 HR 或业务二面再聊不然面试官容易对你的入职动机产生疑虑。关于一面整体的时间分配我复盘了一下环节时间内容占比自我介绍约 3 分钟技术标签 项目亮点项目深挖约 25 分钟文档协同、性能优化、架构设计基础与原理约 15 分钟事件循环、闭包、浏览器渲染、微前端、框架原理手写 场景约 15 分钟任务调度器、大文件上传、协作文档架构反问约 5 分钟岗位技术栈、业务挑战一面接近尾声时面试官的反馈信号比较明显他在反问环节结束后主动问了我“你目前面了几家公司还有什么流程在走”以及“如果你后续有二面希望重点考察哪方面”。这种问题通常意味着面试官对你整体表现比较认可才会做一定的“预期管理”。我当时没有直接回答具体公司名称而是说“目前手上有一个其他公司的流程在走但飞书这个岗位和我的方向最匹配所以我更倾向于这里”。这样说既表达了真诚又不暴露过多细节。6. 社招一面避坑建议从这次经历沉淀出来的方法这一节我专门写给正在准备社招前端面试的同学。我知道很多人会选择刷题、背八股文但我在这次一面里最大的体会就是面试官要的从来不是你知道什么而是你会不会在真实场景里用出来。6.1 八股文要会“组装”不能只会“背诵”面试官问事件循环时不是直接问“宏任务和微任务的区别”而是给了一段代码让你分析输出顺序再追问嵌套场景。问闭包时不是问定义而是直接甩一段循环输出题。这说明你背下来的知识点必须能和具体代码、具体场景关联起来。我建议把每一个面试题整理成自己的一个“最小案例集”每看到一个知识点就想一个“这个现象在项目里出现过吗”。6.2 项目经验要准备“技术决策”而不是“功能列表”这一点是对社招最重要的一条建议。我见过很多简历写“实现了 xx 模块”但面试官问“为什么这么实现”答不上来。我的做法是对简历里的每个项目拆成五个问题准备项目的核心业务目标是什么你负责的技术难点是什么你调研了哪些方案为什么最终选了这个上线后有没有出过问题怎么解决的如果再让你做一次你觉得哪里能做得更好这五个问题基本覆盖了面试官会追问的所有方向。另外每个项目要准备 3 到 5 个可以深挖的技术点并把它们记为“诱饵”。比如大文件上传项目里的“Web Worker 并发调度 断点续传”就很容易把面试官引到你擅长的地方。6.3 手写题要“边说边写”让面试官看到你的思路手写题最容易犯的错误是闷头写代码写完才和面试官说话。正确做法是边思考边表达先确认需求、再说明数据结构、写的过程中解释每一步的意图。甚至在写之前可以说“我先说一下我的整体思路您看方向对不对”这不仅能降低写错的风险还能让面试官觉得你是一个沟通顺畅的协作伙伴。我在写 TaskScheduler 时就先说了一句“我需要一个队列来保存等待执行的任务同时用一个计数器记录当前处于处理中的任务数量”面试官点了点头我才开始写。这比直接甩代码要稳妥得多。6.4 对业务的理解要提前做尤其是有明确业务属性的岗位飞书是 To B 协作工具前端不仅要做界面还要处理复杂的实时协同、离线编辑、权限控制等问题。如果你只是把前端当作“画页面的”那大概率会在这种岗位上吃亏。我在面试前把飞书文档里的“评论、划词、版本历史、多人光标”等功能都实际体验了一番思考背后可能的实现方式。这也让我在聊协作文档场景时显得像是真正对这个业务场景有研究的人。6.5 最后一个小技巧准备一份你的“技术清单”我所谓的“技术清单”不是简历也不是面试题而是一份写给自己看的能力清单。比如我会写我做过最复杂的数据结构是什么我为什么觉得它复杂我最近一个月读过的源码是哪个我当时关注什么问题我在项目里主动优化的最大一个性能瓶颈是什么有什么问题是我比大多数人研究得更深临面试前一晚我会快速扫一遍这份清单它的作用是帮我把“我的技术画像”在脑子里重新拉一遍。面试时无论被问到哪里我都能快速定位到对应的项目经验和知识储备。一面结束当晚我把所有问到的题目重新整理成了一份文档包括每一道题的解题思路、我当时的回答、现在补充的答案。这个习惯我保持了很多年每一次面试无论结果如何都会变成一次技术体系查漏补缺的机会。如果你也在准备大厂社招建议你也试试看这比盲目刷三遍面试题要有用得多。