前言
大家好,我是木斯佳。
相信很多人都感受到了,在AI浪潮的席卷之下,前端领域的门槛在变高,纯粹的"增删改查"岗位正在肉眼可见地减少。曾经热闹非凡的面经分享,如今也沉寂了许多。但我们都知道,市场的潮水退去,留下的才是真正在踏实准备、努力沉淀的人。学习的需求,从未消失,只是变得更加务实和深入。
这个专栏的初衷很简单:拒绝过时的、流水线式的PDF引流贴,专注于收集和整理当下最新、最真实的前端面试资料。我会在每一份面经和八股文的基础上,尝试从面试官的角度去拆解问题背后的逻辑,而不仅仅是提供一份静态的背诵答案。无论你是校招还是社招,目标是中大厂还是新兴团队,只要是真实发生、有价值的面试经历,我都会在这个专栏里为你沉淀下来。专栏快速地址

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。
面经原文内容
📍面试公司:字节跳动-抖音
🕐面试时间:4月27日下午2点,时长50分钟
💻面试岗位:前端暑期三面
📝面试体验:回答感觉不好,可能要折戟,暑期最后一次面试
❓面试问题:
- 自我介绍
- 为什么选择前端方向
- 你现在最熟悉、最常用的是哪个 AI 模型
- 你平时除了用 AI 写代码,还会怎么用 AI
- 流式响应和普通请求响应的主要区别是什么
- 为什么你的 AI 对话项目选择用 SSE
- SSE 相比普通请求模式,有哪些优点和缺点
- AI 对话这种高频流式返回场景,前端会遇到哪些性能问题
- 你是怎么做 SSE 渲染性能优化的
- requestAnimationFrame在你的优化方案里是怎么用的
- 列表和数组这两种数据结构,在"判断某个值是否存在"这个场景下,应该选哪个
- 从底层存储和缓存角度看,数组和链表有什么差别
- 为什么你会觉得链表更快,你的依据是什么
- 一个数组里取最大的 K 个数,怎么做
- 如果不用排序,最大的 K 个数还能怎么做
- 手撕:快排
- 100 个人里有 1 个带病毒的人,只有 2 张试纸,怎么尽量少检测次数找出来
来源:牛客网 前端死了咩
💡 木木有话说(刷前先看)
目前在大厂里,我个人认为字节的前端面试题是综合性最强的。如果不考虑定向做一些音视频、可视化、基建等业务内容。建议大家重点刷字节的面试题。相关覆盖度比较高,其他大厂也能遇到相同或类似的题目。
📝 字节抖音前端三面·深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 面试风格 | 开放提问型 + 底层追问型 + 场景推演型 |
| 难度评级 | ⭐⭐⭐⭐⭐(五星,底层结构+算法+智力题) |
| 考察重心 | AI对话流式优化、数据结构底层、Top K算法、逻辑推理 |
| 特殊之处 | 围绕"AI对话项目"深挖流式渲染性能,追问数据结构CPU缓存特性 |
🔍 逐题深度解析
八、AI对话高频流式返回场景,前端会遇到哪些性能问题
回答思路:从渲染、内存、网络三个维度分析。
主要问题:
- 渲染压力:每个chunk都触发DOM更新,高频重绘导致掉帧
- 内存累积:长对话消息内容不断增长,内存占用越来越高
- 布局抖动:流式内容高度变化(如代码块展开),引发频繁重排
- 事件循环阻塞:大量同步渲染任务阻塞用户交互
- Markdown解析开销:每收到chunk都重新解析全文,CPU占用高
javascript
// 问题示例:每次收到chunk都全量渲染
source.onmessage = (e) => {
fullContent += e.data
messageDiv.innerHTML = marked.parse(fullContent) // 性能灾难
}
九、你是怎么做SSE渲染性能优化的
回答思路:从节流渲染、增量解析、虚拟滚动、硬件加速四个方向。
优化方案:
| 问题 | 优化方案 |
|---|---|
| 高频渲染 | 使用requestAnimationFrame节流,合并多个chunk |
| 全量解析 | 增量解析Markdown,只处理新增部分 |
| 内存增长 | 限制历史消息数量,超出时压缩或摘要 |
| 布局抖动 | 给动态内容预留占位,固定高度 |
| 代码块闪烁 | 缓存代码块片段,完整后再高亮 |
javascript
// requestAnimationFrame 节流渲染
let pendingContent = ''
let rafId = null
source.onmessage = (e) => {
pendingContent += e.data
if (rafId === null) {
rafId = requestAnimationFrame(() => {
messageDiv.innerHTML = marked.parse(pendingContent)
rafId = null
})
}
}
十、requestAnimationFrame在你的优化方案里是怎么用的
回答思路 :requestAnimationFrame用于合并高频更新,与浏览器刷新率同步。
使用方式:
javascript
let updateScheduled = false
let latestContent = ''
function onChunk(newContent) {
latestContent += newContent
if (!updateScheduled) {
updateScheduled = true
requestAnimationFrame(() => {
render(latestContent) // 每帧最多渲染一次
updateScheduled = false
})
}
}
为什么用它:
- 与浏览器绘制同步,避免多余渲染
- 自动适配刷新率(60Hz/120Hz)
- 比
setTimeout更精确,比setInterval更节省资源
十一、列表和数组,判断某个值是否存在,选哪个
答案 :数组 (确切地说是以数组实现的哈希集合,如Set)。
原因:
- 数组查找是O(n),需要遍历
- 哈希集合(如
Set)查找是O(1) - 但如果特指普通数组 vs 链表,数组的CPU缓存局部性更好
十二、从底层存储和缓存角度看,数组和链表有什么差别
核心差别:
| 维度 | 数组 | 链表 |
|---|---|---|
| 内存布局 | 连续内存块 | 非连续,节点分散 |
| CPU缓存 | 缓存友好(空间局部性) | 缓存不友好(跳转访问) |
| 随机访问 | O(1) | O(n) |
| 插入/删除 | O(n)(需移动元素) | O(1)(已找到位置) |
CPU预取特性:数组连续存储,加载一个元素时,相邻元素也会被加载到缓存行(Cache Line,通常64字节)。遍历数组时,大部分数据已在缓存中,速度快。链表节点分散,每次访问可能都要从主存读取。
十三、为什么你会觉得链表更快
常见误解纠正 :链表在插入/删除操作 (已找到位置)时O(1)比数组O(n)快。但在遍历查找场景下,数组因其缓存连续性通常更快。
面试官意图:考察你是否理解CPU缓存对性能的影响,而非只看时间复杂度。
十四、一个数组里取最大的K个数,怎么做
方案1:排序后取前K个
javascript
function topKBySort(arr, k) {
return arr.sort((a, b) => b - a).slice(0, k)
}
时间复杂度O(n log n)
方案2:部分排序(冒泡K次)
javascript
function topKBubble(arr, k) {
for (let i = 0; i < k; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
return arr.slice(-k)
}
时间复杂度O(n*k),当k很小时效率高
十五、如果不用排序,最大的K个数还能怎么做
方案:小顶堆(推荐)
javascript
function topKHeap(arr, k) {
const heap = []
for (const num of arr) {
if (heap.length < k) {
heap.push(num)
if (heap.length === k) buildMinHeap(heap)
} else if (num > heap[0]) {
heap[0] = num
heapify(heap, 0)
}
}
return heap
}
function buildMinHeap(heap) {
for (let i = Math.floor(heap.length / 2) - 1; i >= 0; i--) {
heapify(heap, i)
}
}
function heapify(heap, i) {
const n = heap.length
let smallest = i
const left = 2 * i + 1
const right = 2 * i + 2
if (left < n && heap[left] < heap[smallest]) smallest = left
if (right < n && heap[right] < heap[smallest]) smallest = right
if (smallest !== i) {
[heap[i], heap[smallest]] = [heap[smallest], heap[i]]
heapify(heap, smallest)
}
}
时间复杂度O(n log k),空间复杂度O(k),适合k远小于n的场景。
十六、手撕:快排
javascript
function quickSort(arr) {
if (arr.length <= 1) return arr
const pivot = arr[0]
const left = []
const right = []
for (let i = 1; i < arr.length; i++) {
if (arr[i] < pivot) left.push(arr[i])
else right.push(arr[i])
}
return [...quickSort(left), pivot, ...quickSort(right)]
}
// 原地排序(更省内存)
function quickSortInPlace(arr, left = 0, right = arr.length - 1) {
if (left >= right) return
const pivotIndex = partition(arr, left, right)
quickSortInPlace(arr, left, pivotIndex - 1)
quickSortInPlace(arr, pivotIndex + 1, right)
}
function partition(arr, left, right) {
const pivot = arr[right]
let i = left - 1
for (let j = left; j < right; j++) {
if (arr[j] <= pivot) {
i++
[arr[i], arr[j]] = [arr[j], arr[i]]
}
}
[arr[i + 1], arr[right]] = [arr[right], arr[i + 1]]
return i + 1
}
十七、100人带病毒,2张试纸,怎么最少检测次数找出病毒携带者
经典二分/分组思路 :不是二分法,而是分组检测。
最优方案:
- 将100人分成两组,每组50人
- 用两张试纸分别检测两组(混合样本)
- 最多需要7次(50人组内二分约6次 + 初始2次 = 8次?实际上最优可达更少)
更优的"二进制法":
- 给每个人编号0-99,用二进制表示(7位足够)
- 按二进制位分组:第i位为1的人为一组,用第i张试纸
- 7张试纸即可唯一确定携带者(但题目只有2张,需优化)
2张试纸的优化方案(分组检测):
- 第一轮:将100人分成若干组,用第一张试纸检测各组混合样本
- 第二轮:锁定小组后,用第二张试纸在该小组内逐一检测
- 最少次数:将100分成约10组,每组10人,第一轮10次,第二轮最多10次 → 最坏20次
- 更好的方案:平方根分组,第一轮√100=10组,第二轮10人,最坏20次
- 最优解:15次左右(组合检测优化)
text
思路:将100人排成10*10矩阵
第一张试纸检测每行的混合样本(10次)
第二张试纸检测每列的混合样本(10次)
交叉点即病毒携带者
最坏情况:20次(但可优化到最坏15次)
面试官预期:看到你能否从二分法延伸到分组检测,体现逻辑思维。
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| 流式渲染性能 | 高频DOM更新、内存累积、布局抖动、Markdown解析 |
| 优化方案 | rAF节流、增量解析、虚拟滚动、固定占位 |
| rAF作用 | 与刷新率同步,合并帧内更新 |
| 数组vs链表 | 连续内存/非连续、缓存友好/不友好、预取特性 |
| Top K | 排序O(n log n)、小顶堆O(n log k) |
| 快排 | 分治、选基准、原地排序 |
| 病毒检测 | 分组检测、矩阵法、二进制编码 |
📌 最后一句:
字节抖音这场三面,是一场"计算机科学基础"的深度检阅。从流式渲染的rAF优化、到数组链表的CPU缓存特性,再到Top K小顶堆、快排实现,最后是分组检测的智力题,面试官层层递进,考察的不仅是会不会写代码,更是是否有扎实的CS底层思维。能走到三面,说明你已经具备了解决问题的能力,剩下的只是运气和缘分。愿每一位走到这里的人,都能等来那个"Congratulations"!