前端面试100题
前端面试 100 题
适用方向:中高级前端 / React / Vue / Next.js / Nuxt / TypeScript / 工程化 / 实时通信 / Electron / Node.js / AI 应用前端
使用方式:优先掌握“标准回答”,再练“面试官追问”,最后把“结合你的简历怎么答”组织成自己的项目故事。说明
- “结合你的简历怎么答”只使用你简历中已经出现的项目与技术事实。
- “标准回答 / 追问”属于通用前端知识总结,用来补足面试表达。
- 建议不要逐字背诵,要能用自己的语言讲清楚“为什么”。
目录
- JavaScript 基础:1~15
- 异步、Promise、Event Loop:16~25
- TypeScript:26~35
- 浏览器与网络:36~47
- React:48~60
- Vue / Nuxt:61~68
- TanStack Query / 状态管理:69~75
- Next.js / SSR / 性能优化:76~84
- WebSocket / IM / 实时系统:85~91
- 工程化 / Electron / Node.js:92~97
- AI / Agent / SSE:98~100
一、JavaScript 基础
1. var、let、const 有什么区别?
标准回答
var是函数作用域,let和const是块级作用域。var存在变量提升,声明会被提升并初始化为undefined。let/const也会创建绑定,但在声明前处于暂时性死区。var可以重复声明,let/const不允许同作用域重复声明。const约束的是变量绑定不能重新赋值,不代表对象内部不可修改。
const user = { name: 'A' }
user.name = 'B' // 可以
// user = {} // 不可以
面试官追问
为什么 let / const 也算“提升”?
因为进入作用域时绑定已经建立,但在声明执行前不能访问,因此会产生 TDZ。
2. 什么是闭包?
标准回答
闭包本质上是:函数在创建时会保留对其词法作用域的引用,即使外部函数已经执行结束,内部函数仍然可以访问这些变量。
function createCounter() {
let count = 0
return () => ++count
}
常见应用:
- 私有变量
- 防抖 / 节流
- React Hooks
- 缓存
- 事件处理器
- 函数柯里化
面试官追问
闭包一定会导致内存泄漏吗?
不会。只有当闭包长期持有不再需要的大对象引用,并且引用无法被释放时,才可能造成内存占用问题。
3. 说一下 JS 的作用域链。
标准回答
JavaScript 使用词法作用域。变量查找从当前作用域开始,找不到就沿外层词法环境逐层向上,直到全局作用域。
const a = 1
function foo() {
const b = 2
function bar() {
console.log(a, b)
}
bar()
}
bar 能访问 b 和 a,就是因为作用域链。
4. 什么是原型和原型链?
标准回答
对象访问属性时:
- 先查对象自身属性。
- 找不到则访问其原型。
- 再沿原型的原型继续查找。
- 最终到
Object.prototype。 - 再往上是
null。
function A() {}
const a = new A()
a.__proto__ === A.prototype
A.prototype.constructor === A
建议实际代码中使用:
Object.getPrototypeOf(a)
而不是依赖 __proto__。
5. new 操作符做了什么?
标准回答
大致流程:
- 创建一个新对象。
- 将对象原型指向构造函数的
prototype。 - 用新对象作为
this执行构造函数。 - 如果构造函数显式返回对象,则返回它;否则返回新对象。
function myNew(Constructor, ...args) {
const obj = Object.create(Constructor.prototype)
const result = Constructor.apply(obj, args)
return (
result !== null &&
(typeof result === 'object' || typeof result === 'function')
) ? result : obj
}
6. this 的指向规则有哪些?
标准回答
可以按优先级记:
new绑定call/apply/bind显式绑定- 对象方法调用时的隐式绑定
- 默认绑定
- 箭头函数没有自己的
this,从定义位置继承
const obj = {
name: 'demo',
foo() {
console.log(this.name)
}
}
const fn = obj.foo
fn()
fn() 已经脱离 obj 调用,因此不会再以 obj 为 this。
7. 箭头函数和普通函数有什么区别?
标准回答
箭头函数:
- 没有自己的
this - 没有
arguments - 不能作为构造函数
- 没有
prototype this取决于定义时的词法作用域
因此对象方法、构造函数等场景需要谨慎使用箭头函数。
8. call、apply、bind 有什么区别?
标准回答
call:立即执行,参数逐个传递。apply:立即执行,参数以数组形式传递。bind:返回一个绑定好this的新函数,不立即执行。
9. 深拷贝和浅拷贝有什么区别?
标准回答
浅拷贝只复制第一层引用。
const b = { ...a }
如果嵌套对象仍然是同一个引用,那么修改嵌套对象仍会互相影响。
深拷贝会递归复制整个对象结构。
浏览器环境优先考虑:
structuredClone(value)
但它也不能克隆函数、DOM 节点等所有对象。
10. JSON.parse(JSON.stringify()) 做深拷贝有什么问题?
标准回答
可能丢失或改变:
undefinedfunctionSymbolBigIntDateMapSet- 循环引用
因此不应该把 JSON 序列化当通用深拷贝方案。
11. == 和 === 有什么区别?
标准回答
=== 不做隐式类型转换。
== 会执行抽象相等比较,可能发生复杂的类型转换。
面试和工程实践中,一般优先使用 ===。
经典例子:
[] == false // true
null == undefined // true
NaN === NaN // false
12. 为什么 NaN !== NaN?
标准回答
按照 IEEE 754 和 JavaScript 数值比较规则,NaN 与任何值都不相等,包括它自己。
判断推荐:
Number.isNaN(value)
13. Map 和普通对象有什么区别?
标准回答
Map:
- key 可以是任意类型。
- 迭代顺序明确。
- 有
size。 - 高频增删查场景语义更清晰。
普通对象:
- 更适合描述结构化数据。
- 天然支持 JSON 序列化。
- key 主要是字符串 / Symbol。
14. Set 常见使用场景?
标准回答
- 数组去重
- 快速判断存在性
- 集合运算
- 维护唯一 id 集合
const unique = [...new Set(list)]
15. 防抖和节流有什么区别?
标准回答
防抖 debounce
连续触发时只在停止触发一段时间后执行一次。
适合:
- 搜索输入
- 表单校验
- resize 后重新布局
节流 throttle
固定时间窗口最多执行一次。
适合:
- scroll
- mousemove
- 高频状态采样
二、异步、Promise、Event Loop
16. Promise 有哪些状态?
标准回答
pendingfulfilledrejected
状态只能从:
pending -> fulfilled
pending -> rejected
一旦确定就不能再次改变。
17. async 函数返回什么?
标准回答
async 函数一定返回 Promise。
async function foo() {
return 1
}
等价理解为:
function foo() {
return Promise.resolve(1)
}
18. await 做了什么?
标准回答
await 会暂停当前 async 函数后续代码,将后续逻辑安排为 Promise continuation。
async function foo() {
console.log(1)
await Promise.resolve()
console.log(2)
}
console.log(2) 会在后续微任务阶段继续执行。
19. Event Loop 是什么?
标准回答
浏览器执行模型可以简化为:
同步任务
↓
调用栈清空
↓
执行微任务队列
↓
浏览器可能进行渲染
↓
执行下一轮宏任务
常见微任务:
- Promise.then
- queueMicrotask
常见任务:
- setTimeout
- DOM Event
- MessageChannel
20. 下面代码输出顺序是什么?
console.log(1)
setTimeout(() => console.log(2))
Promise.resolve().then(() => console.log(3))
console.log(4)
标准回答
1
4
3
2
原因:
- 同步先执行。
- Promise 回调是微任务。
- setTimeout 是后续任务。
21. 再解释一道 async/await 顺序题。
async function foo() {
console.log(1)
await Promise.resolve()
console.log(2)
}
console.log(3)
foo()
Promise.resolve().then(() => console.log(4))
console.log(5)
标准回答
3
1
5
2
4
核心是理解 await 后面的逻辑作为后续微任务继续执行。
22. Promise.all 和 Promise.allSettled 区别?
标准回答
Promise.all:
- 全部成功才成功。
- 任意一个 reject,会立即返回 rejected Promise。
Promise.allSettled:
- 等所有 Promise 都结束。
- 返回每一项成功 / 失败状态。
适合批量任务收集结果。
23. Promise.race 和 Promise.any 区别?
标准回答
race:
- 谁最先 settle 就采用谁的结果。
- 成功失败都算。
any:
- 谁最先 fulfilled 就采用谁。
- 全部失败才 reject。
24. 怎么做请求并发控制?
标准回答
核心思想是维护固定数量 worker:
async function runWithLimit(tasks, limit) {
const results = new Array(tasks.length)
let index = 0
async function worker() {
while (true) {
const current = index++
if (current >= tasks.length) return
results[current] = await tasks[current]()
}
}
await Promise.all(
Array.from({ length: limit }, () => worker())
)
return results
}
面试时重点说:
- 限制同时运行任务数。
- 不阻塞后续任务。
- 需要考虑错误处理和取消。
25. 如何取消一个请求?
标准回答
Fetch 可以使用 AbortController:
const controller = new AbortController()
fetch('/api/data', {
signal: controller.signal
})
controller.abort()
在 React 搜索、页面切换、竞态请求等场景很常见。
三、TypeScript
26. interface 和 type 的区别?
标准回答
都可以描述对象结构。
interface:
- 更适合对象 / 类的契约。
- 支持 declaration merging。
type:
- 可以表达 union、intersection、primitive alias 等更多类型组合。
不要机械地认为某一个一定“更好”。
27. any 和 unknown 区别?
标准回答
any 基本关闭类型检查。
unknown 表示“暂时不知道是什么”,使用前必须缩窄。
function foo(value: unknown) {
if (typeof value === 'string') {
value.toUpperCase()
}
}
所以 unknown 更安全。
28. never 有什么用?
标准回答
never 表示永远不会出现的值。
常见:
- 永远抛错的函数。
- 无限循环。
- Exhaustive Check。
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`)
}
29. keyof 是什么?
type User = {
id: number
name: string
}
type UserKey = keyof User
标准回答
得到:
'id' | 'name'
30. typeof 在 TypeScript 中有什么作用?
标准回答
可以从现有变量推导类型:
const config = {
api: '/api',
retry: 3
}
type Config = typeof config
31. 泛型解决什么问题?
标准回答
泛型用于在“保留类型信息”的同时提高代码复用。
function identity<T>(value: T): T {
return value
}
相比 any,泛型不会丢掉输入输出之间的类型关系。
32. 解释这段代码。
function getProperty<T, K extends keyof T>(
obj: T,
key: K
): T[K] {
return obj[key]
}
标准回答
T表示对象类型。K extends keyof T限制 key 必须属于对象合法 key。- 返回值
T[K]能准确推导属性类型。
33. 手写 Partial<T>。
type MyPartial<T> = {
[K in keyof T]?: T[K]
}
面试官追问
类似地准备:
type MyReadonly<T> = {
readonly [K in keyof T]: T[K]
}
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
34. 什么是条件类型?
标准回答
type IsString<T> = T extends string ? true : false
条件类型可以根据类型关系生成不同结果。
高级追问通常会涉及:
- distributive conditional types
infer
35. infer 是什么?
标准回答
infer 用于在条件类型中推断内部类型。
type MyReturnType<T> =
T extends (...args: any[]) => infer R
? R
: never
四、浏览器与网络
36. 从输入 URL 到页面显示发生了什么?
标准回答
建议按链路回答:
URL 解析
↓
DNS
↓
建立网络连接
↓
HTTPS / TLS
↓
HTTP 请求
↓
服务器响应 HTML
↓
HTML 解析为 DOM
↓
CSS 解析为 CSSOM
↓
生成 Render Tree
↓
Layout
↓
Paint
↓
Composite
37. 浏览器的渲染流程是什么?
标准回答
重点:
- HTML -> DOM
- CSS -> CSSOM
- DOM + CSSOM -> Render Tree
- Layout:计算尺寸位置
- Paint:生成绘制记录
- Composite:合成图层
38. 重排和重绘有什么区别?
标准回答
重排 / Layout
元素几何信息变化,需要重新计算布局。
例如:
- width
- height
- position
- font-size
- DOM 插入删除
重绘 / Paint
视觉变化但布局不一定变。
例如:
- color
- background
39. 什么是 Layout Thrashing?
标准回答
连续交替执行:
写样式
读布局
写样式
读布局
可能迫使浏览器频繁同步计算布局。
例如读取:
offsetHeight
getBoundingClientRect()
优化方式:
- 批量读
- 批量写
- 使用 requestAnimationFrame
- 减少高频 DOM 测量
40. 浏览器缓存有哪些?
标准回答
常见:
- Memory Cache
- Disk Cache
- HTTP Cache
- Service Worker Cache
HTTP 缓存重点分:
- 强缓存
- 协商缓存
41. 强缓存和协商缓存有什么区别?
标准回答
强缓存:
Cache-Control: max-age=...
缓存仍新鲜时直接使用本地副本,不需要向服务端确认。
协商缓存:
ETag
If-None-Match
Last-Modified
If-Modified-Since
客户端会与服务端确认资源是否变化。
42. Cookie、LocalStorage、SessionStorage、IndexedDB 区别?
标准回答
Cookie
- 会自动随匹配请求发送。
- 容量较小。
- 可以设置 HttpOnly / Secure / SameSite。
LocalStorage
- 同源长期保存。
- 同步 API。
SessionStorage
- 生命周期主要跟当前标签页会话相关。
IndexedDB
- 异步。
- 适合大量结构化数据和离线场景。
43. XSS 是什么?
标准回答
攻击者将恶意脚本注入页面并执行。
防护:
- 输出编码
- 避免直接插入不可信 HTML
- CSP
- Cookie 使用 HttpOnly
- 富文本严格白名单过滤
44. CSRF 是什么?
标准回答
攻击者诱导用户浏览器向已登录站点发送非预期请求。
常见防护:
- SameSite Cookie
- CSRF Token
- 校验 Origin / Referer
- 敏感操作二次确认
45. HTTP/1.1、HTTP/2、HTTP/3 有什么区别?
标准回答
HTTP/1.1:
- 文本协议
- keep-alive
- 多请求并发能力有限
HTTP/2:
- 二进制分帧
- 多路复用
- Header Compression
- 一个 TCP 连接承载多个 stream
HTTP/3:
- 基于 QUIC
- 运行在 UDP 之上
- 避免 TCP 层的连接级队头阻塞
46. HTTPS 为什么安全?
标准回答
HTTPS = HTTP + TLS。
TLS 主要解决:
- 机密性
- 完整性
- 身份认证
握手阶段通常借助非对称密码体系完成身份验证和密钥协商,之后使用对称加密传输数据。
47. TCP 为什么是三次握手?
标准回答
核心目标是双方确认:
- 自己的发送能力
- 自己的接收能力
- 对方的发送能力
- 对方的接收能力
以及同步双方初始序列号。
五、React
48. React 为什么需要 Virtual DOM?
标准回答
不要说“Virtual DOM 一定比真实 DOM 快”。
更准确的说法:
Virtual DOM 为声明式 UI 提供中间表示,通过 reconciliation 比较新旧树,计算必要更新,再统一提交到真实 DOM。
价值:
- 声明式编程
- 跨平台抽象
- 可预测更新
- 为调度和并发能力提供基础
49. React Diff 的基本思想是什么?
标准回答
经典 React reconciliation 依赖几个重要假设:
- 不同类型元素通常产生不同子树。
- 同层级进行比较。
- 使用
key帮助识别列表节点。
这样把通用树比较问题降到接近 O(n)。
50. 为什么不建议用数组 index 作为 key?
标准回答
如果列表存在:
- 插入
- 删除
- 排序
index 会发生变化,React 可能错误复用组件实例,造成:
- 状态错位
- 输入框值错位
- 不必要更新
稳定业务 id 更合适。
51. React Fiber 是什么?
标准回答
Fiber 可以理解为 React 内部对组件工作单元的结构化表示。
价值:
- 把渲染工作拆成可管理单元。
- 支持暂停、恢复、优先级调度。
- 为并发渲染提供基础。
52. React 渲染阶段和提交阶段有什么区别?
标准回答
Render Phase:
- 计算新 UI。
- 可以被打断、重做。
- 不应该产生不可逆副作用。
Commit Phase:
- 真正修改 DOM。
- 执行 layout effect 等提交相关逻辑。
- 不可随意中断。
53. useEffect 和 useLayoutEffect 区别?
标准回答
useLayoutEffect:
- DOM 提交后、浏览器绘制前同步执行。
- 适合测量布局、立即修正视觉状态。
useEffect:
- 更晚执行。
- 不阻塞浏览器绘制。
- 更适合大多数副作用。
54. 什么是 stale closure?
标准回答
函数捕获的是创建时的变量环境。
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
console.log(count)
}, 1000)
return () => clearInterval(timer)
}, [])
这里 interval 回调捕获的是首次 render 的 count。
解决思路:
- 正确依赖
- 函数式更新
- ref
- 调整逻辑结构
55. useMemo 和 useCallback 什么时候用?
标准回答
useMemo 缓存计算结果。
useCallback 缓存函数引用。
适合:
- 计算成本明显高
- 需要保持引用稳定
- 配合 memoized child
- 依赖引用稳定的第三方 API
不应该机械地“全部包一层”,memo 本身也有比较和缓存成本。
56. React 18 的 Automatic Batching 是什么?
标准回答
React 会尽量把多个状态更新合并到一次 render。
这样可以减少重复渲染。
57. startTransition / useTransition 是干什么的?
标准回答
用于把不紧急更新标记为 transition,让输入等紧急交互保持更高响应优先级。
典型:
输入框值更新:紧急
大列表筛选结果:可以 transition
58. Suspense 的核心思想是什么?
标准回答
让组件树能够声明“某部分暂时还不能完成”,然后由上层 Suspense boundary 展示 fallback。
它可以配合:
- lazy
- 服务端流式渲染
- 支持 Suspense 的数据框架
59. Server Component 和 Client Component 的区别?
标准回答
Server Component:
- 在服务端执行。
- 可以直接访问服务端资源。
- 不会把组件本身对应的客户端 JS 全部发到浏览器。
- 不能使用依赖浏览器运行环境的交互 Hook,例如
useEffect。
Client Component:
- 在浏览器参与交互。
- 可以使用 state、effect、事件处理。
结合你的简历怎么答
你在 GamePortal 中使用了 Next.js App Router,所以面试时可以强调:
页面并不是全部一股脑做成客户端组件。对于需要交互和实时状态的区域使用 Client Component,而偏展示、首屏和服务端数据获取的部分可以尽可能留在服务端边界,从而减少客户端 JS。
60. React 状态更新为什么不是“修改变量”?
标准回答
React state 更像某次 render 的快照。
调用 setState 是“请求一次新的 render”,而不是直接改变当前函数调用里的变量。
六、Vue / Nuxt
61. Vue 2 和 Vue 3 响应式有什么区别?
标准回答
Vue 2 主要基于 Object.defineProperty。
Vue 3 主要基于 Proxy。
Proxy 优势:
- 可以拦截更多操作。
- 对新增 / 删除属性处理自然。
- 对数组和对象能力更完整。
62. ref 和 reactive 有什么区别?
标准回答
ref:
- 可以包装 primitive。
- 通过
.value访问。
reactive:
- 主要包装对象。
- 返回 Proxy。
实际项目更关注语义和可维护性,而不是绝对二选一。
63. computed、watch、watchEffect 区别?
标准回答
computed
- 声明派生状态。
- 有缓存语义。
- 适合根据已有响应式状态得到新值。
watch
- 明确监听指定源。
- 能访问新旧值。
watchEffect
- 自动收集执行过程中使用的响应式依赖。
- 更偏副作用。
64. nextTick 是干什么的?
标准回答
Vue 会批量调度响应式更新,不一定同步立刻更新 DOM。
nextTick 用于等待本轮 DOM 更新提交完成。
65. Vue 组件为什么也需要 key?
标准回答
key 用于帮助 diff 算法识别节点身份。
稳定 key 能正确复用或重建节点。
66. Vue 的 Composition API 有什么优势?
标准回答
主要不是“更高级”,而是:
- 逻辑可以按功能组织。
- 更容易提取 composable。
- TypeScript 体验更自然。
- 复杂组件中相关逻辑不必被 data/methods/watch 分散。
67. Nuxt 的 SSR 解决什么问题?
标准回答
主要价值:
- 首屏 HTML
- SEO
- 更好的初始内容可见性
同时要付出:
- 服务端运行成本
- 数据序列化
- hydration 一致性
- 服务端 / 客户端环境差异处理
结合你的简历怎么答
你在跨境业务项目中用 Nuxt 3 沉淀过独立站模板,并包含 SEO、GTM、多语言和多币种。
可以回答:
独立站类页面本身就比较看重 SEO 和首屏,所以官网和营销页面优先使用 SSR/SSG 思路,而任务型后台更偏 SPA。
68. React 和 Vue 的响应式理念有什么本质区别?
标准回答
React 更偏:
state 改变 -> 重新执行组件函数 -> 重新计算 UI
Vue 更偏:
Proxy 收集依赖 -> 精确触发依赖更新
两者都可以做高性能 UI,只是响应式模型不同。
七、TanStack Query / 状态管理
69. TanStack Query 和 Zustand / Pinia 的区别?
标准回答
TanStack Query 更偏 Server State:
- 异步
- 来自服务端
- 可能失效
- 要刷新
- 要重试
- 要缓存
- 要同步
Zustand / Pinia 更偏 Client State:
- UI 状态
- 本地业务状态
- 跨组件共享状态
结合你的简历怎么答
你的项目里同时使用过:
- TanStack Query
- Zustand
- Jotai
- Pinia
建议表达:
我会优先区分 server state 和 client state。比如游戏列表、任务、用户、数据集这类接口数据交给 Query;当前选中项、弹窗、本地交互或跨组件临时状态才会放 Zustand / Jotai / Pinia。
70. staleTime 是什么?
标准回答
staleTime 控制的是:
数据多长时间内被认为是 fresh。
它不等同于:
过了这个时间数据就一定马上重新请求。
是否重新请求,还受:
- 组件挂载
- refetchOnWindowFocus
- refetchOnReconnect
- 手动 invalidate
- 其他策略
影响。
71. gcTime 是什么?
标准回答
当 Query 没有活跃 observer 后,会进入 inactive 状态。
gcTime 决定 inactive query 多久后从缓存中被回收。
72. Query Key 为什么重要?
标准回答
Query Key 是服务端数据缓存身份。
例如:
['games', category, page]
不同参数应该构成不同 key。
如果 key 设计错误,会出现:
- 错误复用缓存
- 数据串页
- invalidate 范围不准确
73. invalidateQueries 和 setQueryData 的区别?
标准回答
invalidateQueries
- 标记数据失效。
- 通常触发后续重新获取。
setQueryData
- 直接更新本地缓存。
- 适合已有新数据,例如 WebSocket 推送或 mutation 乐观更新。
结合你的简历怎么答
GamePortal 里的任务状态更新可以这样讲:
WebSocket 已经把最新任务状态主动推送到了客户端,所以没必要收到事件后再发一次 HTTP。直接通过
queryClient.setQueryData更新对应 Query Cache,可以让所有消费该 query 的组件同步刷新。
74. 为什么 WebSocket 数据不全部放 Zustand?
标准回答
如果数据本质仍然是服务端资源:
- 有 API 初始加载
- 有刷新
- 有缓存
- 有失效
- 有服务端来源
那继续保留在 TanStack Query 更自然。
WebSocket 只是改变“数据更新通道”,不改变“数据属于 Server State”这个事实。
结合你的简历怎么答
你的项目正好是:
初始数据:HTTP Query
实时变化:WebSocket
最终状态:Query Cache
这套回答很贴合实际经历。
75. 乐观更新怎么做?
标准回答
流程一般是:
- 暂停相关查询。
- 保存旧缓存。
- 立即更新本地缓存。
- mutation 失败则 rollback。
- 最后重新 invalidate 保证最终一致。
八、Next.js / SSR / 性能优化
76. CSR、SSR、SSG 的区别?
标准回答
CSR
浏览器获取 JS 后生成页面。
优点:
- 服务端压力低。
- 后台系统开发模型简单。
缺点:
- 首屏依赖 JS。
- SEO 相对不利。
SSR
每次请求在服务端生成 HTML。
优点:
- 内容首屏可见。
- SEO 友好。
- 数据实时性高。
缺点:
- 服务端计算成本。
- TTFB 可能增加。
SSG
构建阶段提前生成 HTML。
优点:
- CDN 友好。
- 性能非常稳定。
缺点:
- 内容实时性不如 SSR。
结合你的简历怎么答
你简历明确写了 CSR / SSR / SSG 方案设计,并且:
- GamePortal:Next.js
- 跨境独立站:Nuxt
- 博客:Qwik + SSG / SSR 混合
可以结合业务类型说明“不是固定选一种”。
77. 什么是 Hydration?
标准回答
SSR 返回 HTML 后,浏览器还需要加载客户端 JS,让这些已有 DOM 重新具备:
- 事件
- 状态
- 交互能力
这个过程叫 hydration。
78. 什么会导致 Hydration Mismatch?
标准回答
服务端和客户端第一次渲染结果不一致。
典型:
<div>{Date.now()}</div>
或者:
- 随机数
- 依赖 window
- 浏览器 locale
- 客户端才有的数据
- 条件渲染不同
79. Next.js App Router 的核心价值有哪些?
标准回答
可以重点说:
- Layout
- Server Component
- 嵌套路由
- 流式渲染
- Suspense 边界
- 服务端数据获取模型
结合你的简历怎么答
你 GamePortal 是基于 App Router 的大型业务页面,面试时不要只背概念,要把它落到:
- 游戏大厅
- 账户
- 支付
- KYC
- 活动
这些不同模块的页面边界和数据获取方式上。
80. 什么是 Core Web Vitals?
标准回答
重点准备:
- LCP:最大内容绘制
- INP:交互响应
- CLS:布局偏移
同时可补充:
- FCP
- TTFB
81. IntersectionObserver 为什么比直接监听 scroll 更适合懒加载?
标准回答
scroll 事件可能非常高频,开发者需要自己:
- 计算位置
- 节流
- 判断 viewport
IntersectionObserver 由浏览器统一观察元素与 viewport 的相交状态,更适合:
- 懒加载
- 无限列表
- 预加载
- 曝光统计
结合你的简历怎么答
你在 GamePortal 首页游戏列表中用它做“接近列表底部时预加载”。
可以说:
我的目标不是等用户滚到底才请求,而是提前一定阈值开始拉下一页,让分页对用户感知更弱。
82. 图片优化有哪些?
标准回答
- 响应式图片
- srcset / sizes
- WebP / AVIF
- 合理尺寸
- lazy loading
- preload 首屏关键图
- CDN
- 合理 quality
- 避免布局抖动
- 根据设备像素密度选择资源
结合你的简历怎么答
你简历中写了基于 next/image 封装响应式图片组件:
- 加载失败兜底
- 质量映射
- 设备尺寸自适应
- 13 个图片宽度断点
- 20 档质量白名单
面试官如果问“为什么质量不直接 100”,可以答:
图片质量提升并不是线性收益,但文件体积可能明显增加,因此要根据资源类型、展示尺寸和 DPR 做权衡。
83. 首屏性能优化有哪些思路?
标准回答
可以从四层讲:
网络
- CDN
- HTTP 缓存
- 压缩
- preload
资源
- code splitting
- tree shaking
- 图片优化
- 字体优化
渲染
- SSR / SSG
- 减少阻塞
- 合理 Suspense
数据
- 减少首屏接口量
- 分页
- 缓存
- 并行请求
- 预取
结合你的简历怎么答
你有两个很好的真实案例:
- 独立站:组件懒加载、图片压缩、无用资源清理。
- GamePortal:分页降低首屏数据量 + 5 分钟缓存新鲜期 + IntersectionObserver 预加载。
84. 怎么做性能优化才不会变成“玄学”?
标准回答
要有:
指标 -> 定位 -> 改动 -> 对比 -> 回归
常用:
- Performance
- Lighthouse
- Web Vitals
- bundle analyzer
- 网络瀑布图
- React Profiler
不要只说“我用了 memo,所以性能变好了”。
九、WebSocket / IM / 实时系统
85. WebSocket 和 HTTP 有什么区别?
标准回答
HTTP:
- 请求 / 响应。
- 适合资源请求、API。
WebSocket:
- 建立持久双向连接。
- 适合服务器主动推送。
典型:
- IM
- 实时通知
- 游戏状态
- 行情
86. WebSocket 是怎么建立连接的?
标准回答
最初通常从 HTTP Upgrade 开始:
Connection: Upgrade
Upgrade: websocket
握手成功后切换到 WebSocket 协议。
87. WebSocket 断线重连怎么设计?
标准回答
常见:
- 指数退避
- jitter
- 最大间隔
- 网络恢复后立即重连
- 页面可见性恢复
- 登录态变化重新建立连接
避免所有客户端固定 1 秒重连,否则服务恢复瞬间容易产生“惊群”。
88. WebSocket 重连后怎么补数据?
标准回答
实时连接不能天然保证“断线期间消息一定不会漏”。
常见方案:
维护 lastSequenceId
↓
重连成功
↓
向服务端补拉 lastSequenceId 之后的数据
↓
恢复实时流
也可以配合:
- 时间戳
- event id
- version
- cursor
结合你的简历怎么答
你的 GamePortal 已经有:
WebSocket 主链路 + 增量拉取 + 10 分钟低频兜底
这是非常好的“实时 + 最终一致”案例。
89. 消息重复怎么办?
标准回答
客户端和服务端都应该考虑幂等。
常见:
- messageId
- eventId
- requestId
- sequenceId
客户端维护已处理 id 或服务端做幂等写入。
90. IM 消息怎么保证顺序?
标准回答
网络到达顺序不等于业务顺序。
常见方案:
- 服务端生成单调递增 sequence
- 客户端按 sequence 排序
- 检测 gap
- 缺失区间补拉
结合你的简历怎么答
你做过:
- 实时消息
- 在线状态
- 未读计数
- 会话转接
- 草稿
- 消息已读
- 消息转发
这些都可以围绕“会话维度状态”和“消息顺序”展开。
91. 未读消息数怎么设计?
标准回答
一种常见思路:
latestMessageSeq
lastReadSeq
通过两者关系得到未读范围。
实际系统还要考虑:
- 被撤回消息
- 系统消息
- 免打扰
- 跨设备已读同步
- 群聊
十、工程化 / Electron / Node.js
92. Vite 为什么开发环境启动快?
标准回答
传统 bundle-based dev server 往往先把模块打包。
Vite 开发环境主要利用浏览器原生 ESM:
按需请求模块
依赖预构建等环节使用高性能工具,因此大项目启动体验通常更快。
93. Tree Shaking 原理是什么?
标准回答
依赖 ES Module 的静态结构:
import { foo } from './module'
构建工具可以在构建阶段分析:
- 哪些 export 被使用
- 哪些代码不可达
从而删除无用代码。
94. 为什么 CommonJS 不利于 Tree Shaking?
标准回答
CommonJS 依赖可以动态执行:
const module = require(name)
依赖关系更偏运行时,因此静态分析难度更高。
95. pnpm 的特点是什么?
标准回答
重点:
- content-addressable store
- 硬链接
- 符号链接
- workspace
- 节省磁盘
- 依赖结构更严格
追问:什么是幽灵依赖?
项目直接 import 了一个没有声明在自己 package.json 中的包,只是因为它被其他依赖“顺便”安装到可访问位置。
pnpm 的结构能减少这种问题。
96. Electron 的 Main Process 和 Renderer Process 区别?
标准回答
Main:
- 管理窗口
- 生命周期
- 原生能力
- Node 能力
Renderer:
- 负责 UI
- 更像浏览器页面
中间通过 IPC 通信。
结合你的简历怎么答
你经历过:
NW.js -> Electron
并做过 Windows / macOS 打包,所以除了概念,还可以讲迁移和发布流程。
97. 为什么 Electron 不建议直接打开 nodeIntegration?
标准回答
如果 renderer 中执行了不可信内容,同时拥有 Node 权限,风险会非常高。
更推荐:
- contextIsolation
- preload
- contextBridge
- 暴露最小能力 API
十一、AI / Agent / SSE
98. RAG 的完整链路是什么?
标准回答
用户问题
↓
生成 / 改写检索 query
↓
Retrieval
↓
Relevant Chunks
↓
构造 Context
↓
LLM 生成
↓
返回答案 + 来源
结合你的简历怎么答
你的 Joplin Agent:
- 按 Markdown 标题结构切片
- SQLite FTS5 + BM25
- 只把命中片段提交给模型
- 返回可追溯来源
这些都可以直接作为回答。
99. Tool Calling Agent 循环是什么?
标准回答
一个典型流程:
User
↓
LLM
↓
tool_calls
↓
执行工具
↓
Tool Result
↓
再次交给 LLM
↓
继续调用工具 / 输出最终答案
关键设计:
- 最大步数
- 工具 schema
- 超时
- 错误处理
- 上下文控制
- 防止无限循环
结合你的简历怎么答
你的实现中已经有:
- 有限步数 Agent 循环
search_documents- 多轮检索
- tool_calls 解析
所以这题属于非常强的项目题。
100. SSE 和 WebSocket 应该怎么选?
标准回答
SSE
特点:
- 基于 HTTP
- 服务端 -> 客户端单向流
- 文本事件模型
- 浏览器原生 EventSource 有自动重连能力
适合:
- LLM Token Streaming
- 服务端事件流
- 日志流
- 进度流
WebSocket
特点:
- 全双工
- 客户端和服务端都可以主动发送
- 更适合实时交互
适合:
- IM
- 在线状态
- 游戏实时数据
- 协作系统
结合你的简历怎么答
你刚好两种都用了:
GamePortal / IM
→ WebSocket
Joplin Agent 流式回答
→ SSE
这是非常理想的面试回答案例:
我不是按“哪个更新”来选,而是根据通信模型选。业务实时状态有明显双向交互、在线状态和重连需求,所以用 WebSocket;LLM 回答主要是服务端持续向前端输出进度、检索、回答和来源事件,单向流式传输就够了,所以 SSE 更简单。
十二、基于你简历的重点项目追问题
下面这些题,建议比普通八股优先级更高。
A. GamePortal
1. 为什么把 15 秒轮询改成 WebSocket?
回答结构:
原问题
→ HTTP 周期轮询量大
→ 数据其实具有事件驱动特征
→ 改用 WebSocket
→ 推送结果写入 Query Cache
→ UI 自动同步
你简历里的数据:
单用户每小时约 240 次周期请求
→ 0 次
要准备回答:
- WebSocket 断了怎么办?
- 为什么不保留 15 秒兜底?
- 怎么保证任务状态最终一致?
- 多 tab 是否会建立多个连接?
- 消息乱序怎么办?
2. 走马灯为什么不是纯 WebSocket?
你的方案:
WebSocket 主链路
+
增量拉取
+
并发请求复用
+
10 分钟低频兜底
这是一个非常好的回答模板:
实时链路负责低延迟,HTTP 低频兜底负责最终一致,不把系统可靠性完全押在长连接上。
3. TanStack Query 的缓存策略怎么定?
你简历里有:
游戏列表 5 分钟 staleTime
面试时准备说明:
- 为什么是 5 分钟?
- 游戏列表和任务状态 freshness 是否一样?
- 哪些 query 可以长缓存?
- 哪些必须实时?
核心答案:
缓存策略应该按数据变化频率和用户对实时性的敏感度来定,而不是统一一个时间。
B. 跨境独立站 / Nuxt
4. 34 个站点怎么避免复制粘贴?
你简历里是:
- 26 个招聘站
- 8 个商城站
- Nuxt 3 模板
- 多语言
- 多币种
- 市场差异化
- GTM
- SEO
可以围绕:
共享组件
+
主题 / 配置
+
站点级配置
+
运行时 / 构建时变量
说明平台化思路。
C. 在线教育平台
5. React 和 Vue 项目为什么都引入 TanStack Query?
回答重点:
不因为框架不同就维护两套 server state 思想。服务端数据获取、缓存、失效、重试这些问题是通用的,所以 React/Vue 都统一使用 Query 模型。
6. Android WebView 适配有什么坑?
准备:
- JSBridge
- 生命周期
- 音频状态
- 页面返回
- 原生跳转
- Android 低版本浏览器能力
- viewport
- file / media 权限
- 页面缓存
你简历里有实际的:
- 音频控制
- 学习回调
- 原生跳转
D. 企业 IM
7. 会话转接为什么复杂?
需要考虑:
- 当前客服
- 目标客服 / 组
- 会话归属
- 历史消息
- 未读
- 在线状态
- 转接事件
- Web 和 Electron 一致性
8. 已读状态怎么同步?
准备:
用户读取
↓
客户端提交 lastReadSeq
↓
服务端持久化
↓
同步到其他客户端
↓
计算 unread
E. Joplin Agent
9. 为什么用 FTS5 + BM25,而不是向量数据库?
你简历只写了:
- SQLite FTS5
- BM25
- 本地知识库
因此面试可以保守回答:
这是本地工具,优先考虑部署简单、离线、轻量;FTS5 已经能很好支持关键词检索。是否引入 embedding / vector search,要看语义召回不足是否真的成为瓶颈。
不要虚构你已经做了向量检索。
10. 为什么只把命中片段发给模型?
因为:
- 控制 token
- 降低无关上下文
- 提高可追溯性
- 降低模型被大量无关笔记干扰的概率
如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录