外观
前端小杰抖音面试题整理(第一版)
数据来源:抖音公开主页首屏可见作品标题,账号「前端小杰」。整理日期:2026-07-10。
说明:当前版本只覆盖未登录状态下可稳定获取的 20 条公开视频标题;答案为扩展整理版,不代表原视频逐字内容。
使用建议
这份文档适合用来做前端中高级面试复习。每个题目建议按「一句话结论 -> 核心原理 -> 项目落地 -> 风险与权衡」来回答,尤其是工程化、性能、AI 原生前端相关题目,不要只背概念,要能讲出你在项目里怎么做、为什么这么做、效果如何衡量。
题目目录
1. 前端面试宝典
面试官想看什么: 不是看你背了多少八股,而是看你的知识体系是否完整、项目经历是否真实、遇到复杂问题时是否能做判断。
示范回答:
我准备前端面试会分成五块:语言基础、浏览器原理、框架原理、工程化能力、项目与性能优化。基础层面重点是 JavaScript 执行机制、闭包、原型链、异步任务、事件循环、浏览器缓存、跨域和渲染流程。框架层面会准备 React/Vue 的响应式、组件通信、状态管理、性能优化以及常见源码机制。工程化层面会关注 Vite/Webpack、代码拆分、CI/CD、规范化、监控和错误上报。最后会把项目经历整理成 STAR 模型:背景是什么、问题在哪里、我做了什么、最终指标提升多少。
加分点:
- 能把技术点和业务结果关联起来,例如首屏时间降低、构建时间缩短、错误率下降。
- 能讲清楚方案的取舍,而不是只说“用了某个库”。
- 能承认边界,比如某些优化在数据量小、用户量少时不一定值得做。
2. 为什么 Vite 速度比 Webpack 快
一句话结论: Vite 在开发阶段利用浏览器原生 ESM 按需加载源码,并用 esbuild 做依赖预构建;Webpack 开发启动通常需要先构建完整依赖图,所以冷启动和热更新更慢。
示范回答:
Vite 快主要体现在开发阶段。它不会一开始就把整个项目打成一个 bundle,而是启动一个开发服务器,把源码以 ESM 的形式交给浏览器按需请求。依赖包通常变化少,Vite 会先用 esbuild 进行预构建,esbuild 是 Go 写的,解析和转换速度很快。业务源码变化时,Vite 只需要让相关模块失效并重新请求,HMR 边界更小。
Webpack 的核心模型是从入口开始构建完整模块依赖图,再经过 loader、plugin、打包和优化流程,即使有缓存,项目变大后冷启动和增量构建成本也更高。不过生产构建不能简单说 Vite 一定更快,因为 Vite 生产环境主要依赖 Rollup,Webpack 经过长期优化,在复杂场景下也有成熟能力。
追问方向:
- Vite 的依赖预构建解决了什么问题?
- 为什么 CommonJS 依赖需要预构建成 ESM?
- Vite 生产构建为什么使用 Rollup?
3. 项目开发中有遇到什么挑战没
回答原则: 不要回答“没有”。面试官在考察你是否参与过真实复杂度,以及你解决问题的方法论。
示范回答:
有。比如我在一个前端项目里遇到过大文件上传稳定性问题。用户上传大文件时,一旦网络波动就要重新上传,体验很差,也浪费服务器资源。我把上传流程拆成文件 hash 计算、切片、并发上传、失败重试、断点续传、合并校验几个阶段。前端用队列控制并发数量,失败切片按指数退避重试;后端记录已上传切片,前端重新进入页面时先查询上传进度,只补传缺失部分。最后大文件上传失败后的恢复成本明显降低,用户不需要从头等待。
可复用结构:
- 背景:业务场景是什么。
- 问题:慢、卡、错、不可维护、不可观测。
- 方案:技术拆解和关键决策。
- 结果:最好用指标说明。
- 复盘:还有什么可以改进。
4. POST 为什么会发送两次请求
一句话结论: 常见原因是浏览器针对跨域非简单请求先发送了 CORS 预检请求 OPTIONS,通过后才发送真正的 POST。
示范回答:
POST 看起来发送了两次,最常见的是跨域预检。CORS 中请求分为简单请求和非简单请求。如果 POST 的 Content-Type 不是 application/x-www-form-urlencoded、multipart/form-data、text/plain,或者带了自定义请求头,比如 Authorization,浏览器会先发一个 OPTIONS 请求询问服务端是否允许该跨域请求。服务端返回允许的 origin、methods、headers 后,浏览器才会继续发送真正的 POST。
另外也要排查开发环境 React StrictMode 导致副作用执行两次、表单重复提交、请求拦截器重复注册、按钮重复点击、页面重试机制等情况。
解决思路:
- 后端正确处理
OPTIONS并返回 CORS 响应头。 - 合理设置
Access-Control-Max-Age缓存预检结果。 - 前端避免重复绑定事件,提交按钮加 loading/disabled。
- 开发环境下区分 StrictMode 的双调用和生产真实问题。
5. 你在开发过程中有什么困难点
回答重点: 困难点要具体,最好能体现工程判断,而不是简单说“时间紧”“需求变”。
示范回答:
我遇到过比较典型的困难是需求快速变化下如何保持前端模块可维护。最开始页面逻辑都写在组件里,随着状态、权限、接口和交互越来越多,组件变得很难维护。我后来把页面拆成视图层、业务 hooks、接口层和状态层:组件只负责渲染和事件分发,业务 hooks 处理流程编排,接口层统一处理错误和类型,状态层管理跨组件共享数据。同时补充关键流程的单元测试和 E2E 测试,避免后续改需求时反复回归。
面试加分表达:
- 我不是一开始就抽象,而是在重复和复杂度出现后再抽象。
- 抽象后关注可测试性、可替换性和团队协作成本。
- 不把所有问题都归因给产品或后端,而是讲自己可控的改进。
6. 如何使用 TypeScript 解析处理大模型返回的 JSON 数据
一句话结论: 不要相信大模型一定返回合法 JSON;前端需要做提取、修复、运行时校验、降级和错误可观测。
示范回答:
大模型返回 JSON 时,我会分几层处理。第一层是提示词约束,要求模型只返回 JSON,并给出 schema 示例。第二层是流式或完整文本接收后,提取 JSON 片段,比如去掉 Markdown 代码块。第三层是 JSON.parse,失败时做有限修复,比如去除首尾解释文本,但不能无限猜测。第四层是运行时校验,TypeScript 类型只在编译期有效,真正运行时需要用 Zod、Valibot 或自定义 validator 校验字段类型、枚举值、数组长度等。第五层是错误兜底,比如展示原始文本、提示重试、记录日志。
代码思路:
ts
type InterviewQuestion = {
title: string;
tags: string[];
answer: string;
};
function extractJson(text: string) {
return text
.replace(/^```json\s*/i, "")
.replace(/^```\s*/i, "")
.replace(/```$/i, "")
.trim();
}
function parseModelJson<T>(text: string, validate: (value: unknown) => value is T): T {
const raw = extractJson(text);
const value: unknown = JSON.parse(raw);
if (!validate(value)) {
throw new Error("模型返回结构不符合预期");
}
return value;
}追问方向:
- 流式 JSON 不完整时如何处理?
- 大模型字段缺失如何兜底?
- TypeScript 类型和运行时校验的区别是什么?
7. 多 Agent 协作场景下,前端如何设计全局状态机来管理
一句话结论: 多 Agent 前端不要只用零散布尔值管理状态,应该用状态机描述任务生命周期、Agent 状态、消息流、异常和人工介入。
示范回答:
多 Agent 协作场景下,前端通常要展示任务创建、规划、多个 Agent 并行执行、工具调用、等待用户确认、失败重试、完成汇总等状态。如果用很多 isLoading、isError、currentStep 分散管理,很容易出现状态冲突。我会把全局状态拆成任务维度、Agent 维度和消息维度。
任务维度可以有 idle、planning、running、waiting_user、failed、completed。Agent 维度可以有 pending、working、blocked、done。消息维度记录事件流,例如 agent_started、tool_called、approval_required、tool_result、agent_finished。前端根据事件流更新状态机,UI 只订阅派生状态。
设计要点:
- 状态要可枚举,避免多个布尔值组合出非法状态。
- 事件要可回放,便于恢复页面和排查问题。
- 长任务要支持取消、重试、暂停、恢复。
- 人工确认节点要明确阻塞哪个 Agent、等待什么输入。
8. 前端切片断点续传和并发控制如何实现
一句话结论: 前端把大文件切成固定大小分片,计算文件唯一标识,查询已上传分片,只上传缺失部分,并通过队列限制并发和重试。
示范回答:
实现大文件上传一般会先根据文件内容、大小、名称等计算 hash,作为文件唯一标识。然后用 Blob.prototype.slice 把文件切成多个 chunk。上传前调用后端接口查询这个文件已经上传了哪些分片,前端过滤掉已上传分片,只上传缺失的。上传时不能所有分片一起发,需要维护一个并发队列,比如同时上传 3 到 5 个,某个分片失败后进入重试队列。所有分片上传完成后,通知后端合并,并校验最终文件大小或 hash。
关键点:
- 文件 hash 计算可能很耗时,适合放到 Web Worker。
- 分片大小要权衡,太小请求多,太大失败重传成本高。
- 并发数不能固定拍脑袋,可以根据网络和服务端压力调整。
- 上传状态要持久化,刷新页面后能恢复。
伪代码:
ts
async function uploadChunks(chunks: Blob[], concurrency = 4) {
let index = 0;
async function worker() {
while (index < chunks.length) {
const current = index++;
await uploadChunk(chunks[current], current);
}
}
await Promise.all(Array.from({ length: concurrency }, worker));
}9. React 项目中如何封装可复用的 WebSocket Hook
一句话结论: WebSocket Hook 要处理连接、消息分发、心跳、重连、关闭、组件卸载清理和状态暴露。
示范回答:
我会把 WebSocket 封装成 useWebSocket,对外暴露 status、send、lastMessage、close 等能力。内部用 useRef 保存 socket 实例,避免重渲染导致重复连接;用 useEffect 在 URL 或 token 变化时创建连接,并在卸载时关闭。为了稳定性,还要加心跳检测、断线重连、最大重试次数、消息队列和错误回调。
代码骨架:
tsx
function useWebSocket(url: string) {
const socketRef = React.useRef<WebSocket | null>(null);
const [status, setStatus] = React.useState<"idle" | "open" | "closed" | "error">("idle");
const [lastMessage, setLastMessage] = React.useState<MessageEvent | null>(null);
React.useEffect(() => {
const socket = new WebSocket(url);
socketRef.current = socket;
socket.onopen = () => setStatus("open");
socket.onmessage = setLastMessage;
socket.onerror = () => setStatus("error");
socket.onclose = () => setStatus("closed");
return () => {
socket.close(1000, "component unmount");
socketRef.current = null;
};
}, [url]);
const send = React.useCallback((data: string) => {
if (socketRef.current?.readyState === WebSocket.OPEN) {
socketRef.current.send(data);
}
}, []);
return { status, lastMessage, send };
}追问方向:
- 如何避免多个组件重复创建连接?
- token 过期如何重连?
- WebSocket 和 SSE 的选择标准是什么?
10. 如果客户端意外断开连接,如何处理资源释放
一句话结论: 前端要主动清理连接、定时器、订阅和未完成请求;后端也要通过心跳、超时和连接关闭事件释放资源。
示范回答:
在前端,如果页面卸载、路由切换或组件卸载,需要清理 WebSocket、EventSource、定时器、观察器、全局事件监听和未完成请求。对于 fetch 可以用 AbortController 取消。对于 WebSocket,要调用 close 并清空引用。对于长任务轮询,要停止定时器,避免组件销毁后还在 setState。
服务端也不能完全依赖客户端正常关闭,因为用户可能断网、关闭浏览器或进程崩溃。服务端需要心跳机制和超时检测,发现连接长时间无响应就释放会话、文件句柄、任务锁、订阅关系等资源。
前端清理清单:
clearTimeout/clearIntervalremoveEventListenerAbortController.abort()WebSocket.close()EventSource.close()- 取消状态管理订阅
- 清理 Worker、Observer、音视频流等资源
11. Vite 构建的前端项目如何进行依赖拆分和性能调优
一句话结论: Vite 生产构建基于 Rollup,可以通过动态导入、路由懒加载、manualChunks、依赖分析和资源压缩来优化产物。
示范回答:
Vite 项目性能调优我会先分析产物,而不是直接改配置。可以用 rollup-plugin-visualizer 看哪些依赖占体积。业务页面按路由懒加载,大型组件按交互懒加载。对稳定的大型依赖,比如 React、图表库、编辑器、日期库,可以用 manualChunks 拆到独立 chunk,提升浏览器缓存命中率。对于 lodash、moment 这类库,要关注按需导入和替代方案。
同时要注意 chunk 拆得太碎也会增加请求数量,尤其是弱网环境下不一定更快。优化目标应该结合首屏资源大小、请求数量、缓存命中率和真实用户性能数据。
示例配置:
ts
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
react: ["react", "react-dom"],
charts: ["echarts"]
}
}
}
}
};12. requestAnimationFrame
一句话结论: requestAnimationFrame 会在浏览器下一次重绘前执行回调,适合做动画和与渲染节奏相关的更新。
示范回答:requestAnimationFrame 的优势是和浏览器刷新节奏对齐。相比 setTimeout 固定时间调用,requestAnimationFrame 会在下一帧绘制前执行,浏览器可以更好地合并渲染,减少掉帧。在标签页后台时,浏览器通常会降低它的执行频率,从而节省资源。
常见应用包括动画、滚动同步、进度条、虚拟列表测量后的下一帧更新等。使用时要注意取消动画,避免组件卸载后继续执行。
代码示例:
ts
let frameId = 0;
function tick() {
updateAnimation();
frameId = requestAnimationFrame(tick);
}
frameId = requestAnimationFrame(tick);
function cleanup() {
cancelAnimationFrame(frameId);
}追问方向:
requestAnimationFrame和requestIdleCallback的区别。- 为什么用它做动画比
setInterval更合适。 - 如何避免布局抖动。
13. React 中如何实现 LLM 流式输出的打字机效果
一句话结论: 流式输出要把“数据接收”和“展示节奏”解耦,避免每收到一个 token 就高频 setState。
示范回答:
LLM 流式输出通常来自 SSE、fetch stream 或 WebSocket。前端接收到 token 后,不建议每个 token 都立即 setState,否则会导致频繁渲染。更好的方式是用 ref 缓存待展示内容,再用 requestAnimationFrame 或定时器按一定节奏批量刷新 UI。这样既有打字机效果,又能控制渲染频率。
还要处理停止生成、重新生成、错误重试、Markdown 增量渲染、代码块未闭合、光标效果和自动滚动。用户手动滚动历史内容时,不应该强制自动滚到底部。
实现要点:
- 接收层:持续读取流并放入 buffer。
- 展示层:按帧或固定间隔消费 buffer。
- 状态层:区分
streaming、paused、done、error。 - 交互层:支持停止、重试、复制、自动滚动控制。
14. Next.js 的 Server Components 在 AI 原生应用中的价值
一句话结论: Server Components 可以把服务端数据获取、权限控制、密钥保护和重计算放在服务端,减少客户端 bundle,并提升 AI 应用的数据安全性和首屏体验。
示范回答:
在 AI 原生应用里,前端经常要访问模型服务、向量数据库、用户会话和权限数据。Server Components 的价值是可以在服务端直接读取数据并渲染一部分 UI,不需要把所有数据请求逻辑和依赖都打包到客户端。这样可以减少客户端 JavaScript 体积,也避免把 API Key 或敏感逻辑暴露给浏览器。
例如聊天应用的会话列表、知识库列表、用户权限、历史消息摘要适合在 Server Components 中获取;而输入框、流式响应、停止生成、拖拽上传等强交互部分适合 Client Components。两者组合可以兼顾安全、性能和交互体验。
注意点:
- Server Components 不能使用浏览器事件和状态 hooks。
- 流式交互仍需要 Client Components 或服务端 streaming API 配合。
- 要明确哪些数据能缓存,哪些必须按用户权限动态获取。
15. React.memo 和 useMemo 的区别
一句话结论: React.memo 缓存组件渲染结果,避免 props 没变时子组件重复渲染;useMemo 缓存计算结果,避免昂贵计算在每次渲染时重复执行。
示范回答:React.memo 是高阶组件,用来包裹组件。当父组件重新渲染时,如果传给子组件的 props 浅比较没有变化,React 可以跳过这个子组件的渲染。它适合纯展示组件、列表项组件、渲染成本较高的子组件。
useMemo 是 hook,用来缓存一个计算值。只有依赖项变化时才重新计算。它适合昂贵计算、派生数据、复杂过滤排序等场景。需要注意的是,useMemo 本身也有成本,不应该无脑使用。
常见配合:
tsx
const item = React.useMemo(() => ({ id, name }), [id, name]);
const Row = React.memo(function Row(props: { item: Item }) {
return <div>{props.item.name}</div>;
});如果传给 React.memo 子组件的对象或函数每次都是新引用,memo 可能失效,这时可以配合 useMemo 或 useCallback 稳定引用。
16. 如何确定页面的可用性时间
一句话结论: 页面可用性不能只看 load,更应该结合 FCP、LCP、TTI、INP、接口完成、关键交互是否可响应等指标。
示范回答:
页面什么时候“可用”要看业务定义。对内容型页面,用户能看到核心内容可能就是可用,这时关注 FCP 和 LCP。对后台系统,页面渲染出来但按钮点不了不算可用,需要关注关键接口返回、表单可交互、权限加载完成和主线程是否空闲。对交互型页面,还要关注 INP,确保用户点击、输入能够快速响应。
实际项目中可以定义一个业务可用点,比如“核心数据请求完成且首屏主区域渲染完成”,然后通过 Performance API 打点上报。
示例:
ts
performance.mark("app-usable");
performance.measure("time-to-usable", "navigationStart", "app-usable");指标参考:
- FCP:首次内容绘制。
- LCP:最大内容绘制。
- TTI:页面可稳定交互时间。
- INP:交互响应延迟。
- 自定义业务可用时间:更贴近真实体验。
17. 前端如何优化渲染环节
一句话结论: 渲染优化要减少无效计算、减少 DOM 规模、减少布局抖动、合理利用浏览器合成层,并控制框架层面的重复渲染。
示范回答:
浏览器渲染大致包括样式计算、布局、绘制和合成。优化时要避免频繁读写布局属性导致强制同步布局,比如连续读取 offsetHeight 后又修改样式。可以把 DOM 读写分离,使用 requestAnimationFrame 批量更新。动画尽量使用 transform 和 opacity,减少 layout 和 paint。
在 React/Vue 这类框架里,还要减少不必要的组件渲染,例如合理拆分组件、稳定 props 引用、使用 memo、虚拟列表、懒加载和状态下沉/上移的平衡。对于大列表、大表格、复杂图表,要优先减少渲染节点数量。
优化清单:
- 减少 DOM 节点数量。
- 避免频繁触发布局。
- CSS 动画优先使用 transform/opacity。
- 使用虚拟列表处理大量数据。
- 批量状态更新,避免高频 setState。
- 使用性能分析工具定位瓶颈。
18. 大型项目如何进行性能优化
一句话结论: 大型项目性能优化要从指标、构建产物、运行时、网络、缓存、监控和团队规范一起推进,不能只靠单点技巧。
示范回答:
大型项目性能优化我会先建立指标体系,比如 LCP、INP、CLS、首屏接口耗时、JS bundle 体积、错误率。然后通过 Lighthouse、Performance 面板、产物分析和线上 RUM 数据定位主要瓶颈。网络层面做资源压缩、HTTP 缓存、CDN、接口聚合和预加载;构建层面做代码拆分、Tree Shaking、依赖治理;运行时层面优化渲染、长任务、内存泄漏和列表性能。
大型项目还要关注团队协作,比如引入 bundle size 检查、性能预算、组件规范、图片规范、埋点规范。如果没有持续监控,性能很容易随着版本迭代变差。
回答亮点:
- 先量化再优化。
- 区分实验室数据和真实用户数据。
- 建立性能预算和 CI 检查。
- 对优化收益做复盘。
19. 如何处理长列表或无限滚动下的性能问题
一句话结论: 核心是虚拟化渲染,只渲染可视区域和缓冲区的数据,同时处理高度测量、滚动恢复、分页加载和交互状态。
示范回答:
长列表如果一次性渲染几千上万条 DOM,会导致首屏慢、滚动卡顿、内存占用高。常见方案是虚拟列表,只渲染可视区域附近的数据,通过占位高度撑开滚动容器。固定高度列表比较简单,可以根据滚动距离计算开始和结束索引;动态高度列表要维护每项高度缓存,滚动时动态估算和修正。
无限滚动还要处理分页加载、触底检测、加载中状态、错误重试、滚动位置恢复、列表项 key 稳定和图片懒加载。对于复杂卡片,可以把重组件延迟渲染,或者用 skeleton 降低感知等待。
追问方向:
- 固定高度和动态高度虚拟列表的区别。
- 如何避免滚动时频繁 setState。
- 返回上一页如何恢复滚动位置。
- IntersectionObserver 在无限滚动中的作用。
20. 页面加载速度提升应该从哪些方面来思考
一句话结论: 页面加载优化可以从网络链路、资源体积、渲染路径、缓存策略、服务端响应和用户感知六个方面分析。
示范回答:
我会先拆解页面加载过程:DNS、TCP/TLS、HTML 下载、CSS/JS 加载、接口请求、渲染和交互可用。网络层面可以使用 CDN、HTTP/2 或 HTTP/3、资源压缩、缓存和预连接。资源层面要减少 JS/CSS 体积,做 Tree Shaking、代码拆分、图片压缩和现代格式。渲染层面要减少阻塞资源,关键 CSS 内联,非关键 JS 延迟加载。接口层面要减少首屏关键请求数量,必要时做 SSR、SSG、边缘缓存或接口聚合。
用户感知上,可以使用 skeleton、渐进式渲染、预加载下一页资源、乐观更新等方式,让用户更早看到可用内容。
常用手段:
preload加载关键资源。prefetch预取后续可能访问资源。- 图片懒加载和响应式图片。
- 路由级代码拆分。
- SSR/SSG 改善首屏内容到达时间。
- 线上 RUM 监控真实用户体验。
总结复习路线
如果时间有限,建议按这个顺序复习:
- 工程化:Vite、Webpack、依赖拆分、构建优化。
- 浏览器:CORS、渲染流程、页面加载、性能指标。
- React:memo、hooks、WebSocket、流式输出。
- 项目题:挑战、困难点、性能优化、大文件上传。
- AI 前端:LLM JSON 解析、多 Agent 状态机、Server Components。
面试时最好准备 2 到 3 个能反复复用的项目案例:一个性能优化案例、一个复杂交互/状态管理案例、一个工程化或稳定性案例。很多题都可以落回这些案例里回答,这样会比孤立背题更像真实经验。