SSR 与测试

共 34 题
📑 题目列表 34 题
#
★★★

1. hydrateRoot 与 Selective Hydration 在长阻塞组件的工程价值

hydrateRoot 与 Selective Hydration 是什么?Selective Hydration 在长阻塞组件场景的工程价值是什么?

  • hydrateRoot 的水合语义
  • Selective Hydration 的按需水合
  • 长阻塞组件场景的收益

hydrateRoot(container, ) 是 React 19 的 SSR 水合入口:服务端渲染的 HTML 已在浏览器显示,hydrateRoot 把 React 组件树"接"到既有 DOM 上(不重建 DOM,只绑定事件与状态),形成可交互应用;与 createRoot(客户端渲染)不同,水合期间 React 假定 DOM 内容与服务端输出一致(不一致产生 mismatch 警告)。Selective Hydration(选择性水合)是并发特性:水合不再"整树一次完成",而是按需进行——浏览器空闲/用户交互时逐个边界水合(默认按 Suspense 边界 + 优先级),交互优先(用户点击的组件优先水合)、空闲批量水合其余。

长阻塞组件场景的工程价值:若某组件(大图表、富文本)水合昂贵(长任务阻塞主线程),传统整树水合会阻塞整个页面交互直到全部完成;Selective Hydration 下——页面其余部分先水合可交互,长阻塞组件的水合可被推迟/中断(水合是并发任务,可切片、可被输入打断)、被用户交互的组件优先水合;用户点击"未水合的组件"时 React 优先水合该组件(交互驱动的按需水合),感知延迟最小化。边界:水合本身仍要执行(只是编排更优);SSR 输出与客户端一致的约束不变;配合 Suspense 边界的流式内容,水合按边界进行;长阻塞组件应配合代码分割(React.lazy)让"长水合"与"按需加载"叠加;测量用 Performance 面板看水合长任务的切片与交互响应(INP)。

答题先讲 hydrateRoot 的水合语义(DOM 接管 + 一致性约束),再讲 Selective Hydration 的按需/优先级水合机制,最后落在长阻塞组件的"交互优先 + 可中断水合"价值与边界。

#
★★★

2. React 19 的 ref 作为 prop 的传递简化(ref 不再需要 forwardRef)

React 19 中 ref 作为 prop 传递的简化是什么?为什么不再需要 forwardRef?

  • ref-as-prop 的机制与兼容
  • forwardRef 的弃用与迁移
  • 与 useImperativeHandle 的配合

React 19 中函数组件可以直接接收 ref 作为普通 prop:function Child({ ref, ...props })(ref 出现在 props 中),父组件 时子组件无需 forwardRef 包裹即可拿到 ref 并挂到内部 DOM(ref 作为 prop 传递,语义与普通 prop 一致:可命名、可被透传、可在条件中);forwardRef 在 React 19 中已不需要(保留兼容,不推荐新代码使用)。机制:React 19 把 ref 从"特殊保留属性"改为"可透传 prop"(与 className 等并列),协调器直接把 ref 放进 props 传递——消除了 forwardRef 这一层"透传样板"。

工程影响:简化高阶组件与包装组件(直接解构 ref 传给子元素);配合 useImperativeHandle 定制暴露的命令式 API(用法不变:useImperativeHandle(ref, ...));迁移:新代码用 props.ref,存量 forwardRef 可机械移除(react-codemod 提供迁移);注意点:ref 仍是"特殊语义"的 prop(指向 DOM 节点/组件实例的引用),不要与自定义 ref 命名混淆(自定义命名如 inputRef 仍是普通 prop 传法);Class 组件仍通过 this.refs/React.createRef 语义(类组件 ref 指向实例,React 19 未改变);ref 作为 prop 后,HOC 包装的 ref 透传更自然(不再有"ref 穿透"的坑)。整体:ref-as-prop 是 React 19 的"消除样板"类改进,降低命令式 API 的传递成本。

答题先讲 ref 作为普通 prop 的接收与透传机制(免 forwardRef),再讲 useImperativeHandle 配合与 codemod 迁移,最后给兼容性与命名边界。

function Input({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}
// 无需 forwardRef,ref 直接解构使用
#
★★★

3. Testing Library 查询优先级(getByRole 优先、慎用 getByTestId)的设计哲学,及其与可访问性测试的内在关系

Testing Library 的查询优先级(getByRole 优先、慎用 getByTestId)的设计哲学是什么?它与可访问性测试有什么关系?

  • 查询优先级顺序(角色 > 文本 > 测试 id)
  • 用户视角测试哲学
  • 可访问性测试的协同

Testing Library 的查询优先级:getByRole > getByLabelText > getByPlaceholderText > getByText > getByDisplayValue > getByAltText > getByTitle > getByTestId——设计哲学是"以用户能看到/听到的方式查询":用户通过"角色 + 可访问名称"(role/name)感知 UI(按钮、链接、输入框的语义),role 查询最贴近用户视角;getByText 其次(用户读到的文本);getByTestId 是"最后手段"(查询实现细节、用户不可见),仅用于"语义无法表达"的场景(如特殊测试钩子)。哲学依据:测试应验证"用户可见行为"而非"实现细节",实现重构(改 DOM 结构、改类名)不破坏测试;测试与可访问性同构——能通过 role/name 查询的元素,必然具备正确的语义结构。

与可访问性测试的内在关系:role 查询强制开发者提供正确的语义(按钮有 button 角色、输入框有 label 关联、图标有 aria-label)——写不出 role 查询说明元素缺乏可访问性语义;getByRole + name 断言等于"验证可访问名称正确";结合 jest-dom 的无障碍断言(toHaveAccessibleName/toBeValid/toHaveFocus)让组件测试兼具"可访问性测试"功能(自动检查键盘可达、标签完整、无效状态表达);由此"测试驱动可访问性":从测试写起就能发现缺 label、错误角色、tab 顺序问题;边界:getByTestId 不是禁用(复杂虚拟化列表、Canvas 等无语义元素可用),但要记录"为什么用";role 查询的 name 匹配需理解可访问名称计算规则(label、aria-label、文本内容)。

答题先讲查询优先级的完整排序与"用户视角"哲学(语义 > 文本 > 实现细节),再讲与可访问性的同构关系(role 查询即语义检查)与 jest-dom 无障碍断言,最后给 getByTestId 的合理边界。

#
★★★

4. user-event 与 fireEvent 的差异(完整交互序列 vs 单个 DOM 事件),如何测试键盘与指针异步交互

user-event 与 fireEvent 的差异是什么?如何测试键盘与指针的异步交互?

  • 完整交互序列 vs 单个事件的差异
  • 异步交互(键盘、指针)的模拟
  • user-event 的 setup 与 advanced API

fireEvent 直接派发单个 DOM 事件(fireEvent.click(el) 只触发 click 事件本身,跳过事件序列与浏览器行为:焦点变化、事件冒泡顺序、键盘事件的 keydown/keyup 序列、指针事件链);user-event 模拟"真实用户交互":user.click 会派发完整的指针事件序列(pointerover → pointerdown → mousedown → focus → pointerup → mouseup → click)、user.type 派发键盘事件序列(keydown → keypress → input → keyup)并更新目标值、支持 Tab 焦点导航(user.tab 按可聚焦顺序移动焦点)、拖拽(pointer 序列)、粘贴(clipboard)等——测试更贴近真实浏览器行为,能暴露"只靠单个事件测不出"的问题(如 onChange 依赖 input 事件、焦点管理、组合键)。

异步交互测试:user-event 的 API 是异步的(await user.click(...)),因为真实交互包含异步行为(事件循环、默认行为、防抖);使用 setup() 创建实例(userEvent.setup())以隔离配置(delay、skipClick 等);测试键盘——user.keyboard 模拟按键序列(含修饰键 {Shift>}、组合),user.type 输入文本(触发 input 事件);指针——user.pointer 模拟 pointer 事件(hover/active/拖拽);断言异步结果用 waitFor/findBy(真实交互触发状态更新与重渲染);边界:user-event 的完整序列比 fireEvent 慢(测试数量大时权衡)、依赖 jsdom 对事件默认行为的模拟(jsdom 覆盖不了的行为用 Browser Mode/E2E 补);建议默认 user-event(贴近用户),fireEvent 保留给"精确单事件"的边界场景。

答题先讲"完整交互序列 vs 单个事件"的本质差异(序列、默认行为、焦点),再讲 user-event 的异步 API 与键盘/指针模拟方法(setup、keyboard、pointer、tab),最后给选型与边界。

#
★★★

5. 用 MSW 在组件测试层 mock 网络请求的工程实践(setupServer、handler 复用与重置)

如何用 MSW 在组件测试层 mock 网络请求?setupServer、handler 复用与重置的工程实践是什么?

  • MSW 的拦截机制(Service Worker / Node)
  • setupServer 与 handler 的组织
  • 每测试重置与覆盖策略

MSW(Mock Service Worker)在浏览器用 Service Worker、在 Node(测试环境)用网络层拦截(setupServer 基于拦截器)统一 mock HTTP 请求:测试代码写"与真实 fetch 相同形态"的 handler(rest.get('/api/users', ...) 或 http.get),组件内 fetch 请求被拦截并返回 mock 数据——无需改组件代码、无需注入依赖,测的是"真实请求路径"的组件。工程实践:setupServer(...handlers) 在测试文件中创建服务端实例,beforeAll 启动、afterEach 重置、afterAll 关闭(Vitest/Jest 的 setup 文件统一配置,避免每个测试重复样板)。

handler 复用与重置:公共 handler(认证、用户信息等通用接口)放共享模块(test/handlers.ts)供所有测试复用;测试内用 server.use(...) 覆盖特定接口(每个测试独立覆盖,afterEach 的 server.resetHandlers() 恢复默认 handler,防止测试间泄漏);覆盖策略:默认 handler 返回"正常数据",特定测试覆盖为错误/慢响应/空数据;配合"未处理的请求"告警(server.listen({ onUnhandledRequest: 'error' }))——组件发出了未 mock 的请求立即失败,防止漏 mock 导致测试连真实网络;延时模拟(delay)测试 loading 态;MSW 的 handler 与 Playwright 的 request mock 可共享(同定义跨层复用)。边界:MSW 拦截"网络层"(fetch/XHR),不拦截模块内直接调用的函数(非网络调用用 vi.mock);Browser Mode 测试用浏览器版 MSW(worker 启动)。

答题先讲 MSW 的拦截原理(Node 网络层/浏览器 SW)与"不改组件代码"的收益,再讲 setupServer 生命周期与共享 handler/每测试覆盖重置,最后给未处理请求告警与跨层复用的实践。

#
★★★

6. React Testing Library 的 act 警告产生原因,及其与并发渲染、状态批量更新的关系

React Testing Library 的 act 警告产生原因是什么?它与并发渲染、状态批量更新有什么关系?

  • act 的作用域语义
  • 警告产生的场景(未包裹的更新)
  • 与并发渲染/批处理的关联

act(react-dom/test-utils 的 act,RTL 内部自动使用)声明"测试中的一段更新作用域":React 在 act 内执行的更新会被同步 flush(渲染与 effect 立即完成),让断言时机确定;act 警告("not wrapped in act(...)")产生于——测试代码(或被测代码触发的异步回调)在 act 之外执行了状态更新(未 await 的 user-event 回调、setTimeout/setInterval 回调、promise resolve 后的 setState、非 RTL 派发的事件),React 检测到"更新发生在 act 外"而警告,因为该更新未同步 flush,断言可能读到旧状态、effect 未运行导致测试不稳定。

与并发渲染、批量更新的关系:React 19 并发渲染下更新默认异步调度(批量合并、Transition 可延迟),act 通过"捕获 act 作用域内的所有更新并强制同步提交"把异步调度变成确定性时序——act 警告的本质是"调度与测试时序不同步"的信号;RTL 的 waitFor/findBy 内部处理异步更新(轮询直到断言通过),配合 act 避免警告;user-event 的异步 API(await)内部已包裹 act;并发特性(startTransition 的更新、useDeferredValue 的延迟更新)在测试中同样需 await/waitFor 收敛(act 包裹的更新不保证 Transition 立即完成,需 waitFor 等待最终状态);实践:所有异步交互 await、异步断言用 waitFor/findBy、定时器用 fake timers 配合 act、全局启用"警告即失败"(错误边界 + strict 配置)强制无警告测试;根治思路:警告不是装饰,是"测试时序不真实"的信号。

答题先讲 act 的"同步 flush 作用域"机制与警告场景(act 外的更新),再讲与并发异步调度的关系(act 把异步调度转确定性时序),最后给 await/waitFor/fake timers 的规范实践。

#
★★★

7. 流式 SSR 中 Suspense boundary 在 renderToReadableStream 的暂停-恢复机制

流式 SSR 中 Suspense boundary 在 renderToReadableStream 的暂停-恢复机制是什么?

  • renderToReadableStream 的流式输出
  • 边界的暂停(shell 先行)与恢复(补发 chunk)
  • 客户端对增量内容的处理

renderToReadableStream(, { onShellReady, onShellError, onAllReady }) 把组件树渲染为可读流:渲染到第一个 Suspense 边界"挂起"时,流会"暂停"该边界——先把边界之外的 HTML(shell:首屏可呈现部分,含边界占位符与脚本)冲刷给客户端(onShellReady 触发,TTFB 提前);边界内的数据(异步组件/数据)就绪后,服务端继续渲染该边界并把增量 HTML(含模板注释标记的边界内容)以独立 chunk 流式补发(恢复),客户端收到后把内容插入对应边界位置,替换 fallback。机制核心:"暂停"是渲染中断等待 promise,"恢复"是 promise resolve 后继续渲染并输出增量——每个 Suspense 边界是独立的流式单元,嵌套边界各自独立暂停/恢复。

工程细节:shell 内的 HTML 完整可显示(无 JS 也可看到首屏,SEO 友好);补发的 chunk 含边界标识(模板注释),客户端水合时按标识挂载;onShellError(shell 阶段错误)与边界错误(由 errorElement/ErrorBoundary 处理或 onError 收集);onAllReady 表示全部边界完成(可配合缓存完整 HTML);超时/慢数据控制:慢边界延迟整体完成时间(可通过"降级为同步等待关键边界"平衡);配合 RSC:同一流中既有 HTML 流又有 Flight payload 流(React 19 统一管线);测量:TTFB(shell 时间)与 TTI(全部水合)的差值是流式收益的体现;实现上基于 Web Streams(Node 18+),兼容 renderToPipeableStream(Node 流形态)。

答题先讲 renderToReadableStream 的"先 shell 后增量"流式模型(暂停 = 等待 promise、恢复 = 补发 chunk),再讲客户端接收与水合机制,最后给错误处理、超时与 RSC 双流协作的工程细节。

#
★★★

8.

React Server Functions 在 Mutations 与表单

的协作

React Server Functions 在 Mutations 与

中如何协作?工程实践是什么?

  • Server Function 作为 action 的调用协议
  • 表单提交的 Mutation 流程
  • 结果返回与状态管理

Server Function 可直接作为

的 action:提交时浏览器(JS 启用)把 FormData 序列化为 Flight 请求(含 action ID)POST 到服务端,服务端执行函数(写操作/校验/返回结果),结果经 Flight 序列化回传;React 自动管理 pending(useFormStatus/useActionState)与提交后表单重置。Mutation 语义:Server Function 承担"写操作"职责(数据库、外部 API),与"读操作"(RSC 的数据获取)分离——表单提交 = 客户端收集 FormData → 服务端执行 mutation → 返回结果/错误 → 客户端回显(useActionState 的 state)→ 需要时触发数据刷新(revalidatePath/invalidateQueries)。

工程实践:action 内做完整校验(服务端信任边界)与事务处理(多个写操作原子化),错误以结构化结果返回({ ok: false, errors })而非抛错(抛错不进入 state);useActionState(fn, init) 包装 action 获得 [state, formAction, isPending],state 用于渲染服务端返回的错误/结果;提交成功后的数据刷新:Next.js 中 action 内调用 revalidatePath/revalidateTag 让相关 RSC 数据失效重取(表单提交与列表刷新闭环);乐观更新:useOptimistic 在 pending 期间展示预期结果,action 返回后收敛;幂等与防重:action 内用幂等键 + useFormStatus 禁用按钮;渐进增强:无 JS 时表单原生 POST 到同一端点(服务端渲染返回结果页)。边界:action 参数/返回值必须可序列化;不要在前端直接调用服务端内部逻辑(权限校验在 action 内);大文件上传走 multipart 约定而非 Flight 序列化。

答题先讲"Server Function 作为 form action 的 Flight 调用协议与 pending/重置管理",再讲 mutation 职责分离与结构化错误、revalidate 闭环与乐观更新,最后给幂等与渐进增强边界。

#
★★★

9. React 19 useOptimistic 在低延迟交互体验的工程价值

React 19 的 useOptimistic 在低延迟交互体验中的工程价值是什么?如何正确使用?

  • 乐观更新的即时反馈机制
  • 与 pending/真实状态的收敛
  • 应用场景与边界

useOptimistic 的工程价值:把"网络往返的等待"从用户体验中移除——交互(点赞、收藏、发送、数量调整)发生时立即展示预期结果(乐观值),服务端确认后收敛为真实结果,用户感知延迟趋近于零;与手动"setState 乐观值 + 失败回滚"不同,useOptimistic 是声明式的:乐观值只在 pending(Transition/Action 进行中)期间生效,pending 结束自动回落到真实 state(以服务端为准),失败时无需手写回滚逻辑(真实 state 未变,乐观值自动消失)。配合 useTransition(startTransition 包裹 action 调用)获得 isPending,用于"提交中"的轻量提示(微妙的 disabled 或 spinner,而非阻塞交互)。

正确使用:useOptimistic(state, updateFn) 的 updateFn 基于当前乐观值计算新值(连续操作需累加:点赞两次、购物车连加);乐观值参与渲染(按钮状态、计数、条目插入位置);action 返回权威结果后真实 state 更新(useActionState 或 setState),乐观值收敛;失败时显示错误提示(真实 state 未变 + 错误信息);场景选择——适合"可最终一致、失败可恢复"的交互(社交、购物车、消息发送);不适合强一致场景(支付、库存扣减、权限变更)——乐观值可能误导用户(应使用明确 pending 态)。边界:乐观值只在 Transition 内生效(未用 Transition 的更新不会展示乐观态);依赖"乐观值 + 真实值"双份数据源的 UI 要小心一致(以乐观值为渲染源);StrictMode 下 updateFn 双调用需幂等。

答题先讲"即时反馈 + pending 自动回落"的声明式机制与零延迟价值,再讲连续累加、收敛与失败提示的使用要点,最后给"可最终一致场景适用、强一致场景慎用"的边界。

const [optCount, addLike] = useOptimistic(count, (c, d) => c + d);
startTransition(async () => { addLike(1); await likeAction(); });
#
★★★

10. React 19 Suspense + RSC 流式 SSR 在 TTFB/FCP 的工程价值与现代框架支持

React 19 的 Suspense + RSC 流式 SSR 在 TTFB/FCP 上的工程价值是什么?现代框架如何支持?

  • 流式 SSR 对 TTFB/FCP 的改善机制
  • RSC 与流式的双流协作
  • 现代框架(Next.js 等)的支持

流式 SSR 的工程价值:传统 SSR 必须等整棵树(含最慢数据)渲染完才发送,TTFB 被最慢数据拖长;Suspense 流式让"壳(shell)+ 快数据"先行输出——浏览器在数据未齐时已收到可显示的 HTML(TTFB 提前、FCP 提前:首屏骨架/关键内容先呈现),慢数据(评论、推荐)以增量 chunk 后补,用户感知加载显著变快。RSC 与流式的协作:同一渲染管线输出两类流——HTML 流(SSR 呈现)与 Flight payload 流(RSC 组件树数据,客户端水合与更新用),Suspense 边界同时是两条流的暂停-恢复单元(边界挂起 → 两流都先跳过该边界 → 就绪后 HTML 与 payload 增量补发),实现"服务端渲染内容与客户端可更新数据"的同步渐进呈现。

现代框架支持:Next.js App Router 默认流式(页面 async 组件 + Suspense 边界即流式单元,loading.tsx 生成 fallback),RSC 数据获取与服务端渲染一体;Remix 的 deferred + 流式;React Router 框架模式的 deferred 流式;框架还提供——streaming 的缓存与 ISR 协作(流式响应缓存策略:部分缓存 shell、动态边界实时)、PPR(Partial Prerendering:静态 shell + 动态边界流式)把"流式 + 静态优化"结合;工程注意:TTFB 收益的度量(shell 时间 vs 全部时间)、流式与 CDN 缓存(动态流难缓存,静态部分用 PPR)、水合时间(HTML 先到但交互等待水合,Selective Hydration 缓解)。价值总结:TTFB/FCP 的感知提速 + 数据新鲜度 + 渐进增强,是 React 19 服务端体验的核心竞争力。

答题先讲流式对 TTFB/FCP 的机制(shell 先行、增量补发、感知提速),再讲 RSC 双流(HTML + Flight)的边界同步暂停恢复,最后给 Next.js/Remix 支持与 PPR/缓存协作的边界。

#
★★★

11. Next.js App Router 的 PPR(Partial Prerendering)

Next.js App Router 的 PPR(Partial Prerendering)是什么?它的工作原理与工程价值是什么?

  • 静态外壳 + 动态边界的混合渲染
  • PPR 与流式 SSR/缓存的协作
  • 工程价值与适用边界

PPR(Partial Prerendering)是 Next.js App Router 的渲染优化:把页面拆为"静态部分"与"动态部分"——构建时预渲染静态外壳(shell:导航、布局、静态内容,生成静态 HTML 与缓存友好的产物),动态部分(依赖 cookies/headers/个性化数据/Suspense 边界内的动态内容)保留为"动态孔位";请求时:静态外壳直接由 CDN/边缘缓存响应(零计算、TTFB 极快),动态孔位以流式 SSR 实时渲染补发(含数据获取与个性化)。本质是"预渲染与流式的混合":把"整页静态化"与"整页动态渲染"的二分,细化为"按边界混合"。

工作原理:静态边界(无动态 API 依赖的组件树)在构建期执行并缓存(产物含静态 HTML + 动态边界的占位);动态边界在运行时流式渲染(与普通流式 SSR 相同管线);检测规则:读取 dynamic API(cookies/headers/searchParams 动态使用)或显式 Suspense 边界的子树为动态;通过 PPR 配置(experimental.ppr)与"静态优先"默认开启。工程价值:静态部分获得 CDN 缓存与零延迟(TTFB 接近静态站)、动态部分保持个性化与实时性(数据新鲜度),兼顾"性能与动态";成本:构建时间增加、动态边界过多时收益递减(退化为流式 SSR);适用:内容站/营销页 + 个性化区块(推荐、用户信息)混合的页面;边界:PPR 只作用于"可静态化"的部分(完全动态的应用收益小)、动态边界内的数据仍受请求时网络影响、与 ISR 的 revalidation 配合管理静态缓存的新鲜度。

答题先讲 PPR 的"静态外壳 + 动态孔位"混合模型(构建期预渲染 + 请求时流式补发),再讲动态边界判定与缓存协作,最后给价值(TTFB/缓存)与适用边界(动态占比)。

#
★★★

12. Suspense 在 SSR 下的 fallback 行为

Suspense 在 SSR 下的 fallback 行为是什么?与客户端渲染有什么差异?

  • SSR 中挂起边界的流式处理
  • fallback 的 HTML 输出与替换
  • 与 CSR 的差异(时序、水合)

SSR 下 Suspense 边界的 fallback 行为:服务端渲染到挂起的边界时,不阻塞整体——先把 fallback 渲染为 HTML 随 shell 输出(fallback 占位内容进入首屏 HTML),边界数据就绪后渲染真实内容以流式 chunk 补发(客户端用补发内容替换 fallback)。差异与 CSR 对比:CSR 中挂起发生在客户端渲染阶段(fallback 显示、数据就绪后替换,无 HTML 增量);SSR 中挂起发生在服务端流式输出阶段(fallback 进首屏 HTML、真实内容后补,替换发生在"流式接收时"或水合前);关键差异——SSR 的 fallback 会出现在初始 HTML(无 JS 用户看到 fallback 而非空白,SEO 可见占位),CSR 的 fallback 在 JS 执行后才渲染;水合:客户端水合时"流式补发的内容"已就位的边界直接水合(无 fallback 闪现),未完成的边界保持 fallback 直至补发到达(配合 Selective Hydration 按需水合)。

工程要点:fallback 的选择影响感知体验——SSR 首屏中 fallback 是"真实可见内容"(骨架屏优于 spinner,避免布局抖动);流式补发后 fallback 替换可能引起布局偏移(用固定尺寸/占位样式);边界错误:SSR 中边界渲染错误由 errorElement 处理或服务端记录(不影响 shell);"降级为同步"场景:关键边界可配置等数据(不流式)以保证首屏完整性(如 SEO 关键内容);流式补发的 chunk 含边界标记,客户端替换逻辑由 React 处理(开发者无需手动);测量:shell 时间(fallback 可见时间)与全量完成时间是流式调优的两个指标。React 19 中 fallback 在流式与 RSC payload 流同步处理。

答题先讲 SSR 下"fallback 进 shell + 真实内容流式替换"的行为,再对比 CSR 的时序差异(HTML 可见性、水合时序),最后给 fallback 设计(骨架屏、防抖动)与降级选择的工程要点。

#
★★★

13. Edge Runtime(Next.js Middleware)

Next.js 的 Edge Runtime 与 Middleware 是什么?它们的工程应用与边界是什么?

  • Edge Runtime 的运行时约束
  • Middleware 的执行时机与用途
  • 与 Node 运行时(Route Handlers)的分工

Edge Runtime 是 Next.js 的轻量运行时(基于 V8 isolate、边缘网络分发):在 CDN 边缘节点执行,接近用户、冷启动极快(毫秒级),但约束严格——仅支持标准 Web API(fetch、Request/Response、crypto)、无 Node 原生模块(fs、path、process 环境变量受限)、代码体积限制(约 1MB,独立打包)。Middleware(middleware.ts,旧名 middleware)运行在 Edge Runtime,在请求到达路由前执行:重写/重定向、鉴权/会话校验、A/B 测试、地域与设备检测、请求头注入、响应头(安全头、缓存控制)处理——适合"请求前拦截"类逻辑。

工程分工与边界:Middleware 做"边缘决策"(鉴权、重定向、头处理——在边缘完成避免回源);复杂业务逻辑(数据库、文件、长任务)用 Node 运行时(Route Handlers/Server Components 默认 Node runtime,可配 runtime = 'nodejs');数据访问:Middleware 不能直接连数据库(无 Node 驱动)——鉴权后可把用户信息放入 request header/rewrite 传给后续处理;体积限制:Middleware 包应小而纯(避免引入大依赖);性能注意:Middleware 每次请求都执行(边缘成本与延迟),只放必要逻辑;与缓存协作:Middleware 可设置 CDN 缓存头(配合 ISR/PPR 的分层缓存);安全:边缘校验(token 快速验证)与 Node 层的深度校验(权限数据)分层;选型判断:"能否用标准 Web API 实现 + 是否需近用户执行"决定 Edge 与否。React 19/Next.js 16 中 Middleware 仍为 Edge 优先,但可配 nodejs 运行时(权衡冷启动)。

答题先讲 Edge Runtime 的约束(Web API 子集、无 Node 模块、体积限制)与 Middleware 的拦截能力(鉴权重定向、头部处理),再讲与 Node 运行时的分工(边缘决策 vs 数据逻辑)与体积/性能边界。

#
★★★

14. react-dom/static 的 prerender/prerenderToNodeStream 与 renderToPipeableStream 的差异及其在 SSG 的应用

react-dom/static 的 prerender/prerenderToNodeStream 与 renderToPipeableStream 有什么差异?在 SSG 中的应用是什么?

  • 静态预渲染 API 的形态与输出
  • 与流式渲染的差异(同步、无暂停恢复)
  • SSG 场景的工程应用

react-dom/static 提供静态预渲染 API:prerender(返回 promise,产物为 { html, rscPayload } 等)与 prerenderToNodeStream(Node 流形态,更老);renderToPipeableStream 是运行时流式渲染:实时渲染、Suspense 边界可暂停-恢复(边渲染边输出、等待数据)、用于"每请求动态渲染"。差异核心:静态 API 是"一次性渲染完整产物"——预渲染等待所有 Suspense 边界 resolve 后输出完整 HTML(不流式补发、不暂停),适合"数据在构建期可确定"的内容;流式 API 面向"请求时数据不确定"的动态场景(个性化、实时数据);静态产物可缓存复用(构建期生成 → 部署/CDN 分发),流式每次请求计算。

SSG 应用:构建阶段用 prerender 生成页面的完整 HTML(静态文件部署,访问零计算);工程配合——预渲染时的数据获取(构建期 fetch/静态数据)与动态数据分离:静态部分 prerender、动态部分预留边界用客户端获取(或配合 ISR 的 revalidate 重新预渲染);prerender 输出的 RSC payload 一并静态化(客户端水合与导航数据可复用);SSG 的边界:预渲染内容陈旧(需 ISR/revalidate 刷新)、个性化内容无法预渲染(动态边界)、构建时长随页面数增长(规模化考虑按需生成);与 renderToPipeableStream 选型:内容确定性 + 性能优先 → prerender(SSG);请求时个性化 → 流式 SSR;混合 → PPR(静态外壳 + 动态流式)。React 19 中 prerender 支持静态 RSC 输出(Flight payload 一并生成),Next.js 的 generateStaticParams/ISR 底层使用该能力。

答题先对比"一次性完整输出(prerender)vs 可暂停恢复的流式(renderToPipeableStream)",再讲 SSG 的应用(构建期生成 + 缓存分发 + RSC payload 静态化),最后给陈旧性/个性化边界与选型矩阵。

#
★★★

15. Hydration Mismatch 的常见原因与排查

Hydration Mismatch(水合不匹配)的常见原因是什么?如何排查与修复?

  • 水合不匹配的定义与检测
  • 常见原因(时间、随机、环境差异)
  • 排查工具与修复策略

Hydration Mismatch 指客户端水合时 React 生成的虚拟 DOM 结构与服务端输出的 HTML 不一致(水合假定"DOM 与服务端渲染结果一致",不一致时 React 重建该子树并警告,导致性能损失与状态错乱)。常见原因:渲染结果依赖"环境差异"的值——时间/日期(new Date() 在服务端与客户端执行时刻不同)、随机数(Math.random/id 生成器)、window/document 读取(服务端无浏览器对象返回默认值)、locale/时区差异、浏览器扩展/第三方脚本注入 DOM、条件渲染基于"首次访问 vs 已访问"(localStorage 读取)、以及 CSS-in-JS 服务端收集与客户端运行时注入的时序差异;还有服务端与客户端"代码版本不一致"(部署中间态)与 React 版本差异。

排查与修复:DevTools 控制台警告定位"不匹配的节点与属性"(React 19 增强的错误叠加层:高亮差异、展示 server/client 树);修复策略——"延迟到客户端":把依赖环境的渲染放到 useEffect 后的 state(挂载后更新,服务端输出稳定内容)或 useSyncExternalStore 的 getServerSnapshot 返回稳定默认值;"消除随机":id 用 useId(两端一致)、时间显示固定到秒/服务端时间;"禁用差异渲染":SuppressHydrationWarning(仅对确实无害的差异,如时间戳,谨慎使用);统一环境:时区/语言在服务端与客户端配置一致;排查顺序:先看警告的节点 → 检查该节点的数据来源(环境敏感?)→ 用"服务端 HTML 与客户端渲染对比"(SSR 调试/浏览器查看源码)定位。工程上"渲染纯度"(不依赖环境)是根治原则。

答题先讲不匹配的定义与后果(重建子树、警告),再列四类常见原因(时间/随机、浏览器 API、扩展注入、环境差异),最后给"延迟客户端、useId、SuppressHydrationWarning"的修复与排查流程。

#
★★

16. useFormStatus 等 Hook 在 SSR 边界的行为

useFormStatus 等 Hook 在 SSR 边界的行为是什么?SSR 兼容的注意点有哪些?

  • 表单类 Hook 在服务端的初始渲染
  • 状态序列化与水合一致性
  • SSR 安全的使用模式

useFormStatus/useActionState 等表单 Hook 在 SSR 中的行为:服务端渲染时不存在"表单提交进行中"(pending 恒为 false)——useFormStatus 返回默认值(pending: false、data: null),useActionState 返回初始 state(initialState 由服务端渲染携带输出);服务端输出"初始状态"的 UI(按钮非禁用、错误为空),水合后客户端接管交互,提交时状态才开始变化。SSR 边界注意点:初始 state 的序列化一致性——useActionState 的 initialState 若是"派生值"(如服务端数据),两端必须相同(避免水合不匹配);不能把"客户端提交状态"在服务端渲染出来(服务端无 pending 概念);useFormStatus 读取的 form 状态在 SSR 时不存在(服务端不执行 action),组件需按"默认值"渲染且与客户端首帧一致。

SSR 安全的使用模式:表单组件渲染不依赖"提交中状态"(初始渲染用初始值);使用 useActionState 时 initialState 显式传给服务端可序列化的值;错误信息由服务端渲染注入(RSC 首帧携带)而非客户端补;"服务端初始校验"(如服务端渲染时校验并回填错误)需保证与客户端水合状态一致(错误结构序列化);避免在渲染期读取 window/navigator(SSR 无)——表单相关浏览器能力(focus、剪贴板)延迟到 effect;测试:SSR 测试验证服务端输出的 HTML 包含初始表单状态(不包含 pending 态逻辑);渐进增强:无 JS 时服务端渲染的初始状态就是最终状态(原生提交后整页刷新)。整体原则:"服务端输出初始态、客户端拥有动态态",边界处保证序列化一致。

答题先讲 SSR 下表单 Hook 的行为(pending 恒 false、输出初始 state),再讲初始 state 序列化一致性与"不渲染提交中状态"的注意点,最后给 SSR 安全使用模式与测试验证。

#
★★

17. React 19 的 useFormStatus 在 Server Action 待处理状态的工程价值

React 19 的 useFormStatus 在 Server Action 待处理(pending)状态的工程价值是什么?如何应用?

  • pending 状态的就近读取
  • 提交按钮的自动反馈
  • 与 Server Action 的协作边界

useFormStatus 的工程价值集中在"提交中状态的就近反馈":Server Action 提交期间(网络往返 + 服务端执行),表单内部子组件通过 useFormStatus 直接读取 pending(无需把 isPending 当 props 层层传递),实现——提交按钮自动显示"提交中"文案、自动 disabled 防重复提交(用户双击、回车重复触发)、表单区域显示进度指示(骨架、遮罩)、按 pending 切换"提交/保存中"等语义文案。与 useActionState 的分工:useActionState 的 isPending 面向"发起提交的组件"(提交方),useFormStatus 面向"表单内部的任何子组件"(反馈方)——两者在 React 19 中都基于 Server Action/form action 的待处理状态,一内一外互补。

工程应用:提交按钮组件(独立于表单主体的子组件)用 useFormStatus 渲染 pending 态,避免表单主体因 pending 变化重渲染(状态读取精确化);多提交按钮(保存 / 保存并发布)共享同一 pending(任一提交进行中均反馈);配合 useOptimistic:pending 期间展示乐观值、结束后收敛(useFormStatus 提供"什么时候结束"的 UI 信号);错误/完成反馈用 useActionState 的结果 state;边界:useFormStatus 只能读"最近的 form 祖先"的 action 状态(跨表单无效);它不提供"提交结果"(结果用 useActionState);无 action 的普通表单返回默认值;SSR 下 pending 恒 false(服务端无提交进行中,初始渲染输出默认值);性能:useFormStatus 的变化只重渲染"读取它的组件"(细粒度)。实践:把"提交按钮 + pending 反馈"封装为通用组件(SubmitButton),全站复用统一反馈体验。

答题先讲"pending 就近读取、免 props 传递"的价值与按钮自动反馈(禁用、文案、防重复),再讲与 useActionState 的内外分工与多按钮共享,最后给"最近 form 边界"与细粒度渲染的边界。

#
★★

18. 流式 SSR 与 CDN/ISR 缓存策略的协作(Cache-Control 设置、revalidate 时机与流式响应的可缓存性)

流式 SSR 与 CDN/ISR 缓存策略如何协作?Cache-Control 设置、revalidate 时机与流式响应的可缓存性如何设计?

  • 流式响应的可缓存性判定
  • Cache-Control 的分层设置
  • ISR/revalidate 与流式的配合

流式响应的可缓存性:普通流式 SSR 响应(动态 shell + 增量补发)默认不可缓存(CDN 无法缓存"尚未完成"的流,且内容可能个性化);可缓存的流式形态——PPR 的静态外壳(构建期完整 HTML,可整页缓存)与 ISR 预渲染的产物;因此"流式 + 缓存"的协作原则是"分层":静态部分(外壳、静态内容)走 CDN 缓存(Cache-Control: public, s-maxage),动态边界不缓存(private/no-store 或短 s-maxage 配合 CDN 请求回源)。

Cache-Control 设置:静态/ISR 页面——public, s-maxage=<revalidate 秒>, stale-while-revalidate(CDN 边缘缓存 + 后台刷新);动态页面——private, no-store(或 s-maxage=0)防止 CDN 缓存个性化内容;流式响应中途发头:响应头在 shell 输出前确定(Cache-Control 由页面配置决定,流式过程不改变头);revalidate 时机:ISR 的 revalidate(时间型)在 CDN/服务器层按 s-maxage 过期后回源重渲染(流式渲染仅发生在回源时);revalidateTag/revalidatePath(事件型)主动清除 CDN 缓存(Next.js 的 revalidate 机制支持 CDN 缓存失效);配合流式:ISR 重渲染用流式输出(动态边界补发),静态部分仍缓存;注意 stale-while-respond(SWR 的 stale 响应流式补发)的边界支持。

工程实践:先分类页面——"内容确定性"(文章、产品页)用 ISR/静态 + CDN 缓存 + 流式重渲染;"个性化/实时"(仪表盘、会话数据)用动态流式 + 私有缓存(浏览器缓存 private);用 Vary 头处理"按 Cookie/地域变化的缓存"(避免缓存串用户);监控缓存命中率与回源延迟(流式回源的 TTFB 与缓存命中的差异是缓存价值的度量);安全:敏感数据绝不进 CDN 缓存(no-store)。核心原则:"静态外壳可缓存、动态边界不可缓存"的分层策略让流式的实时性与缓存的性能兼得。

答题先讲流式响应的可缓存性(动态流不可缓存、PPR/ISR 静态产物可缓存),再讲 Cache-Control 分层(public/s-maxage vs private/no-store)与 revalidate 时机,最后给页面分类与 Vary/监控实践。

#
★★

19. React 19 hydration mismatch 的 DevTools 诊断(错误叠加层、server/client 差异提示)与常见根因归类

React 19 的 DevTools 如何诊断 hydration mismatch?错误叠加层与差异提示是什么?常见根因如何归类?

  • React 19 的 mismatch 诊断增强
  • 错误叠加层与差异高亮
  • 根因归类与修复流程

React 19 增强了水合不匹配的诊断:控制台警告附带"发生不匹配的节点位置 + 属性差异",DevTools 提供"错误叠加层"(overlay)——在不匹配的 DOM 元素上高亮标记、展示服务端与客户端各自渲染的树结构差异(节点类型、属性、文本内容逐项对比),开发者可直接点击差异节点定位问题,无需在代码里猜。诊断输出包含:不匹配的节点(元素/文本/属性)、服务端 HTML 片段与客户端预期片段、相关组件栈——三者结合快速定位"哪一层组件、哪个属性/内容"不一致。

常见根因归类:环境敏感值(时间、随机数、ID、URL 搜索参数);浏览器 API 依赖(window/localStorage/matchMedia 在服务端返回默认值);客户端注入(浏览器扩展修改 DOM、第三方脚本插入节点);序列化差异(RSC payload 与 HTML 的双通道不一致、表单初始状态序列化两端不同);代码/版本差异(部署中间态、React 版本不一致);样式库时序(CSS-in-JS 服务端收集与客户端注入顺序);修复流程:按"警告 → 叠加层定位节点 → 判断数据来源(环境敏感/客户端注入/序列化)→ 应用修复(延迟客户端渲染、useId 稳定 ID、稳定默认值、统一环境配置、版本对齐)"执行;对"确实无害"的差异(如实时时钟)用 suppressHydrationWarning 显式豁免(记录原因);根治原则:渲染纯函数(输出不依赖环境)。测试:CI 中把 mismatch 警告升级为错误(全局捕获),防止回归。

答题先讲 React 19 的诊断能力(叠加层高亮、server/client 树对比、组件栈),再按"环境敏感/浏览器 API/注入/序列化/版本"归类根因,最后给"定位-归因-修复"流程与警告升级实践。

#
★★

20. React 19 的 Server Actions 在跨域 CSRF 防护的工程实践

React 19 的 Server Actions 在跨域场景下的 CSRF 防护工程实践是什么?

  • 跨域请求的 CSRF 暴露面
  • Origin/Referer 校验与 token 方案
  • 多域部署的防护配置

跨域场景下 Server Actions 的 CSRF 暴露面更大:攻击者可从不相关的第三方域构造 POST 请求(表单提交)触发 action,浏览器自动携带 Cookie(SameSite 之外的场景);防护的第一原则是"来源校验"——校验 Origin 头(跨域请求必带 Origin)与 Referer(旧客户端),白名单校验"请求来源是否为本应用域集合";同域请求(部分浏览器不带 Origin 或同源 Origin)配合 Host 校验(Host 必须在合法域列表)兜底;跨域合法场景(前端域名与 API 域分离、多自定义域、预览域)需在白名单中显式配置(环境变量注入),避免"合法跨域被误杀"。

进一步防护:SameSite Cookie(Lax/Strict)阻止跨站携带凭证(Server Action 依赖 Cookie 认证时,Lax 对跨站 POST 不携带——认证失效即拒绝,注意对"无 Cookie 纯 token"方案无效);CSRF token:服务端签发、表单/请求携带、action 校验(对"非浏览器客户端"也有效);Fetch Metadata(Sec-Fetch-Site: cross-site/none 拒绝)作为纵深防御;敏感 action(改密、支付)附加"二次验证"(最近登录确认、OTP);幂等键防重放(跨域攻击的重放变体)。工程实践:中间件/封装层统一执行"Origin + Host + token"校验(非每 action 手写);错误响应不泄露内部信息(统一 403/CSRF 页面);对"合法无 Origin 请求"(curl、服务端调用、隐私模式)按"Host 白名单 + 额外凭证(自定义头/token)"降级放行(绝不"缺失即放行");多域部署:白名单随部署环境管理(env 配置 + 发布流程校验),定期审计端点暴露面。整体:"来源校验为基、凭证策略加固、token/元数据纵深"。

答题先讲跨域暴露面与"来源校验"基线(Origin/Referer/Host 白名单),再讲 SameSite/token/Fetch Metadata 的加固与幂等防重放,最后给统一封装、缺失 Origin 降级与多域配置的实践。

#
★★

21. React 19 的 RSC Payload 体积优化与缓存策略

React 19 的 RSC Payload 体积如何优化?缓存策略如何设计?

  • Payload 体积的组成与压缩
  • 去重与传输优化
  • 缓存分层与失效

RSC Payload(Flight 数据)体积优化:减少"重复数据"——同一实体被多个组件/边界重复序列化时用服务端缓存共享(cache() 请求内去重)与引用传递(传 id 而非对象);裁剪"过深边界"(边界越深、传输给客户端的服务端产物越多,静态内容留在服务端);序列化格式本身是紧凑的 JSON 形态(Flight 二进制/JSON 流),开启压缩(gzip/brotli,Streaming 下对 chunk 有效);避免把"客户端也能生成的静态结构"跨边界传输(用 children 插槽而非 props 传 JSX 描述);服务端返回数据的字段裁剪(select 需要的字段,勿整表序列化);RSC Payload 与 HTML 的重复(内容两处出现)用"静态部分不进 payload"治理(客户端只水合动态交互部分)。

缓存策略:分层——请求内缓存(cache():同请求去重)、跨请求缓存(use cache:数据/组件输出缓存,按参数键)、HTTP 缓存(ISR/静态产物:整页缓存含 payload)、客户端缓存(Flight payload 在客户端导航缓存——Next.js router cache 缓存已访问路由的 payload,返回时零请求);失效——时间型(use cache 的 revalidate 秒数)与事件型(revalidateTag/revalidatePath 按 tag 精准失效,写操作后失效相关数据);动态数据不缓存(个性化/实时);策略组合:"静态内容长缓存(tag 失效)+ 动态数据请求内去重 + 客户端导航缓存",体积与新鲜度双目标;度量:构建产物与运行时对比(payload 字节数、重复率),设定体积预算;边界:缓存键基于参数序列化(参数不稳定导致碎片化)、缓存的数据需可序列化、跨请求缓存的一致性由失效机制保证。

答题先讲体积优化的四类手段(去重、边界裁剪、压缩、字段裁剪),再讲四层缓存(请求内/跨请求/HTTP/客户端)与时间/事件失效的组合,最后给预算度量与缓存键边界。

#
★★

22. React 19 的 streaming HTML 在慢网络下的用户体验取舍

React 19 的 streaming HTML 在慢网络下有什么用户体验取舍?如何优化?

  • 流式的渐进呈现收益
  • 慢网络下的权衡(首屏 vs 完整)
  • 优化手段与降级

慢网络下 streaming HTML 的收益:shell 与关键内容先到达(TTFB 早、首屏可读),慢数据边界后补——用户"感知加载"被压缩到"关键内容"而非"全部内容";弱网下传统 SSR"等全部数据"的首屏延迟被流式消除。取舍:流式补发依赖"多个网络往返/chunk 到达"——慢网络下增量 chunk 的到达时间被带宽拉伸(补发慢)、流式连接的中断(弱网掉线)导致后续内容丢失(需刷新重试);浏览器对"流式 + 渐进水合"的呈现依赖支持(现代浏览器 OK);shell 先行也意味着"首帧可见但不完整"(内容逐步填充,可能出现布局移动——用骨架占位)。

优化与降级:优先级编排——关键边界(LCP 内容)不流式(同步渲染进 shell),次要边界流式(评论、推荐),避免"首屏等最慢 chunk";预取与缓存——慢数据提前预取(服务端缓存/客户端预取)、ISR 静态化高频内容;传输优化——压缩(brotli)、HTTP/2 并发 chunk、减少往返(合并补发);断线恢复——客户端重连/重试策略、渐进增强(无流式支持环境退化为完整 HTML);度量:以"关键内容时间(LCP 可交互前)"而非"全量完成时间"评估体验;离线/弱网测试(Network throttling)纳入 CI;对"慢网络用户"降级策略——服务端渲染更少动态边界(更多静态内容)或客户端渲染兜底(缓存优先)。核心取舍:"关键内容确定性优先、次要内容渐进补发",把慢网络的等待时间花在用户最需要的内容上。

答题先讲流式在慢网络的收益(关键内容先到、感知压缩)与代价(补发慢、断线、布局移动),再讲优先级编排(关键边界同步、次要流式)与预取压缩优化,最后给断线降级与度量建议。

#
★★

23. React 19 的 Server Components 的数据获取(无需 useEffect)

React 19 的 Server Components 如何实现"无需 useEffect"的数据获取?价值与边界是什么?

  • 服务端组件内直接 await 数据
  • 消除客户端加载态编排
  • 数据获取位置与边界

Server Components 可以 async:在组件函数体内直接 await fetchData()(或读取数据库/文件),渲染结果包含数据——数据获取发生在服务端渲染阶段,无需 useEffect + state + loading 三件套;配合 Suspense 边界实现"挂起等待":await 的数据未就绪时边界挂起(服务端等待或流式补发),就绪后渲染内容——"加载态"由 Suspense fallback 表达(服务端流式场景下 fallback 进 shell、内容后补)。价值:数据获取代码与渲染同处(无"数据在哪里加载"的分散);客户端 bundle 不包含获取逻辑;无"客户端请求 → loading → 数据"的瀑布与闪烁;SSR 首屏直接携带数据(SEO、无 JS 可用)。

边界:Server Components 只能在服务端获取(客户端组件仍需 useEffect 或数据获取库——除非把获取逻辑放进服务端边界并传结果);async 组件不能传给客户端(跨边界用 props/children 传递已获取的数据);数据获取的缓存与失效(fetch 缓存、use cache、revalidate)需要显式设计(服务端获取不等于自动缓存);并发请求聚合(多个 await 串行 → 用 Promise.all 并行);错误处理(await 抛错 → 最近的错误边界);"交互后获取"(用户操作触发的数据更新)仍需 Server Actions/客户端获取(RSC 的获取是"渲染期"的);调试:服务端获取的日志与跟踪(服务端日志链路)。现代实践:"渲染期服务端获取(RSC)+ 交互期 Server Actions/客户端库(Mutation/变更后刷新)"分工,前者消除加载样板、后者处理动态交互。

答题先讲"async Server Component 直接 await"的机制与 Suspense 挂起协作,再讲消除加载三件套与首屏携带数据的价值,最后给客户端边界、缓存失效与交互后获取的边界。

#
★★

24. Vitest Browser Mode 与 Playwright component testing 在真实浏览器组件测试上的取舍

Vitest Browser Mode 与 Playwright component testing 在真实浏览器组件测试上如何取舍?

  • 两种方案的能力对比
  • 执行模型与集成差异
  • 选型建议

Vitest Browser Mode:在 Vitest 内启动真实浏览器(默认 Playwright 驱动 Chromium)运行测试——测试代码仍是"单元/组件测试"形态(RTL、vi.mock、fake timers 全部可用),只是执行环境从 jsdom 换成真实浏览器(完整 DOM、真实布局、真实事件);集成在现有 Vitest 配置(test.browser 配置 + 启动浏览器),与 jsdom 测试共享 API 与依赖。Playwright component testing:Playwright 的组件测试模式——用 Playwright 的测试运行器 + 组件挂载 API(mount),测试以"浏览器自动化"视角执行(page 对象、真实交互、截图、网络拦截),测试文件即"浏览器脚本"。

取舍:执行模型——Vitest Browser Mode 是"单测环境升级"(同文件内并发、模块级 mock 语义、与 jsdom 测试无缝切换);Playwright component 是"E2E 视角下钻"(page 级交互、真实导航、多标签/多浏览器、截图对比);生态集成——Vitest 系(RTL + jest-dom + user-event + MSW Node)在 Browser Mode 直接可用;Playwright 有自身的交互/断言/夹具体系(与 E2E 测试同栈);调试——Playwright 的 trace/view 调试更强;并行与速度——Vitest Browser Mode 与 Playwright 都按 worker 并发,Vitest 单测心智更轻;选型:已有 Vitest 单测体系、希望"jsdom → 真实浏览器"平滑升级 → Browser Mode(测试代码改动小);已有 Playwright E2E 体系、希望"组件层到 E2E 同栈" → component testing;混合实践:jsdom(逻辑单测)+ Browser Mode(真实行为精选)+ E2E(关键路径)分层,或"Browser Mode + Playwright E2E"(组件行为归 Vitest、端到端归 Playwright)。边界:Browser Mode 的浏览器版本管理、与 jsdom 的行为差异(测试需按环境调整);Playwright component 的 mock 模型(网络拦截为主)与模块 mock 不如 Vitest 直接。

答题先对比两种方案的本质(单测环境升级 vs E2E 视角下钻)与执行模型,再比集成/调试/速度的差异,最后按"已有栈"给选型与分层实践建议。

#
★★

25. 自定义 Hook 的测试(renderHook)与状态更新断言的边界

如何用 renderHook 测试自定义 Hook?状态更新断言的边界是什么?

  • renderHook 的使用模式
  • 状态更新的断言时机
  • 异步与 effect 的测试边界

renderHook(() => useCounter(0)) 返回 { result, rerender, unmount }:result.current 读取 Hook 当前返回值;触发状态更新(调用返回的 setter/action)后,在 act 中包裹再断言(act 确保更新被 flush:渲染与 effect 完成);rerender(props) 验证依赖变化行为(传入新参数看返回值/effect 是否更新);unmount() 触发清理逻辑(断言订阅取消、清理调用)。模式:同步断言——act(() => result.current.increment()) 后断言 result.current.count;异步断言——异步 Hook(内部 setTimeout/请求)用 waitFor 轮询(await waitFor(() => expect(result.current.data).toBe(...)))或 fake timers(vi.useFakeTimers + advanceTimersByTime 推进时间,配合 act)。

状态更新断言的边界:断言"最终状态"而非"中间状态"(并发/批处理下中间态不确定);effect 中的更新需 waitFor(effect 在 act 后异步执行,React 19 中 effect 的 flush 依赖 act/waitFor);Transition 更新(startTransition 内 setState)在 act 中不保证立即完成——用 waitFor 等待收敛;"不应更新"的断言(保证某次调用不改变状态)需小心(异步回调可能晚到,用 waitFor 反向或 fake timers 控制);对外部依赖(fetch、store)的断言用 mock(vi.mock、MSW)隔离;StrictMode 双调用下 Hook 的双执行是预期(断言幂等);错误断言(Hook 抛错)用 expect(() => renderHook(...)).toThrow 或 result.error(renderHook 捕获渲染错误);边界:renderHook 不渲染真实 UI——涉及 DOM 的 Hook(测量、focus)需组件测试(render 组件 + 断言 DOM);涉及 context 的 Hook 用 wrapper 包裹 Provider。

答题先讲 renderHook 的 API 与 act 包裹的同步断言模式,再讲异步(waitFor/fake timers)与 Transition 收敛的断言时机,最后给"测值不测 UI、DOM 相关转组件测试"的边界。

#
★★

26. 快照测试在现代 React 团队中的适用边界与失效成本

快照测试在现代 React 团队中的适用边界与失效成本是什么?

  • 快照测试的原理与优势
  • 失效成本(脆弱性、假阳性)
  • 现代团队的适用建议

快照测试(toMatchSnapshot)把组件渲染输出(DOM/序列化值)与存储的快照对比,差异即失败——优势是"零成本断言"(覆盖 UI 结构、防意外回归)与快速反馈。失效成本:脆弱性——快照包含大量实现细节(类名、结构、库生成属性),非功能性变更(样式类名、日志、库升级)导致海量快照失效(噪音淹没问题);假阳性——"通过"不代表行为正确(快照只证"与上次相同"),改动快照(--ci 更新)可能掩盖真实 bug;维护成本——快照文件的评审负担(大 diff 难 review)、更新时的"无意识接受"风险;并发/时序内容(时间、随机 id)导致快照不稳定(需 mock 或排除)。

现代团队的适用建议:限制快照范围——只对"稳定的纯展示结构"(图标、徽章、序列化配置)快照,复杂组件用"断言式测试"(getByRole + 行为断言,测试意图而非结构);小快照(内联快照或精细 toMatchObject)代替整树大快照;对"确实需要防回归的结构"(如 JSON 序列化、配置文件)快照合适;禁用"快照当全部测试"的做法——快照是补充(回归哨兵),行为测试是主体;CI 中禁止 --ci 外更新快照(强制 review);定期清理失效噪音(快照的"变更成本"纳入迭代预算)。结论:现代 React 团队"少用整树快照、多用行为断言",快照保留给"结构性契约"(序列化、稳定 UI 形态),快照的失效成本(噪音、假阳性)超过收益的场景应移除。

答题先讲快照的原理与零成本覆盖优势,再重点讲失效成本(实现细节噪音、假阳性、维护负担、时序不稳定),最后给"行为断言为主、快照限于稳定结构"的现代适用建议。

#
★★

27. React Server Components 与 Server Actions 在测试中的 mock 与隔离策略

React Server Components 与 Server Actions 在测试中如何 mock 与隔离?策略是什么?

  • 服务端模块的测试隔离
  • Server Action 的 mock 方式
  • 边界测试与集成验证

测试 RSC/Server Actions 的策略分层:单元测试(RSC 渲染逻辑)——直接测试组件函数(服务端环境:Node 测试运行器),用 vi.mock mock 数据获取层(数据库/API 模块返回固定数据),渲染产物断言(RSC 输出结构/传出的 props);由于 RSC 在服务端执行(无浏览器 API),测试环境用 Node 即可(无需 jsdom);Server Actions 单元测试——直接调用 action 函数(它是普通异步函数),mock 其内部依赖(DB、外部 API、auth 上下文),断言"参数处理、校验逻辑、返回结构";action 的"调用协议"(Flight 序列化、action ID)由框架层测试覆盖。

隔离与集成:组件测试(客户端侧)——mock Server Action(vi.mock 导入的 action 返回固定结果),测"表单调用 action、pending/错误 UI、useActionState 回显",隔离服务端;RSC 与客户端边界的集成——用框架测试工具(Next.js 的测试辅助/renderServerComponent)或构建产物级测试(E2E 通过真实请求验证 Flight 交互);端到端——Playwright + 真实 server(MSW/真实后端),验证"客户端提交 → 服务端执行 → 结果回显"全链路(含序列化边界);隔离原则:单元层 mock"IO 边界"(DB/网络/auth),不 mock"自身逻辑"(校验、映射);Server Action 的权限/CSRF 在集成层验证(真实请求带凭证);共享数据(session、用户)用测试上下文注入;快照:RSC 输出快照(稳定结构时)辅助防回归。整体:"逻辑单测 + 组件测 mock action + E2E 验协议"三层,mock 放最外层(网络/DB),内部逻辑真实执行。

答题先讲 RSC 单元测试(Node 环境 + mock 数据层)与 Server Action 直测(mock IO 依赖),再讲客户端组件测 mock action 与 E2E 验证协议的隔离分层,最后给"mock IO 边界、不 mock 自身逻辑"的原则。

#
★★

28. waitFor/findBy 与 fake timers(vi.useFakeTimers)协作测试异步 UI 的注意事项

waitFor/findBy 与 fake timers(vi.useFakeTimers)协作测试异步 UI 有什么注意事项?

  • 异步断言与假定时器的冲突
  • 推进时间与 flush 的配合
  • 协作的常见坑与解决

冲突根源:waitFor/findBy 内部用"真实定时器"轮询(间隔重试、超时),启用 fake timers 后这些轮询计时也被"假化"——若不推进时间,waitFor 永远等不到重试(挂起直到真实超时)或立即超时。协作的正确模式:启用 fake timers(vi.useFakeTimers())时,对"基于 setTimeout/interval 的 UI 逻辑"(防抖、轮询、延迟消失)用 vi.advanceTimersByTime(ms)(或 advanceTimersToNextTimer)推进时间,配合 act 让推进触发的更新被 flush;或用 vi.runAllTimers() 跑完所有定时器(注意死循环风险);异步断言本身(等待 DOM 出现)优先用 findBy 的"自动重试"(RTL 的 findBy 使用真实的 MutationObserver/微任务重试——在 fake timers 下仍可用,因为它是"检查-重试"循环而非定时器?注意:findBy 的重试也基于 setTimeout(waitFor 内部),fake timers 下同样需要推进或采用"真实定时器 + 假定时器隔离")。

注意事项与技巧:区分"被测逻辑的定时器"与"测试工具的定时器"——Vitest 支持 vi.useFakeTimers({ shouldAdvanceTime: true })(自动推进真实时间,模拟"定时器随时间自然触发")或对特定 API 选择性假化(vi.useFakeTimers({ toFake: ['setTimeout'] }));组合模式——先 advance 触发逻辑、再 await waitFor 断言(此时 waitFor 若仍被假化需配 shouldAdvanceTime);简单场景优先"真定时器 + 快速重试"(不启用 fake timers,用 waitFor 的 timeout 控制);fake timers 与"并发/微任务"(promise 链)无关(只影响宏任务定时器);清理——afterEach 恢复真实定时器(vi.useRealTimers()),避免测试间泄漏;React 19 的 act 与 fake timers:advance 后需 act 包裹(更新 flush);常见坑:"推进不足"(时间未到逻辑未触发)、"假化过度"(把请求库内部定时器也假化导致挂起——用 toFake 白名单);实践建议:fake timers 只用于"显式定时逻辑"的测试,异步 UI 断言用 findBy/waitFor(真定时器或 shouldAdvanceTime),两者边界清晰。

答题先讲冲突根源(waitFor 轮询也被假化),再讲正确协作(advance + act、shouldAdvanceTime、toFake 白名单),最后给推进不足/假化过度等常见坑与清理实践。

#
★★

29. React 19 的 useActionState(合并 useFormState)在表单提交的 pending/错误状态管理上与 useFormStatus 的分工,以及 SSR 下初始状态的序列化?

React 19 的 useActionState(合并 useFormState)与 useFormStatus 在 pending/错误状态管理上如何分工?SSR 下初始状态的序列化如何设计?

  • 两个 API 的状态管理分工
  • 错误状态的归属与传递
  • SSR 初始状态序列化

分工:useActionState(React 19 中合并了 useFormState,同一 API)管理"提交结果状态"——action 返回值成为 state(错误信息、成功数据),isPending 表示"本次提交进行中",它在"发起提交的组件"(拥有 action 的组件)中使用;useFormStatus 管理"提交过程的展示状态"——在表单内部的子组件(提交按钮、进度 UI)中读取 pending(及 data/method/action),免 props 传递。错误状态:服务端校验/业务错误由 action 返回结构化结果 → useActionState 的 state 承载(渲染错误列表);useFormStatus 不承载错误(它只提供 pending 与请求数据);"字段级错误"若由服务端返回,需映射到表单字段(RHF setError 或手动渲染);pending 的读取——提交组件用 useActionState 的 isPending,表单内子组件用 useFormStatus(两个 pending 是同一次提交的两种视角)。

SSR 初始状态序列化:useActionState 的 initialState 在服务端渲染时输出(首帧 HTML 中的表单状态:初始值、初始错误),必须"可序列化且两端一致"——initialState 应来自可序列化的数据(服务端数据、常量),避免使用"渲染期随机/浏览器值";错误初始态(服务端预校验结果)由服务端渲染携带(如 RSC 渲染时校验并传 initialState),水合后一致;无 JS 场景:服务端渲染的初始状态即"提交前状态",原生提交后整页刷新返回服务端渲染的"结果状态"(action 在服务端执行并渲染新页)——"初始状态序列化"保证两端的基线一致;注意:不要在 SSR 渲染"提交后状态"(pending/结果依赖运行时);序列化实现:Next.js/RSC 的 Flight 序列化支持 state 内的可序列化类型(对象、数组、Date 等),函数/非序列化值禁止放入 initialState;测试:SSR 测试断言初始 HTML 包含序列化后的初始状态(不含 pending 逻辑)。整体:"结果与 pending 分层(useActionState vs useFormStatus)、初始状态服务端序列化保证水合一致"。

答题先讲分工(结果状态 vs 过程展示、错误归属 useActionState),再讲 SSR 初始状态的序列化要求(可序列化、两端一致、无 JS 基线),最后给字段级错误映射与测试验证。

#
★★

30. 流式 SSR 的请求取消与超时,AbortController 在 renderToPipeableStream 的 abort 行为、客户端中断与慢连接下的降级处理?

流式 SSR 的请求取消与超时如何处理?AbortController 在 renderToPipeableStream 的 abort 行为、客户端中断与慢连接下的降级策略是什么?

  • abort 信号与流式渲染的中断
  • 客户端断开/超时的处理
  • 慢连接的降级策略

renderToPipeableStream 支持 abort 信号(options 中的 signal 或返回对象的 abort()):渲染过程中收到 abort 时——未完成的渲染工作被终止(停止继续执行组件与输出 chunk)、已输出的内容保留(客户端已收到的部分不受影响)、触发 onShellError 或相应错误回调(客户端接收流中断会触发错误处理);典型用途:服务端超时(响应时间预算到点 abort,避免慢渲染占用资源)、客户端断开(连接关闭时 abort 停止浪费的服务端计算)、降级切换(abort 后按缓存/降级内容响应)。客户端中断:浏览器取消请求(导航离开、用户刷新)时,Node 侧的请求对象触发 close/abort,服务端应监听并调用 abort(防止"无人接收的渲染继续消耗资源");流式输出中断后的客户端行为——已接收的 HTML 保留、未接收部分丢失(页面呈现"部分内容"状态,可配合重新加载/错误提示)。

慢连接降级:慢连接下流式补发被带宽拉伸(chunk 到达慢),策略——限制流式边界数量与大小(减少补发往返)、对慢数据降级(超时后放弃等待,渲染 fallback/降级内容而非无限挂起)、缓存优先(慢数据有缓存先发缓存、后台 revalidate)、压缩与 chunk 合并减少往返、设置"最大等待时间"(慢数据超时用兜底内容);超时参数:框架/自建"渲染超时"(整个流或每边界),超时 abort + 记录指标;监控:abort 原因分类(超时/客户端断开)与频率、慢连接下的 TTFB/完整时间;体验降级:超时/中断后客户端提供"重试/刷新"入口(页面级兜底);配合 CDN/代理的超时设置(网关超时需大于服务端预算)。整体:"abort 止损(超时与断连)+ 慢连接降级(少边界、有兜底、先缓存)"。

答题先讲 abort 的机制与行为(终止渲染、保留已输出、触发回调)与三类用途(超时、断连、降级),再讲客户端中断的监听与服务端止损,最后给慢连接的降级策略(边界限制、缓存优先、超时兜底)。

#

31. React 19 的 cache 函数在请求作用域缓存的工程应用

React 19 的 cache 函数在请求作用域缓存的工程应用是什么?使用边界是什么?

  • cache() 的请求作用域语义
  • 去重重复获取的工程价值
  • 缓存键与边界

React 的 cache(fn) 把函数结果缓存在"当前请求作用域"(一次渲染/一次请求内):同一请求中多次调用 cache 函数且参数相同(参数序列化相等)时,返回首次调用的结果——主要价值是"请求内去重":组件树中多个组件/边界各自调用同一数据获取函数(如 getCurrentUser()、getProduct(id))时,合并为一次执行(一次数据库/API 调用),消除 RSC 场景的重复请求与瀑布。典型应用:RSC 数据获取包装(getUser = cache(async () => db.user()))、服务端渲染中的共享数据(布局与页面都读的配置)、Server Action 中的请求级复用(同请求多次调用防重复执行)。

使用边界:作用域是"请求内"——跨请求/跨用户不共享(跨请求缓存是 use cache/HTTP 缓存);缓存键是"参数序列化"——参数需可序列化且稳定(对象参数序列化结果相同才命中,注意对象字段顺序/引用);非纯函数(依赖外部可变状态、随机)不应 cache(缓存了错误结果);与并发安全:React 19 的 cache 实现与请求上下文(AsyncLocalStorage)绑定,确保请求间不串数据;Suspense 协作:cache 的 promise 可被 use()/Suspense 消费(请求内挂起共享);请求结束缓存自动释放(无手动清理);测试:mock 需理解 cache 语义(同参数同结果)。工程实践:"数据获取函数一律包 cache"作为 RSC 基线规范(防重复请求),需要跨请求缓存时再叠加 use cache;注意 cache 与"参数解析"配合(对象参数规范化序列化)。

答题先讲 cache() 的请求作用域与去重机制(参数序列化命中),再讲 RSC/共享数据的工程价值,最后给跨请求不共享、非纯函数、参数稳定性与请求上下文隔离的边界。

#

32. React 19 的 Progressive Hydration 在低性能设备的工程取舍

React 19 的 Progressive Hydration(渐进水合)在低性能设备上有什么工程取舍?如何应用?

  • 渐进水合的机制
  • 低性能设备的收益与风险
  • 应用策略与度量

Progressive Hydration(配合 Selective Hydration 的渐进式水合):水合不再"整树一次性",而是按边界分批、按优先级(交互优先、空闲批量)进行,页面在"未完全水合"时即可交互(已水合部分)——低性能设备上收益:整树水合的长任务(主线程阻塞)被拆散到空闲期,首屏交互(用户点击的部分)优先水合,避免"长时间白屏等待全部水合完成";CPU 弱/内存小的设备上,水合长任务导致的卡顿(滚动、点击无响应)被显著缓解。代价与风险:水合分批意味着"部分交互可用、部分未水合"的过渡态(未水合的按钮点击无响应——需轻量提示/禁用态);水合状态的管理复杂度(哪些边界水合了、何时水合);多次小批量水合的调度开销(低端设备上调度本身也耗电)。

工程取舍与应用:默认开启(React 19 的并发水合是默认行为)并优化——按"交互重要性"组织 Suspense 边界(首屏关键交互的边界优先水合/提前水合:preload 相关 JS);非交互内容(静态区块、图表)延迟水合或"部分水合"(不需要交互的边界甚至可不水合——配合静态化);低性能设备识别(客户端硬件检测/网络提示)后降级:减少水合边界数量、禁用非关键水合(把静态内容真正静态化而非"水合但不交互");度量:低端设备的 TTI/INP(真实设备或 throttling 模拟)对比水合前后的交互延迟;边界:水合总要发生(渐进是编排优化,不减少总量——除非组件本身无需水合);SSR 内容必须水合才能交互(无 JS 环境交互天然不可用);测试:低性能模拟(CPU 4x 降速、弱网)纳入回归。结论:"按交互价值编排水合优先级 + 非交互内容免水合"是低端设备的关键取舍。

答题先讲渐进水合的机制(按边界分批、交互优先、空闲批量)与低端设备收益(长任务拆散、首屏可交互),再讲过渡态与调度开销的风险,最后给边界组织、静态化与降级度量的应用策略。

#

33. jest-dom 的无障碍断言(toHaveAccessibleName/toBeInvalid/toHaveFocus)的工程价值

jest-dom 的无障碍断言(toHaveAccessibleName/toBeInvalid/toHaveFocus)有什么工程价值?如何应用?

  • 无障碍断言的语义
  • 与可访问性测试的协同
  • 应用场景与边界

jest-dom 提供语义化断言:toHaveAccessibleName() 断言元素的可访问名称(来自 label、aria-label、文本内容,按可访问名称计算规则)——验证"表单控件有正确标签、图标按钮有 aria-label";toBeInvalid() 断言表单控件处于无效状态(aria-invalid 或浏览器原生校验态)——验证"校验错误已正确传达给辅助技术";toHaveFocus() 断言元素获得焦点——验证"焦点管理正确"(打开弹窗聚焦、关闭归还焦点、错误聚焦到首个无效字段)。工程价值:把"可访问性要求"变成可执行的测试断言——团队在组件测试中即可拦截无障碍回归(缺 label、错误态未表达、焦点丢失),无需等到人工审计或 E2E 无障碍扫描。

应用:与 RTL 的查询哲学协同——getByRole 查询(语义结构)+ 无障碍断言(语义质量)组合,测试即"无障碍契约";典型场景——表单:断言每个输入 toHaveAccessibleName(label 完整)、提交空表单后 toBeInvalid(错误表达)、校验失败 toHaveFocus 落在首个错误字段;弹窗:打开后焦点在弹窗内、关闭后归还触发按钮(toHaveFocus);图标按钮:toHaveAccessibleName(aria-label 存在);动态内容:toHaveAccessibleName 随状态更新。边界:断言的是"可访问性树语义"而非"视觉呈现"(样式/颜色对比度需人工或视觉测试);计算可访问名称需理解规则(label 优先于 placeholder、aria-labelledby 覆盖文本)——断言失败先查名称来源;复杂组件(虚拟列表、组合控件)的 ARIA 契约可用 role 查询 + 名称断言组合覆盖;与 axe-core 互补:jest-dom 断言"应用声明的语义",axe 扫描"语义与规范的偏差"(两者结合完整)。整体:"断言驱动无障碍"让可访问性进入日常测试闭环。

答题先讲三个断言的语义(名称、无效态、焦点)与"可访问性可测试化"的价值,再讲表单/弹窗/图标的典型应用场景,最后给"测语义不测视觉、与 axe 互补"的边界。

#

34. Storybook interaction testing(play 函数 + test-runner)与 RTL 组件测试的分工

Storybook interaction testing(play 函数 + test-runner)与 RTL 组件测试的分工是什么?如何选型与配合?

  • play 函数与 test-runner 的机制
  • 两种测试的定位差异
  • 分工配合的最佳实践

Storybook interaction testing:在 Storybook 中给 story 写 play 函数(组件挂载后执行交互:点击、输入、断言,基于 Testing Library 同源的 user-event/jest 断言),story 既是"文档示例"又是"交互测试用例";test-runner 在 CI 中用 Playwright 执行所有 story(真实浏览器),验证 play 中的交互与断言——测试运行在"Storybook 环境"(组件已加载、装饰器已应用),定位是"组件文档化场景的交互验证"。RTL 组件测试:在测试运行器(Vitest/Jest + jsdom/Browser)中渲染组件、模拟交互、断言行为,定位是"组件行为的单元/集成验证",与业务逻辑、mock、状态管理深度集成。

分工与差异:RTL 测试——适合"行为契约"(逻辑、状态流转、与 store/API 的交互),mock 能力强、速度快、与代码库结构绑定;Storybook 测试——适合"视觉场景 + 交互演示"(组件在真实样式/装饰器/主题下的行为、异常状态展示),play 函数同时是"可交互文档"(开发者看故事即懂交互),test-runner 用真实浏览器验证(保真度高于 jsdom)。配合模式:每个组件——RTL 覆盖核心行为(状态、事件、边界)"测试驱动开发";Storybook 覆盖"场景级交互"(完整 story 的 play:表单填写、加载态切换、错误展示)与视觉回归(配合 image snapshot 或 chromatic);重迭部分(基础交互断言)避免双写(同一交互只在一处断言,另一处轻量);流程:RTL 先行(逻辑 TDD)→ Storybook story + play(场景验证与文档)→ test-runner CI 执行 + 视觉回归;选型:团队已用 Storybook(组件库、设计系统)→ play 函数低边际成本补充交互测试;无 Storybook 或纯逻辑组件 → RTL 足够。整体:"RTL 管行为、Storybook 管场景",两层互补而非替代。

答题先讲 play 函数 + test-runner 的机制(story 内交互 + 真实浏览器 CI 执行),再对比定位差异(行为契约 vs 场景文档),最后给"RTL 先行、Storybook 场景补充、避免双写"的配合实践。