十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

抖音前端一面实录:React面试题与性能优化复盘

抖音前端一面实录:React面试题与性能优化复盘 前几天刚面完抖音前端的一面趁热把整个过程和复盘思路写下来。这篇不打算写成“面试题答案速查”那就浪费了。我尽量还原当时被问的问题、面试官追问的角度、我回答时的思路以及哪些地方我答得不够好、后来怎么补的。如果你正在准备大厂前端面试尤其是偏 React 技术栈的这篇应该能帮你在“知道答案”和“能讲清楚”之间补上那段差距。开始之前先交代下我的情况方便你对号入座两年前端经验主技术栈是 React TypeScript工作中做过中后台系统也接手过面向 C 端的 H5 项目简历上的重点项目是一个数据可视化中台。这次面的抖音前端岗位base 北京一面全程约 70 分钟。1. 一面到底在筛什么从面试官视角拆解考察点很多人准备一面喜欢刷海量面试题我这次面完最大的感受是抖音的一面甚至可以说字节系的一面核心不是考你会背多少八股而是看你的基础知识是不是“活”的能不能在工作中真正用起来。字节的面试官普遍有个习惯——他们很少直接问“请说明一下闭包的原理”这种宽泛问题而是给你一个场景或者从你自己的项目里抽一个点然后顺着你的回答一路追下去像剥洋葱一样直到碰到你的边界。一面能撑多久、追问到多深其实取决于你自己把项目铺得多开。我把这次一面实际遇到的考察点整理了一下大致是这四个维度考察维度实际问到的方向占比感受项目深挖首屏性能优化、组件复用设计、复杂状态管理约 40%网络与浏览器HTTP 缓存、跨域、渲染流程、事件循环约 25%React 原理与手写Fiber 与 hooks 原理、防抖、深拷贝、Promise约 25%场景与开放题大文件上传、前端监控、微前端约 10%注意这个占比项目深挖永远是大头。因为这能看出你是一个“调包侠”还是一个真正写过东西、踩过坑的人。所以准备一面之前我花了一整天重新梳理简历上的项目把每个技术点能往下追问两到三层的问题都列出来。这一点后面细说。还有一个容易被忽略的点一面本来就有“压力面”的成分在里面不是问你多难而是看你在连续追问下能不能保持思路清晰能不能坦诚说“这个我不太清楚”而不是硬编。这一点我觉得比答对某道题更重要。2. 项目深挖环节我准备的性能优化案例是怎么被层层追问的自我介绍结束后面试官直接说“你讲讲那个数据可视化中台项目吧我看你写了首屏加载优化具体怎么做的”这一听就知道后面全是坑。2.1 从“怎么统计首屏时间”开始的追问链条我当时说的是一个数据中台的前端页面里图表库很重ECharts 打包后体积不小首屏加载白屏时间一度到 4 秒以上我做了三件事路由级代码分割、ECharts 按需引入、骨架屏。这回答本身不难难的是面试官接下来的每一步追问。他第一个追问就是“你怎么知道优化后从 4 秒变成了 1.8 秒这个时间是怎么统计出来的”这个问题其实是在考察“性能优化闭环”里最容易忽视的一环——你怎么量化优化效果。如果只是凭感觉说“感觉快了”那就露馅了。我当时回答的是用 Performance API 里的PerformanceObserver去采集first-contentful-paint和dom-content-loaded再加上项目里接的监控平台上报了load事件耗时。他又追了一句“那你有没有对比过 optimized 和 unoptimized 的同口径数据”意思是优化前后的统计数据是不是在同一个标准下测的这个细节真的很容易被忽略。我当时承认了优化前我只统计了window.onload优化后加了FCP指标严格来说口径不完全一致。面试官反而点头说“能发现这个问题说明你真看过数据”。2.2 代码分割的坑公共代码被重复打包接着他问路由级代码分割具体怎么做的。我说的是 React.lazy Suspense加 Webpack 的splitChunks把 node_modules 里的公共依赖单独抽出来。他马上追问“那splitChunks的chunks参数你设置的什么all、async还是initial如果设成all会不会把同步引入的代码也拆出去造成公共代码被重复加载”这问题有点冷门但也确实是我实际配过才知道的。我当时配的是all确实把一些同步引用的公共模块也抽了 chunk导致部分页面多了额外的请求。这里我自己的经验是splitChunks不是配置得越细越好默认配置对大部分项目就够用了一定要在做了 performance budget 之后针对打包产物体积确实过大的模块再单独抽。面试官显然在这个业务场景下有实战经验对于“配置项背后的取舍”非常敏感。2.3 组件设计与状态管理从“哪个组件最复杂”到请求竞态项目讲完面试官话锋一转“你刚才提到那个数据中台的筛选器组件复杂在哪如果让你重构你会怎么拆”这是一个典型的“组件设计”考察点重点看你会不会从复用性、可维护性、外部属性设计这些层面去思考。我提到三个问题筛选条件多超过二十个表单字段、筛选条件和 URL 参数要联动、不同筛选条件组合会触发不同的接口请求顺序。他的追问是“那你怎么处理快速切换筛选条件导致的请求竞态”这个我确实踩过坑项目里初期直接用axios的CancelToken后来切到了AbortController。这里我现场给了一个简单示例useEffect(() { const controller new AbortController(); fetch(/api/list?params${JSON.stringify(filters)}, { signal: controller.signal }) .then((res) res.json()) .then((data) setList(data)) .catch((err) { if (err.name ! AbortError) { // 真实错误处理 } }); return () controller.abort(); }, [filters]);面试官点了点头又补了一个问题“如果不用 AbortController用useRef存一个递增序号来忽略旧请求返回这种方案你怎么看”这个点非常有意思因为他不是让你否定某一种方案而是看你能不能分析两种方案的适用边界。我的回答是AbortController能真正取消网络请求减少带宽浪费但会多一层事件监听逻辑序号方案没有真正终止请求但代码侵入小、与 UI 状态解耦更好在请求库没有暴露取消机制时是个不错的降级方案。这种对比分析远比单背一种方案更能体现功力。3. 从 URL 输入到事件循环网络与浏览器基础的高频连环问项目环节大概聊了 20 多分钟面试官切到了基础题。字节一面的基础题范围比较集中在网络、浏览器和 JS 运行时这几个板块而且问法通常是从一个实际问题切入。3.1 HTTP 缓存的“最后一层”他先是问了一个很常见的场景“页面里有个 logo 图片你希望用户第二次访问时不走网络怎么设置响应头”这其实是考察强缓存与协商缓存。我回答强缓存设置Cache-Control: max-age31536000面试官追问了一个很容易忽略的点“那如果这个 logo 文件更新了文件名不变用户在第二次访问时拿到的还是旧缓存你怎么处理”这一层是缓存的经典坑标准解法是给文件名加 hashWebpack 打包时输出logo.a3f9d.png这样的文件名资源更新后哈希变化自然请求新文件。他又追问“哈希有hash、chunkhash、contenthash你平时用哪个”这个必须搞清楚contenthash是拿文件内容生成的文件不变哈希不变适合长效缓存chunkhash按 chunk 的粒度变化当你一个 chunk 里只有一个入口文件时它和 contenthash 差不多但 js 文件里如果动态 import 了 css可能 css 变化连带 js 文件 hash 也变。这块我用一个表格来总结一下面试复习很有用缓存策略关键响应头触发条件常见问题强缓存Cache-Control: max-agexxx缓存未过期直接命中文件更新后仍需请求新资源协商缓存ETag/Last-Modified缓存过期后向服务器验证需要一次额外的网络请求文件名哈希contenthash内容变化导致的新 URL需要构建工具配合3.2 跨域不要只背 CORS 的流程跨域问题也是必问的。面试官直接问“你们项目里前端和服务端不在同一个域CORS 预检请求什么时候会触发怎么减少预检”我回答非简单请求会触发比如application/json的 POST、自定义请求头、还有带PUT/DELETE方法。他追问到“OK 那如果不用 CORS你还有什么方案”我提了 dev 环境下用 Vite 的 proxy线上走 Nginx 反向代理。他追问“这两种方案的核心区别是什么”关键点在于 CORS 是浏览器协议层面对非同源请求的限制Nginx 代理是同源转发从根本上规避了跨域。还有一个更冷门但面试官可能追问到的点是Nginx 配置反代时还要注意Access-Control-Allow-Origin如果是动态设置的话Vary: Origin响应头不能丢不然缓存代理会给不同来源的用户返回错误的 CORS 头。3.3 渲染流程、回流与事件循环的串问他给了一个场景“用户打开页面输入框输入一段文字页面发生什么”这个其实串起了渲染管线里的“任务队列 样式计算 布局 绘制 合成”几乎全链路。我从事件循环开始讲浏览器把输入任务放入任务队列如果此时主线程正在跑长任务输入事件会被延后处理这就是为什么长任务会阻塞交互。然后讲到任务处理结束后进入渲染时机浏览器会执行样式计算、布局、绘制、合成。他立刻问“那 Vue 和 React 的异步更新机制和这个有什么关系”这个问题很妙它把框架特性和浏览器渲染绑在了一起。我回答的时候提到了 React 的调度器优先保证用户输入这类高优先级任务能被及时处理这和浏览器的事件循环模型是配套设计的。然后他带着追问到了回流和重绘问我“哪些属性会导致回流transform为什么比left性能好”。我答了几何属性会触发布局像offsetWidth、scrollTop这些属性读取会强制同步布局如果读到的一个个属性正好和刚才写入的样式有依赖就会产生 layout thrashing。transform只会触发合成层不会触发布局和重绘所以性能好。这块我平时写动效的时候会刻意用transform代替top/left但很多人写业务代码的时候是想不到这层的面试官想听的其实就是你“有没有这个意识”。4. React 原理与手写代码一面里最容易暴露底子的环节到了这个环节面试官说“我们做几道题吧不用跑讲思路、写核心代码就行。”抖音的前端技术栈里 React 占了大头所以 React 原理和 JS 手写题基本上是一面的“标配分水岭”。4.1 Fiber 架构从“为什么需要”而不是“是什么”切入他先问“你了解 React 的 Fiber 架构吗它解决了什么问题”很多人在这一步就开始背 reconcile、链表、双缓存但没回答清楚“动机”。我的思路是先说痛点在 React 15 及以前虚拟 DOM 的递归 diff 是一口气执行完的不能中断如果组件树很大、一次更新涉及几千个组件主线程会被占住很久用户输入、动画都会被卡。Fiber 做的事情是把一次更新拆成一个个小单元——每个组件对应一个 Fiber 节点全部节点连接成链表结构React 可以“做一点、停一下、看看主线程有没有更紧急的活”没有就继续这种协作式调度就是 Fiber 解决的核心问题。面试官接着问“那为什么是链表结构用数组不行吗”这个点想答好的话你得明白链表在中断恢复场景下的优势它不依赖下标和连续内存只需要记住当前处理到哪个节点就能在任意位置暂停和恢复而且兄弟节点的查找是 O(1) 的这对深度遍历很友好。数组的话你打断一次之后要恢复你得记录索引而且数组的插入删除成本更高设计上就没这么自然。4.2 hooks 闭包陷阱一个经常被忽略的细节React 原理之后是 hooks。面试官问“你写过自定义 hook 吗有没有遇到过闭包陷阱”我说遇到过比如在setTimeout回调里读某个状态读到的总是旧值。这是闭包导致的经典问题——函数捕获的是它创建时那个渲染周期里的值。他顺着问“那你怎么解决用useRef和直接把这个值塞进依赖数组有什么区别”我当时答了useRef能确保回调里读到的永远是最新的值适合给 setTimeout、事件监听这种“脱离 React 渲染周期”的场景使用如果只是 useEffect 依赖直接塞依赖数组也可以但 setTimeout 是注册一次之后长期存在依赖数组方案会重新注册定时器逻辑上不够干净。面试官补了一句“那你理解为什么useRef可以跨渲染周期拿到值吗因为它返回的就是同一个对象引用你改的是对象的.current属性。”这算是对上面回答的一个很好的收尾。4.3 手写题的答题节奏从最简单版到边界版手写题他挑了三道防抖、深拷贝、手写 Promise.all。听起来很基础但字节的面试官特别爱追问边界条件。先看防抖function debounce(fn, delay, immediate false) { let timer null; let invoked false; return function (...args) { if (immediate !invoked) { fn.apply(this, args); invoked true; return; } if (timer) clearTimeout(timer); timer setTimeout(() { fn.apply(this, args); invoked false; timer null; }, delay); }; }他没问 immediate 版本追问的是“如果用户在延迟时间里连续触发了 10 次你的定时器是每次都被清掉再重开还是只在最后一次触发时创建”正确答案是每次触发都 clearTimeout 再重新 setTimeout这样只有最后一次生效。这里还要说一句防抖的this绑定也得是传入函数被调用时的上下文用fn.apply(this, args)这一点很多简写版都会漏。然后是深拷贝。基础版本很简单但面试官的追问是“你考虑 Symbol 和函数了吗你考虑循环引用了吗Date、RegExp 这类特殊对象要怎么处理”这是标准答案里的三个层次function deepClone(value, map new WeakMap()) { if (value null || typeof value ! object) return value; if (value instanceof Date) return new Date(value); if (value instanceof RegExp) return new RegExp(value.source, value.flags); if (map.has(value)) return map.get(value); const clone Array.isArray(value) ? [] : {}; map.set(value, clone); Reflect.ownKeys(value).forEach((key) { clone[key] deepClone(value[key], map); }); return clone; }用Reflect.ownKeys是为了把 symbol 键也算进去用WeakMap是为了处理循环引用。面试官会问你为什么用 WeakMap 而不是 Map因为如果原对象不再被引用WeakMap 里的键值对也会被垃圾回收防止内存泄漏。最后是手写 Promise.allfunction promiseAll(promises) { return new Promise((resolve, reject) { if (!Array.isArray(promises)) { reject(new TypeError(promises must be an array)); return; } const results []; let remaining promises.length; if (remaining 0) { resolve(results); return; } promises.forEach((item, index) { Promise.resolve(item).then((value) { results[index] value; remaining - 1; if (remaining 0) resolve(results); }, reject); }); }); }核心点有三个要用Promise.resolve包一层兼容非 Promise 值结果数组需要用索引存储保证顺序和传入顺序一致要用一个计数器判断是否全部完成而不是用results.length promises.length因为如果某个结果是 undefinedlength 也算进去了逻辑就错了。面试官还追了一句“如果其中一个 Promise 先 reject 了但其他还在跑你会怎么处理”答案是最终状态由先 reject 的那个决定因为 Promise 的状态一旦变更不可逆后续 resolve/reject 都会被忽略。5. 场景设计题大文件上传、监控与微前端的开放式答法手写题结束之后面试官进入了一个更开放的环节“你项目里有没有遇到过一个交互很复杂、需要前端设计方案的功能你怎么设计”我提了大文件上传这几乎是前端的经典面试题因为在面试官看来它能串起非常多细节文件切片、并发控制、断点续传、秒传、服务端合并、进度计算。字节也很吃这一套。5.1 大文件上传的四层设计我按这个思路回答也是我自己项目中实际采用过的方案第一层文件分片。用file.slice(start, end)把一个大文件切成固定大小的 chunk比如每片 2MB。分片大小要权衡太小会导致请求数量过多太多 HTTP 连接对服务器压力大太大又会导致失败重试的粒度太粗。2MB 到 5MB 是实践里比较常见的选择。第二层并发控制。不能一次性把所有分片全部发出去浏览器对同域名的并发连接数有限制而且会拖垮服务端。我用了p-limit做并发限制把并发数设为 3 或 5每完成一个就从队列里拉一个新的。第三层断点续传。一旦中途失败需要知道哪些分片已经上传成功了。方案是客户端维护一个Set记录每个分片的 hash上传前向后端发一个“查询已上传分片”的请求后端返回已完成分片列表前端只上传缺失的部分。分片 hash 一般用文件的 md5 生成但要注意大文件计算 md5 本身也耗时所以实际项目里可以用增量 hash 或者抽样 hash 来优化。第四层秒传。把整个文件的 hash 发给后端后端判断已经存在这个文件就直接返回上传完成前端不再传任何分片。这个方案的核心是文件 hash 的计算稳定性前后端必须用同一种算法。我在回答时用一个表把四个层次和对应的目的讲清楚了层次技术手段解决的问题分片file.slice 固定大小分片单文件过大上传失败成本高并发控制p-limit或自定义队列并发数过高导致连接和服务器压力断点续传分片 hash 已上传列表查询中断后需要重新上传秒传全文件 hash 服务端校验相同文件重复上传浪费带宽面试官追问了一个很细的环节“用户上传到一半时切到后台浏览器真的会暂停网络请求吗你怎么保证续传”这个问题的关键点在于移动端浏览器在页面切到后台后网络请求可能被系统挂起但你的fetch超时时间如果在服务端那请求状态可能已经不可控。所以设计方案时不能只靠“浏览器不销毁页面”还要在前端主动捕获应用进入后台的时机把当前已上传完成的分片列表同步到 localStorage这样下次打开页面时能恢复上传进度。面试官听到这里明显更满意因为这确实是从真实线上故障里才能总结出来的经验。5.2 前端监控从错误收集到性能埋点他还顺带问了一个更偏工程化的场景题“如果你负责的前端项目凌晨突然报警你第一件事做什么”这是典型的“监控与定位”问题。我的回答分三步先看告警优先级如果只是少量用户报错先看用户分布、版本号、报错堆栈筛选出影响面如果影响面大再看是发版引入了回归还是依赖的 CDN 资源挂了还是后端接口大面积超时。面试官追问“前端怎么自己监控到这种情况”我提到了错误边界ErrorBoundary捕获渲染异常、window.onerror和unhandledrejection捕获未处理异常、PerformanceObserver 采集 LCP/INP 等核心性能指标上报时带上userId、pageUrl、stackTrace、deviceInfo。这里要考虑上报的可靠性用navigator.sendBeacon在页面卸载时也能发出请求这是fetch做不到的。5.3 微前端知道“为什么”比知道“是什么”更重要因为项目背景提到中台面试官问了一句“你觉得中台前端有必要做微前端吗”这个问题是个温柔的陷阱因为如果上来就列 qiankun、single-spa、webpack Module Federation 怎么配反而会让面试官失望。我直接从动机出发如果中台有多个团队并行开发、技术栈不统一一部分 React、一部分 Vue而且构建产物需要独立部署微前端才有价值。如果只是一个团队维护、统一技术栈的项目引入微前端只会增加复杂度——应用间通信、路由隔离、样式隔离、依赖重复加载这些都是沉重的成本。面试官点头说明这个答法比较合他的口味。他之后追问“那样式隔离怎么做”我说常见有四个手段子应用构建时给类名加前缀CSS Modules 归入子应用自己的构建链路动态插入style标签时给选择器包一层作用域以及 shadow DOM这个实践中比较少用因为兼容和事件处理都比较麻烦。聊这种题不追求标准答案关键是让人看到你有过取舍思考。6. 面试节奏与反问那些面经里没人细说的细节上面其实是面经的主体。但真的去面过一次之后你会发现除了知识点还有几个直接影响面试体验和结果的因素这部分反而值得单独聊一聊。6.1 不会的题怎么自然地“撑过去”面到一半的时候面试官问了一个我确实没准备过的问题“你对 BFC 是怎么理解的能给我举两个实际的应用场景吗”BFC 这个概念我平时是知道的复习的时候也看过但不知道是不是因为已经面了一个多小时脑子有点卡壳我第一反应是“完了”接下来 5 秒钟我的脑子没转出任何具体的例子。我没有硬编直接跟面试官说“BFC 这个概念我知道但您让我现场立刻举两个实际场景我一下子没想出来能给我一点时间梳理一下吗……能想到的第一个应用是清理浮动带来的父元素高度塌陷问题。第二个是避免 margin 穿透我给子元素加 margin不想影响父元素的外边距时可以把父元素变成 BFC 容器。”面试官没有继续深挖这个问题我事后复盘觉得这种回应方式比愣住不吭声或者背答案要强因为面试官不是要考倒你而是想看到你在压力下不会崩而且不会被一个问题带走节奏。后来我又想了一下这个问题现场答得其实不够深面试官大概率是想听“BFC 是块级格式化上下文内部的块级盒子有自己的布局规则和外界隔离”。如果让我重新答我会多说一句“BFC 的隔离性正是它的价值所在很多看似奇怪的布局问题都是因为元素处于同一个 BFC 里导致的”。6.2 反问环节的价值从问题里展示思考深度面试官到末尾问我“你有什么想问我的”这种环节千万不要说“没有”因为这是你展示自己在“招聘”这家公司而不只是“被面试”。我当时问了两个问题第一个是“抖音前端目前 React 和跨端技术的发展节奏是怎样的新人进来之后一般是从业务页面入手还是从工程化设施入手”这个问题能看出团队的技术氛围和成长路径面试官也认真回答了很久。第二个是“团队目前上线的监控体系覆盖到什么程度有没有专门做前端稳定性和性能优化的方向”这个问题看似是问业务实际是在告诉面试官“我不只是来写页面的我对工程质量有兴趣”。这比那些“一天几顿饭、加班多不多”的问题更有信息量。6.3 复盘比刷题更重要我从这次面试里学到的其实面完之后当天晚上我没有立刻去准备二面而是做了一次完整的复盘。我把整个面试过程录音下来跟面试官打过招呼这个看情况不一定每个都允许然后逐句听把每一个没答透的点都记下来整理成一张表格问题、我当时怎么答、标准答案、我能改进的空间。这个过程大概花了我两个晚上但收获比刷几百道题都大。复盘里我发现了一个自己很容易犯的毛病回答问题时太急着讲“怎么做”而忽略了“为什么这么做”。很多问题面试官真正想听的其实是“你为什么会有这个判断”比如大文件上传为什么选 2MB 而不是 20MB那是因为 2MB 在大多数网络条件下单分片耗时约 1 到 3 秒失败重试的粒度合适而且服务端临时空间占用也可控比如代码分割为什么选 React.lazy 而不是手动动态 import那是因为 React.lazy 能配合 Suspense 自动维护加载态减少手动维护 loading 的样板代码。这些“为什么”才是区分熟练工和思考者的关键。最后再说一个心态层面的东西一面没过不代表你基础差很多时候只是项目匹配度和表达方式的差异一面过了也别飘二面的深度通常翻倍。把每一场面试当成一次免费的水平测试拿到反馈之后去补短板这比结果本身重要得多。
返回列表