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

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。
面经原文内容
📍面试公司:字节跳动
🕐面试时间:近期
💻面试岗位:前端一面
⏱️面试时长:45分钟
📝面试体验:面试官非常和善,会复述你的回答double check
❓面试问题:
实习项目穿插八股
- 登录页面怎么做的
- 后端传过来的jwt存在哪里?为什么放在cookie里?放在local storage里怎么携带的?
- sessionStorage和localStorage有什么区别
- 除了这种缓存还有其他的缓存手段(答了协商缓存、强缓存)
- 协商缓存和强缓存下浏览器的请求行为,协商缓存的相关字段+状态码
- 大文件上传怎么做的
- 为什么要用sse
- sse连接断开怎么办
- 接口降级方案具体是怎么实现的
- 主要观测的性能指标有哪些,LCP是怎么算的,具体LCP数值是多少
- 性能优化做了哪些内容
- vite和webpack的流程、区别
- 技术选型问题,为什么项目开发使用vue3不用react
- vue3的响应式
- vue编译渲染是怎么做的
coding(3道)
- 限制数量的事件调度器(Scheduler)
- hot100中的括号生成,问了一下时间复杂度
- 两个版本序列号排序问题
来源:牛客网 盐酸不酸
💡 木木有话说(刷前先看)
字节这场一面,是典型的"项目深挖+基础考察+算法"组合。面试官非常友善,会复述你的回答确认理解正确,这种风格下更容易发挥真实水平。尤其值得关注的是"接口降级方案"和"LCP具体数值",考察的是生产环境的实战思维。(最近的题多讲思路,因为重复题比较多,建议大家看汇总篇,根据汇总篇的热点考点去进行专项复习)
📝 字节前端一面·深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 面试风格 | 项目驱动型 + 友善沟通型 + 实战追问型 |
| 难度评级 | ⭐⭐⭐⭐(四星,覆盖面广,算法有区分度) |
| 考察重心 | 登录鉴权、缓存策略、大文件上传、SSE、性能优化、构建工具、Vue原理、算法 |
| 特殊之处 | 面试官会复述回答确认理解,考察沟通确认能力 |
🔍 逐题深度解析
一、登录页面怎么做的
回答思路:从表单交互、数据提交、token处理、路由守卫等方面说明。
要点:
- 表单校验(手机号/邮箱、密码格式)
- 提交时禁用按钮,防止重复提交
- 调用登录API,获取token
- token存储(cookie/localStorage)
- 登录成功跳转,失败展示错误提示
- 路由守卫:未登录时重定向到登录页
二、JWT存在哪里?为什么放cookie?localStorage怎么携带?
回答思路:对比存储方式的优劣。
存储位置:
- localStorage :需手动在请求头
Authorization: Bearer <token>携带 - cookie :设置
httpOnly防止XSS,设置Secure和SameSite,请求自动携带
为什么放cookie:
- httpOnly:防止XSS攻击窃取token
- 自动携带:无需手动处理请求头
- SameSite:可防范CSRF
localStorage携带方式:
javascript
// 存储在localStorage
localStorage.setItem('token', token)
// 请求拦截器添加
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
三、sessionStorage和localStorage区别
| 维度 | localStorage | sessionStorage |
|---|---|---|
| 生命周期 | 永久(手动清除) | 标签页关闭即失效 |
| 作用域 | 同源 | 同源+同标签页 |
| 容量 | 5-10MB | 5-10MB |
四、除了本地存储,还有其他缓存手段
回答思路:用户答了协商缓存、强缓存,正确。
HTTP缓存:
- 强缓存 :
Cache-Control: max-age=3600,缓存期间不发请求 - 协商缓存 :
ETag/If-None-Match,Last-Modified/If-Modified-Since,返回304则复用
其他:CDN缓存、Service Worker缓存、内存缓存(Memory Cache)。
五、强缓存和协商缓存的请求行为、字段、状态码
强缓存:
- 请求行为:缓存有效期内,不发请求,直接从缓存读取
- 状态码:
200 (from disk cache)或200 (from memory cache) - 字段:
Cache-Control(优先级高)、Expires
协商缓存:
- 请求行为:缓存过期后,发请求验证资源是否变化
- 状态码:
304 Not Modified(资源未变化)或200(资源更新) - 字段:请求头
If-None-Match(对应响应头ETag)、If-Modified-Since(对应Last-Modified)
六、大文件上传怎么做的
回答思路:核心是分片上传、断点续传、并发控制。
流程:
- 文件分片:Blob.slice()将文件切分成多个chunk(如每片1MB)
- 计算哈希:用spark-md5计算文件哈希,用于秒传判断
- 上传分片:并发上传(控制并发数),每个分片带序号
- 断点续传:服务端记录已上传分片,前端跳过已上传的
- 合并通知:所有分片上传完成后,通知服务端合并
javascript
// 分片上传示例
async function uploadFile(file) {
const chunkSize = 1024 * 1024 // 1MB
const chunks = Math.ceil(file.size / chunkSize)
for (let i = 0; i < chunks; i++) {
const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize)
const formData = new FormData()
formData.append('chunk', chunk)
formData.append('index', i)
formData.append('total', chunks)
await fetch('/upload', { method: 'POST', body: formData })
}
// 通知合并
await fetch('/merge', { method: 'POST', body: JSON.stringify({ fileName: file.name }) })
}
七、为什么要用SSE
回答思路:SSE适用于服务端向客户端单向推送的场景。
AI对话场景:
- LLM生成是服务端→客户端的单向流
- SSE基于HTTP,实现简单,自动重连
- 比WebSocket轻量,资源开销小
八、SSE连接断开怎么办
回答思路:SSE原生支持自动重连,但生产环境需做增强。
处理方案:
- 原生重连 :SSE的
EventSource会自动重连 - 自定义重试 :监听
onerror,实现指数退避重连 - 断点续传 :携带
Last-Event-ID,服务端从断点继续推送
javascript
let retryCount = 0
function connectSSE() {
const source = new EventSource('/api/stream')
source.onerror = () => {
source.close()
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000)
setTimeout(() => {
retryCount++
connectSSE()
}, delay)
}
source.onopen = () => { retryCount = 0 }
}
九、接口降级方案具体是怎么实现的
回答思路:降级是高可用设计,当主接口失败时,使用备用方案。
实现方式:
- 超时降级:设置超时时间,超时后走降级逻辑
- 失败降级:主接口报错后,调用备用接口或返回缓存数据
- 熔断降级:连续失败N次后,直接走降级,不请求主接口
- 限流降级:超出QPS限制时,返回默认数据或友好提示
javascript
async function fetchWithFallback() {
try {
const response = await fetch('/api/main', { timeout: 3000 })
return await response.json()
} catch (error) {
// 降级:使用缓存或备用接口
const cached = localStorage.getItem('cachedData')
if (cached) return JSON.parse(cached)
const fallback = await fetch('/api/fallback')
return await fallback.json()
}
}
十、主要观测的性能指标、LCP计算方式与数值
回答思路:核心Web Vitals包括LCP、FID、CLS。
LCP(Largest Contentful Paint) :最大内容绘制时间,衡量加载性能。通过PerformanceObserver观测页面中最大的元素(图片、视频、文本块)渲染完成的时间。
计算方式:
javascript
new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
console.log('LCP:', lastEntry.renderTime || lastEntry.loadTime)
}).observe({ entryTypes: ['largest-contentful-paint'] })
数值标准:
- 优秀:≤ 2.5秒
- 需改进:2.5-4秒
- 差:> 4秒
十一、性能优化做了哪些内容
回答思路:从加载、渲染、网络多个维度展开。
优化项:
- 代码分割:路由懒加载、动态导入
- 图片优化:WebP格式、懒加载、响应式图片
- 资源压缩:JS/CSS压缩、Gzip/Brotli
- 缓存策略:强缓存、协商缓存、CDN
- 虚拟滚动:长列表优化
- 减少重排重绘:用transform/opacity做动画
- 预加载:preload关键资源、prefetch低优先级资源
十二、Vite和Webpack的流程、区别
| 维度 | Webpack | Vite |
|---|---|---|
| 开发环境 | 打包所有模块,启动慢 | 利用ESM,直接启动 |
| 热更新 | 重新打包相关模块 | 只更新变更的模块 |
| 生产打包 | 统一打包成bundle | 使用Rollup预打包 |
| 配置复杂度 | 高 | 低 |
流程:
- Webpack:入口→依赖解析→loader转换→打包→输出
- Vite:启动服务器→请求时编译→利用浏览器ESM加载
十三、技术选型:Vue3 vs React
回答思路:结合项目需求说明。
Vue3优势:
- 上手简单,模板语法直观
- 响应式自动依赖收集,开发效率高
- 官方生态完善(Vue Router、Pinia)
React优势:
- 灵活度高,适合复杂交互
- 生态更丰富
- 函数式编程范式
选型原因:团队熟悉度、项目复杂度、社区生态。
十四、Vue3响应式
回答思路:基于Proxy实现,懒递归。
核心:
reactive:用Proxy代理对象,拦截get/set/deletePropertyref:包装基本类型,通过.value访问effect:依赖收集,触发更新- 懒递归:只有访问嵌套对象时才代理
十五、Vue编译渲染是怎么做的
回答思路:模板 → AST → 渲染函数 → VNode → 真实DOM。
流程:
- 解析:将模板字符串解析成AST(抽象语法树)
- 转换:对AST进行优化(标记静态节点)
- 生成 :生成渲染函数(
render函数) - 渲染:执行渲染函数生成VNode
- 更新:VNode与旧VNode进行diff,更新真实DOM
Coding题一:限制数量的事件调度器(Scheduler)
题目:实现一个并发限制的调度器,最多同时执行2个任务。
javascript
class Scheduler {
constructor(limit = 2) {
this.limit = limit
this.running = 0
this.queue = []
}
add(promiseFactory) {
return new Promise((resolve, reject) => {
this.queue.push(() => {
promiseFactory().then(resolve, reject).finally(() => {
this.running--
this.next()
})
})
this.next()
})
}
next() {
if (this.running < this.limit && this.queue.length) {
const task = this.queue.shift()
this.running++
task()
}
}
}
Coding题二:括号生成
题目:给定n对括号,生成所有有效的括号组合。
javascript
function generateParenthesis(n) {
const result = []
function backtrack(str, left, right) {
if (str.length === 2 * n) {
result.push(str)
return
}
if (left < n) backtrack(str + '(', left + 1, right)
if (right < left) backtrack(str + ')', left, right + 1)
}
backtrack('', 0, 0)
return result
}
时间复杂度 :卡特兰数,O(4^n / √n)
Coding题三:版本序列号排序
题目 :对版本号数组进行排序,如['1.0.1', '1.0', '1.0.2', '2.0']。
javascript
function compareVersion(v1, v2) {
const parts1 = v1.split('.').map(Number)
const parts2 = v2.split('.').map(Number)
const maxLen = Math.max(parts1.length, parts2.length)
for (let i = 0; i < maxLen; i++) {
const num1 = parts1[i] || 0
const num2 = parts2[i] || 0
if (num1 !== num2) return num1 - num2
}
return 0
}
function sortVersions(versions) {
return versions.sort(compareVersion)
}
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| 登录鉴权 | JWT存储(cookie/localStorage)、请求拦截器携带 |
| 存储区别 | localStorage永久/sessionStorage标签页/cookie自动携带 |
| HTTP缓存 | 强缓存(Cache-Control/Expires)、协商缓存(ETag/Last-Modified) |
| 大文件上传 | 分片(Blob.slice)、断点续传、秒传(文件哈希) |
| SSE | 单向推送、自动重连、断线恢复(Last-Event-ID) |
| 接口降级 | 超时降级、熔断、限流、缓存兜底 |
| LCP | 最大内容绘制,≤2.5s优秀,PerformanceObserver获取 |
| 性能优化 | 代码分割、图片优化、虚拟滚动、预加载 |
| Vite vs Webpack | 开发环境ESM/打包、热更新机制、生产构建 |
| Vue3响应式 | Proxy代理、懒递归、ref包装基本类型 |
| Vue编译 | 模板→AST→渲染函数→VNode→DOM |
| 调度器 | 并发限制、任务队列 |
| 括号生成 | 回溯、左右括号数量约束 |
| 版本排序 | 拆分解析、补0对齐 |
📌 最后一句:
字节这场一面,风格友善但内容硬核。从登录JWT存储、大文件上传,到LCP性能指标、Vue编译原理,再到三道算法题,全面考察了项目实战能力和计算机基础。面试官会复述你的回答double check,这种沟通方式值得学习------确认理解一致,是高效协作的关键。能通过这样的面试,说明你不仅会写代码,更能清晰地表达技术决策。