跳到主要内容

React 面经

核心考点

1. Hooks 原理

  • useState:基于链表结构存储状态,每次渲染按调用顺序取值,不能在条件/循环中使用
  • useEffect:在 commit 阶段后异步执行;依赖数组为空时只在 mount 时执行;返回清理函数在下次 effect 执行前或 unmount 时调用
  • useCallback / useMemo:依赖不变时跳过重新计算;useCallback 缓存函数引用,useMemo 缓存计算结果
  • useRef:返回 mutable 对象,不触发重新渲染;常用于存访问 DOM 或保存跨渲染的值

2. Fiber 架构

  • React 16 引入,将渲染拆成可中断的「工作单元」
  • 两个阶段:Reconcile(可中断,生成 effect list)→ Commit(同步,不可中断,操作 DOM)
  • 支持时间分片(Time Slicing)和并发特性(Concurrent Mode)

3. Virtual DOM & Diff 算法

  • Diff 策略:同层对比 + key 复用
  • key 相同类型相同 → 复用节点;key 不同 → 销毁重建
  • 为什么不直接操作 DOM?批量更新 + 跨平台抽象

4. 状态管理对比

特点
Redux单向数据流,中间件丰富,适合大型项目
Zustand轻量,基于 hooks,无样板代码
Jotai原子化状态,按需订阅,细粒度更新
RecoilFacebook 出品,selector 依赖图

5. 性能优化

  • React.memo 包裹组件,跳过 props 未变化的渲染
  • useMemo / useCallback 缓存昂贵计算和回调
  • 列表渲染加稳定 key,避免不必要的重建
  • 懒加载:React.lazy + Suspense
  • 使用 useTransition 标记非紧急更新

6. 高频面试题

  • 合成事件 vs 原生事件:React 对原生事件封装,统一冒泡到 root 上处理
  • 批量更新:React 18 默认开启 Automatic Batching,多次 setState 合并为一次渲染
  • 为什么不能在循环中用 Hooks:Hooks 靠调用顺序与 fiber 节点一一对应
  • 受控 vs 非受控组件:受控组件状态由 React 管理;非受控用 ref 读取 DOM 值

💡 建议结合源码阅读 React 官方文档 react.dev 加深理解

React 性能优化实战

性能优化全景

1. 渲染优化

避免不必要的重渲染

  • React.memo(Component, areEqual) — 对比 props,默认浅比较
  • 父组件回调用 useCallback 稳定引用,防止子组件因回调引用变化重渲染
  • 将「变化的状态」下沉到最小作用域的子组件,减少影响范围

拆分组件粒度

// ❌ 状态变化导致整个列表重渲染
function Page() {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<HeavyList /> {/* 每次都重渲染 */}
</>
)
}

// ✅ 将 count 状态隔离
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
function Page() {
return (<><Counter /><HeavyList /></>)
}

2. 列表虚拟化

  • 数据量 > 500 条时使用 react-windowreact-virtual
  • 只渲染可视区域内的元素,降低 DOM 节点数
  • 配合 overscanCount 预渲染边缘元素,避免滚动白屏

3. 代码分割与懒加载

// 路由级分割
const Dashboard = React.lazy(() => import('./Dashboard'))

function App() {
return (
<Suspense fallback={<Skeleton />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
)
}
  • import() 动态导入 + React.lazy — 路由/弹窗级切割
  • webpack magic comment:/* webpackChunkName: "xxx" */ 命名 chunk
  • 预加载:鼠标 hover 时触发 import()

4. 并发特性(React 18)

API作用
useTransition将状态更新标记为「非紧急」,保持 UI 响应
useDeferredValue延迟某个值的更新,类似防抖
startTransition在事件处理器外触发低优先级更新
const [isPending, startTransition] = useTransition()
startTransition(() => {
setSearchQuery(value) // 低优先级,不阻塞输入
})

5. 状态管理性能

  • Context 拆分:避免把所有状态放一个 Context,按功能拆分降低订阅范围
  • Context + useMemo:value 对象需 memoize,否则每次渲染都是新引用
  • Zustand selectoruseStore(state => state.count) 只订阅用到的切片
  • Jotai 原子化:精确更新,只有依赖该 atom 的组件重渲染

6. 工具与度量

  • React DevTools Profiler — 录制渲染时间,找到耗时组件
  • Why Did You Render — 检测不必要的重渲染并输出原因
  • Lighthouse — 页面整体性能分
  • Web Vitals — LCP / FID / CLS 核心指标监控

7. 高频面试题

  • memo 一定有收益吗? 浅比较本身有成本,仅对渲染耗时高的组件使用
  • 如何定位慢渲染? Profiler 找 commit 时长 > 16ms 的组件,再缩小范围
  • Suspense 与错误边界的关系? Suspense 处理加载态,ErrorBoundary 处理错误态,两者搭配
  • 并发渲染会破坏 useEffect 顺序吗? 不会,effect 仍在 commit 后同步运行

React 源码解析(Fiber & Reconciler)

源码核心模块

1. Fiber 数据结构

type Fiber = {
tag: WorkTag // 节点类型(FunctionComponent/ClassComponent/HostComponent...)
key: null | string
type: any // 对应 createElement 的第一个参数
stateNode: any // DOM 节点 或 class 实例

return: Fiber | null // 父节点
child: Fiber | null // 第一个子节点
sibling: Fiber | null // 下一个兄弟节点

pendingProps: any // 本次渲染的 props
memoizedProps: any // 上次渲染的 props
memoizedState: any // hooks 链表的头节点

flags: Flags // 副作用标记(Placement/Update/Deletion)
subtreeFlags: Flags
updateQueue: any // 状态更新队列
}

2. 双缓冲机制

  • React 同时维护两棵 Fiber 树:current tree(当前屏幕)和 workInProgress tree(构建中)
  • 渲染完成后 root.current = workInProgress,两棵树交替复用
  • 好处:构建过程可中断,不影响当前显示内容

3. Reconcile 阶段(beginWork & completeWork)

深度优先遍历 Fiber

beginWork:根据 type 处理节点(diff children,打 flags)

若有子节点 → 递归处理子节点
若无子节点 → completeWork:创建/更新 DOM,收集 subtreeFlags

返回父节点继续 completeWork

Diff 策略

  1. 单节点 Diff:key 和 type 都相同 → 复用;否则销毁重建
  2. 多节点 Diff:两轮遍历
    • 第一轮:按顺序对比,遇到 key 不同则停止
    • 第二轮:将剩余旧节点放入 Map,用 key 查找可复用节点
    • 最长递增子序列优化移动操作(React 18)

4. Commit 阶段(三个子阶段)

子阶段工作内容
BeforeMutation读取 DOM 快照,调用 getSnapshotBeforeUpdate
Mutation操作真实 DOM(增/改/删),调用 useLayoutEffect 清理
Layout调用 useLayoutEffect 回调,更新 ref
  • useEffect 在 commit 后异步调度,不阻塞浏览器绘制
  • useLayoutEffect 在 Layout 阶段同步执行,会阻塞绘制

5. Hooks 实现原理

Fiber.memoizedStateHook₁Hook₂Hook₃ (链表)

{ memoizedState, queue, next }
  • mount 阶段:每个 useState 在链表末尾插入新 Hook 节点
  • update 阶段:按顺序取链表节点,计算新状态
  • 为什么不能条件渲染 Hooks:顺序错位会取到错误的 Hook 节点

6. 调度器(Scheduler)

  • 基于 MessageChannel 实现任务切片(非 requestAnimationFrame)
  • 每帧预算 5ms,超出则中断,让出主线程
  • 优先级队列:ImmediatePriority > UserBlockingPriority > NormalPriority > IdlePriority
  • lane 模型(React 18):用位运算表示优先级,支持批量处理多个优先级

7. 高频面试题

  • 为什么要用 MessageChannel 而非 setTimeout? setTimeout 最小延迟 ~4ms,MessageChannel 延迟更低
  • useEffect 清理函数什么时候执行? 下次 effect 执行前 或 组件 unmount 时
  • Concurrent Mode 如何保证状态一致性? 同一优先级的更新批量处理,低优先级更新被高优先级打断后重新计算

React 生态与工程化

React 生态全景

1. 路由:React Router v6

  • 嵌套路由<Outlet /> 渲染子路由
  • loader / action:数据加载和表单提交(Data Router 模式)
  • 懒加载路由lazy: () => import('./Page') 配合 <Suspense>
  • useNavigate / useParams / useSearchParams — 常用 hooks

2. 服务端渲染(SSR)与 Next.js

渲染模式说明适用场景
SSG(静态生成)构建时生成 HTML博客、文档
SSR(服务端渲染)请求时生成 HTML动态内容
ISR(增量静态再生)后台定时更新静态页电商、新闻
CSR(客户端渲染)浏览器渲染后台管理系统

Next.js App Router 核心概念

  • Server Components(默认):零 JS 发送到客户端
  • Client Components:'use client' 标记,保留交互能力
  • Server Actions:表单/变更直接调用服务端函数

3. 数据请求:React Query / SWR

// React Query 示例
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 分钟内认为数据新鲜
})
  • 自动缓存与去重:相同 queryKey 的请求只发一次
  • 后台刷新:窗口重新聚焦时自动 refetch
  • 乐观更新onMutate 先更新 UI,失败后回滚
  • 无限滚动useInfiniteQuery + fetchNextPage

4. 样式方案对比

方案特点代表库
CSS Modules局部作用域,零运行时内置支持
CSS-in-JS动态样式,有运行时开销styled-components, Emotion
Atomic CSS极致复用,HTML 类名多Tailwind CSS
Zero-runtime编译期提取,无运行时Linaria, vanilla-extract

5. 表单方案:React Hook Form

const { register, handleSubmit, formState: { errors } } = useForm()

<input {...register('email', { required: true, pattern: /^\S+@\S+$/ })} />
{errors.email && <span>邮箱格式错误</span>}
  • 非受控模式 + ref 收集值,避免每次输入触发重渲染
  • 配合 zod / yup 做 schema 验证

6. 测试工具链

  • Vitest / Jest — 单元测试
  • React Testing Library — 组件集成测试(关注行为而非实现)
  • Playwright / Cypress — E2E 测试
// RTL 示例
test('点击按钮增加计数', async () => {
render(<Counter />)
await userEvent.click(screen.getByRole('button', { name: /增加/ }))
expect(screen.getByText('1')).toBeInTheDocument()
})

7. 高频面试题

  • Server Component 与 Client Component 如何混用? SC 可以导入 CC,但 CC 不能导入 SC
  • React Query 与 Redux 的区别? RQ 专注服务端状态(异步/缓存),Redux 管理客户端状态
  • Next.js 如何做 SEO? generateMetadata 动态 meta,SSG/SSR 保证爬虫可读内容
  • monorepo 下 React 多版本共存的问题?peerDependencies + 包管理器 alias 解决

React 并发渲染与 Streaming SSR 深入

并发模式核心机制

1. Time Slicing 时间切片

  • Scheduler 将渲染任务切成 5ms 小片,每片执行完检查是否有高优先级任务
  • 利用 MessageChannel 而非 setTimeout,避免 4ms 最小延迟
  • 有更高优先级任务(如用户输入)则中断当前渲染,先处理紧急任务

2. Lane 优先级模型(React 18)

// 简化版 lane 定义(用二进制位表示优先级)
const SyncLane = 0b0000001 // 最高:同步
const InputContinuousLane = 0b000100 // 用户连续输入(scroll/drag)
const DefaultLane = 0b010000 // 普通更新
const TransitionLane = 0b001000000 // transition 标记的更新
const IdleLane = 0b100000000000000 // 最低:空闲

// 多个优先级合并
const pendingLanes = SyncLane | TransitionLane
// 取最高优先级
const nextLanes = getHighestPriorityLanes(pendingLanes)

3. Automatic Batching(自动批量更新)

// React 17 之前:setTimeout 中不批量
setTimeout(() => {
setCount(c => c + 1) // 触发一次渲染
setFlag(f => !f) // 再触发一次渲染
}, 1000)

// React 18:所有场景自动批量
setTimeout(() => {
setCount(c => c + 1)
setFlag(f => !f)
// 只触发一次渲染!
}, 1000)

// 需要强制非批量时
import { flushSync } from 'react-dom'
flushSync(() => setCount(c => c + 1)) // 立即渲染

4. useTransition 与 useDeferredValue

// useTransition:标记低优先级状态更新
const [isPending, startTransition] = useTransition()

function handleSearch(value: string) {
setInputValue(value) // 高优先级:立即更新输入框
startTransition(() => {
setSearchQuery(value) // 低优先级:可被中断
})
}

// isPending 可用于显示过渡状态
return (
<>
<input value={inputValue} onChange={e => handleSearch(e.target.value)} />
{isPending ? <Spinner /> : <SearchResults query={searchQuery} />}
</>
)

// useDeferredValue:延迟某个值的更新,类似防抖但不固定时间
function SearchPage() {
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
const isStale = query !== deferredQuery

return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<Suspense fallback={<Loading />}>
<Results query={deferredQuery} />
</Suspense>
</div>
)
}

5. Streaming SSR

传统 SSR vs Streaming SSR

传统 SSR(All-or-Nothing):
服务端获取所有数据 -> 渲染完整 HTML -> 发送 -> 客户端 Hydration
瓶颈:必须等最慢的数据请求完成才能发送第一个字节

Streaming SSR(React 18):
服务端流式发送 HTML -> 客户端逐步 Hydration
优势:TTFB 更低,用户更快看到内容
// 服务端使用 renderToPipeableStream
import { renderToPipeableStream } from 'react-dom/server'

const { pipe, abort } = renderToPipeableStream(
<App />,
{
bootstrapScripts: ['/static/js/main.js'],
onShellReady() {
// Shell 就绪(不含 Suspense 边界内容)立即发送
response.statusCode = 200
response.setHeader('Content-type', 'text/html')
pipe(response)
},
onError(error) {
response.statusCode = 500
console.error(error)
}
}
)

// 客户端组件使用 Suspense 分割边界
function App() {
return (
<Layout> {/* Shell:立即渲染 */}
<Suspense fallback={<ArticleSkeleton />}>
<Article /> {/* 数据就绪后流式注入 */}
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments /> {/* 独立加载,不阻塞其他 */}
</Suspense>
</Layout>
)
}

6. Selective Hydration(选择性水合)

  • 用户与某个还未 hydrate 的组件交互时,React 优先 hydrate 该组件
  • 其他组件继续排队,不会阻塞用户交互
  • 需要配合 React.lazy + Suspense 使用

7. Server Components 数据流

1. 请求到达服务端
2. Server Components 执行(可直接访问 DB
3. 生成 RSC Payload(特殊序列化格式,非 HTML
4. Client Components 占位符插入 Payload
5. Payload 流式传输到客户端
6. React 在客户端重建组件树并 Hydrate Client Components

RSC Payload 格式示例:
0:["$","div",null,{"children":["$","$L1","1",{}]}]
1:I{"id":"./ClientComponent","chunks":["client"]}

8. 资深面试题

  • useSyncExternalStore 解决了什么问题?
    • 并发渲染中,一次渲染可能被中断后重新执行,期间外部 store 可能变化导致 UI 撕裂(tearing)
    • useSyncExternalStore 强制同步读取,确保一次渲染内数据一致
  • startTransition 如何实现「可中断」?
    • 更新被标记为 TransitionLane(低优先级)
    • 执行过程中如有高优先级更新(SyncLane)到来,当前渲染丢弃重新来过
    • 因此 transition 内的更新可能执行多次,需保证纯函数
  • Streaming SSR 中如何处理 SEO?
    • Shell(包含关键内容)立即发送,搜索引擎可读取
    • 关键 meta 信息放在 Shell 中,非关键内容放在 Suspense 边界内
    • Google 支持 JavaScript 渲染,但 Streaming 方式更友好

React 状态管理深度对比

状态管理方案全面对比

1. Redux Toolkit(RTK)

// slice 定义
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

export const fetchUser = createAsyncThunk(
'user/fetchById',
async (userId: string, { rejectWithValue }) => {
try {
return await api.getUser(userId)
} catch (err) {
return rejectWithValue(err.message)
}
}
)

const userSlice = createSlice({
name: 'user',
initialState: { data: null, loading: false, error: null } as UserState,
reducers: {
clearUser: state => { state.data = null } // Immer 支持「可变」写法
},
extraReducers: builder => {
builder
.addCase(fetchUser.pending, state => { state.loading = true })
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false
state.data = action.payload
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false
state.error = action.payload as string
})
}
})

RTK Query(数据获取层)

const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['User', 'Post'],
endpoints: builder => ({
getUser: builder.query<User, string>({
query: id => `/users/${id}`,
providesTags: (result, err, id) => [{ type: 'User', id }]
}),
updateUser: builder.mutation<User, Partial<User>>({
query: ({ id, ...body }) => ({ url: `/users/${id}`, method: 'PUT', body }),
invalidatesTags: (result, err, { id }) => [{ type: 'User', id }]
})
})
})

// 组件中使用
const { data: user, isLoading } = useGetUserQuery(userId)
const [updateUser, { isLoading: isUpdating }] = useUpdateUserMutation()

2. Zustand

// 轻量,基于 hooks,无需 Provider
import { create } from 'zustand'
import { devtools, persist, immer } from 'zustand/middleware'

const useUserStore = create<UserStore>()(
devtools(
persist(
immer(set => ({
user: null,
loading: false,
fetchUser: async (id: string) => {
set(state => { state.loading = true })
const user = await api.getUser(id)
set(state => { state.user = user; state.loading = false })
},
clearUser: () => set({ user: null })
})),
{ name: 'user-storage' } // 自动持久化到 localStorage
),
{ name: 'UserStore' } // DevTools 中显示的名称
)
)

// 精确订阅(避免不必要的重渲染)
const user = useUserStore(state => state.user)
const loading = useUserStore(state => state.loading)
// 不要:const { user, loading } = useUserStore() 会订阅整个 store

Zustand 的切片模式(大型应用)

// 将 store 拆成多个切片组合
const useUserSlice = (set, get) => ({
user: null,
setUser: (user) => set({ user })
})

const useCartSlice = (set, get) => ({
items: [],
addItem: (item) => set(state => ({ items: [...state.items, item] }))
})

const useBoundStore = create((...a) => ({
...useUserSlice(...a),
...useCartSlice(...a)
}))

3. Jotai(原子化状态)

import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
import { atomWithQuery } from 'jotai-tanstack-query'

// 基础 atom
const countAtom = atom(0)
const doubleAtom = atom(get => get(countAtom) * 2) // 派生 atom

// 异步 atom
const userAtom = atomWithQuery(get => ({
queryKey: ['user', get(userIdAtom)],
queryFn: () => fetchUser(get(userIdAtom))
}))

// 只订阅需要的 atom,精确更新
function Counter() {
const [count, setCount] = useAtom(countAtom)
const double = useAtomValue(doubleAtom) // 只读
// ...
}

4. 方案选型指南

场景推荐方案
大型企业应用,需要 DevTools 调试Redux Toolkit
中型应用,追求简洁Zustand
细粒度更新,服务端状态Jotai + React Query
纯服务端状态管理React Query / SWR
简单全局状态Context + useReducer

5. Context 性能优化

// 问题:Context 值变化导致所有消费者重渲染
const ThemeContext = createContext(null)

// 方案1:拆分 Context(读写分离)
const ThemeValueContext = createContext(null)
const ThemeDispatchContext = createContext(null)

function ThemeProvider({ children }) {
const [theme, dispatch] = useReducer(reducer, initialState)
return (
<ThemeValueContext.Provider value={theme}>
<ThemeDispatchContext.Provider value={dispatch}>
{children}
</ThemeDispatchContext.Provider>
</ThemeValueContext.Provider>
)
}

// 方案2:useMemo 稳定 value 引用
function Provider({ children }) {
const [user, setUser] = useState(null)
const value = useMemo(() => ({ user, setUser }), [user])
return <Ctx.Provider value={value}>{children}</Ctx.Provider>
}

// 方案3:使用 use-context-selector 精确订阅
import { createContext, useContextSelector } from 'use-context-selector'
const user = useContextSelector(AppContext, ctx => ctx.user)

6. 资深面试题

  • Redux 与 Zustand 的根本区别?
    • Redux:单一 store,严格的单向数据流,action -> reducer -> state
    • Zustand:多 store,直接更新 state,无 action 约束,更灵活
    • Redux 更适合需要 time-travel 调试、复杂中间件的场景
  • 为什么说 React Query 不是状态管理工具?
    • React Query 管理的是服务端状态的缓存和同步
    • 状态管理工具管理的是纯客户端状态
    • 两者职责不同,可以共存使用
  • Jotai 如何解决 Context 的性能问题?
    • 每个 atom 是独立的 Context,消费者只订阅用到的 atom
    • 使用 WeakMap 存储 atom 值,不同组件订阅不同 atom 互不影响
    • 派生 atom 只在依赖变化时重新计算

React 大型应用架构设计

大型 React 应用设计原则

1. Feature-Sliced Design 架构

src/
app/ # 应用层:路由、Provider、全局样式
pages/ # 页面层:组合 widgets 组成页面
widgets/ # 组件层:独立的页面区块(Header、Sidebar)
features/ # 功能层:用户操作(点赞、搜索、评论)
entities/ # 实体层:业务对象(User、Article、Order)
shared/ # 共享层:UI 组件库、工具函数、API 客户端

依赖规则:上层可引用下层,同层之间不能随意引用。

模块边界:每个 slice 只通过 index.ts 暴露公开 API。

2. 状态分层管理

服务端状态(Server State)
-> 来自 API,有缓存、失效、同步问题
-> React Query / SWR / RTK Query

客户端状态(Client State)
-> UI 状态(弹窗开关、选中项)
-> useState / useReducer / Zustand

表单状态(Form State)
-> 表单值、验证、提交
-> React Hook Form + Zod

URL 状态(URL State)
-> 搜索参数、分页、筛选条件
-> useSearchParams / nuqs 库

关键原则:不要把服务端状态放进 Redux/Zustand,React Query 专门处理异步状态。

3. 代码分割策略

// 路由级分割(必做)
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Analytics = lazy(() => import('./pages/Analytics'))

// 功能级分割(大功能模块)
const RichEditor = lazy(() => import('./features/editor/RichEditor'))

// 预加载策略(hover 时触发)
const prefetchDashboard = () => import('./pages/Dashboard')

<Link
to="/dashboard"
onMouseEnter={prefetchDashboard} // 鼠标悬停时预加载
>
Dashboard
</Link>

4. 组件设计模式

Compound Components(复合组件)

// 用于构建高度可定制的组件
const Select = ({ children, value, onChange }) => {
return (
<SelectContext.Provider value={{ value, onChange }}>
<div className="select">{children}</div>
</SelectContext.Provider>
)
}
Select.Option = ({ value, children }) => {
const { value: selected, onChange } = useSelectContext()
return (
<div
className={selected === value ? 'selected' : ''}
onClick={() => onChange(value)}
>
{children}
</div>
)
}

// 使用
<Select value={val} onChange={setVal}>
<Select.Option value="a">Option A</Select.Option>
<Select.Option value="b">Option B</Select.Option>
</Select>

Render Props / Children as Function

// 将渲染逻辑控制权交给使用者
function DataProvider({ render }) {
const [data] = useFetch('/api/data')
return render(data)
}

// 使用:自定义渲染
<DataProvider render={data => <CustomChart data={data} />} />

useImperativeHandle 暴露命令式接口

const Dialog = forwardRef((props, ref) => {
const [open, setOpen] = useState(false)

useImperativeHandle(ref, () => ({
open: () => setOpen(true),
close: () => setOpen(false),
}))

return open ? <div>{props.children}</div> : null
})

// 使用
const dialogRef = useRef(null)
<Dialog ref={dialogRef} />
<button onClick={() => dialogRef.current.open()}>打开</button>

5. Monorepo 组织

packages/
ui/ # 共享 UI 组件库(Design System)
utils/ # 工具函数
types/ # 共享 TypeScript 类型
api-client/ # API 客户端(自动生成)
config/ # 共享配置(ESLint、TS、Tailwind)
apps/
web/ # 主应用
admin/ # 管理后台
mobile/ # React Native 应用

工具链:Turborepo(增量构建缓存)+ pnpm workspaces

版本管理:内部包用 workspace:*,公共包用 Changesets

6. 测试体系

// 单元测试:纯函数、hooks、工具
test('useCounter', () => {
const { result } = renderHook(() => useCounter(0))
act(() => result.current.increment())
expect(result.current.count).toBe(1)
})

// 集成测试:组件 + API Mock(MSW)
test('登录成功后跳转首页', async () => {
server.use(
http.post('/api/login', () => HttpResponse.json({ token: 'abc' }))
)
render(<LoginPage />, { wrapper: Providers })
await userEvent.type(screen.getByLabelText('邮箱'), 'a@b.com')
await userEvent.click(screen.getByRole('button', { name: '登录' }))
await expect(screen.findByText('欢迎回来')).resolves.toBeInTheDocument()
})

// E2E(Playwright):核心用户旅程
test('完整购买流程', async ({ page }) => {
await page.goto('/products')
await page.click('[data-testid=add-to-cart]')
await page.goto('/checkout')
await page.fill('[name=card]', '4242 4242 4242 4242')
await page.click('button[type=submit]')
await expect(page.getByText('订单已确认')).toBeVisible()
})

7. 资深面试题

  • 如何设计一个跨团队复用的组件库?
    • 严格的语义化版本 + Changesets 管理
    • Storybook 文档 + 视觉回归测试(Chromatic)
    • Compound Component 模式提供灵活性
    • 组件 API 向后兼容,breaking change 走大版本
  • 微前端架构中 React 应用如何共享状态?
    • CustomEvent / window.postMessage(松耦合)
    • Module Federation 共享单例 store
    • URL 作为通信载体(筛选条件、路由参数)
  • 大型 React 应用如何做权限控制?
    • 路由级:<PrivateRoute> 包裹需鉴权路由
    • 组件级:usePermission('edit:post') hook
    • 数据级:服务端返回时过滤,前端只做展示判断
    • Feature Flag:LaunchDarkly 实现灰度和 A/B 测试

前端工程化与性能监控面经

前端工程化核心知识

1. Webpack 深度

核心概念

Entry -> Loader 处理 -> Plugin-> Bundle 输出

Loader:转换单个文件(babel-loader / css-loader / file-loader)
Plugin:串联整个构建生命周期(HtmlWebpackPlugin / MiniCssExtractPlugin)
Resolver:模块寻址解析

Tree Shaking 原理

  • 基于 ESM 静态分析模块导入导出
  • Webpack 标记未使用的导出为 unused exports
  • Terser 在压缩时删除未使用代码
  • sideEffects: false 可帮助 Webpack 更激进地删除

Code Splitting

// 1. 入口分割
entry: { main: './src/index.js', vendor: './src/vendor.js' }

// 2. 动态导入
const LazyComp = () => import('./HeavyComponent')

// 3. SplitChunksPlugin 公共依赖提取
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /node_modules/,
name: 'vendors',
chunks: 'all'
}
}
}
}

Webpack vs Vite 对比

对比项WebpackVite
开发启动打包所有模块ESM 按需加载
HMR 速度所有模块出发只有变化的模块
生产构建WebpackRollup
配置复杂度
适用场景复杂大型中小型,新项目

2. Vite 深度

开发模式原理

1. 浏览器请求模块
2. Vite Dev Server 收到请求
3. 对模块应用转换(如 TypeScript 编译)
4. 返回转换后的模块
-> 无需打包,启动速度极快

预构建依赖

  • 第一次启动时用 esbuild 预构建 node_modules
  • esbuild 是 Go 写的,比 Babel 快 10-100x
  • 预构建结果缓存在 node_modules/.vite 中

自定义插件

// vite.config.ts
export default {
plugins: [
{
name: 'my-transform',
transform(code, id) {
if (id.endsWith('.svg')) {
return `export default ${JSON.stringify(code)}`
}
}
}
]
}

3. CI/CD 流水线

# GitHub Actions 示例
name: Deploy
on:
push:
branches: [main]

jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'

- name: Install
run: pnpm install --frozen-lockfile

- name: Lint
run: pnpm lint

- name: Test
run: pnpm test --coverage

- name: Build
run: pnpm build
env:
VITE_API_BASE: ${{ secrets.API_BASE }}

- name: Deploy to CDN
run: pnpm deploy

4. Web 性能监控

Core Web Vitals

指标全称良好阈值优化策略
LCP最大内容绘制< 2.5sSSR、图片优先级、CDN
INP交互到下一帧< 200ms少 JS、Web Worker
CLS累积布局偏移< 0.1图片设宽高、字体预加载

Performance API

// 页面性能采样
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (entry.entryType === 'largest-contentful-paint') {
console.log('LCP:', entry.startTime)
}
if (entry.entryType === 'layout-shift') {
console.log('CLS score:', entry.value)
}
})
})
observer.observe({ type: 'largest-contentful-paint', buffered: true })
observer.observe({ type: 'layout-shift', buffered: true })

// Navigation Timing
const timing = performance.timing
const pageLoad = timing.loadEventEnd - timing.navigationStart
const domReady = timing.domContentLoadedEventEnd - timing.navigationStart
const ttfb = timing.responseStart - timing.requestStart

前端监控体系

// 错误监控
window.addEventListener('error', (event) => {
sendToSentry({
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error?.stack
})
})

window.addEventListener('unhandledrejection', (event) => {
sendToSentry({ message: event.reason?.message })
})

// 资源监控
const resourceObserver = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (entry.duration > 1000) {
// 资源加载过慢
reportSlowResource(entry)
}
})
})
resourceObserver.observe({ type: 'resource', buffered: true })

5. 包体积分析

# Webpack Bundle Analyzer
npx webpack-bundle-analyzer stats.json

# Vite 分析
npx vite-bundle-visualizer

# 常见优化手段
1. 添加 lodash 用 lodash-es 替代
2. moment.js 用 dayjs 替代(体积约 80% 小)
3. echarts 按需导入组件,不要全量导入
4. 图片用 WebP + 懒加载
5. 富文本编辑器就答动态加载

6. 资深面试题

  • Webpack 的 HMR 原理?
    • Dev Server 与浏览器建立 WebSocket
    • 文件变化时,重新编译对应模块
    • 向浏览器推送更新后的模块 hash
    • 浏览器下载新模块,通过 module.hot.accept 回调操作
  • Vite 为什么开发时很快但构建时还用 Rollup?
    • 开发时利用浏览器原生 ESM,无需打包
    • 生产构建需要 Tree Shaking、代码分割、压缩等 Rollup 的能力
    • 未来 Rolldown(Rust 实现)将统一开发和生产
  • 如何实现前端性能监控平台?
    • Performance API 采频 Core Web Vitals
    • 错误监控捕获未处理异常
    • User Timing API 自定义性能埋点
    • 上报到分析平台(自建 + 各种 SaaS)
  • Webpack 构建慢如何优化?
    • cache: { type: 'filesystem' } 文件系统缓存
    • thread-loader 并行处理 Loader
    • babel-loader 配置 include: /src/ 缩小范围
    • 将稳定依赖定义为 外部扩展 externals + CDN