一、React (T0)
1.1 hooks原理
题目
请实现一个简化版的 useState Hook,并说明 React Hooks 的实现原理核心机制(链表、Fiber节点关联等)。
参考答案
javascript
// 简化版 useState 实现
let isMount = true; // 是否首次渲染
let workInProgressHook = null; // 当前工作的 hook 指针
let firstWorkInProgressHook = null; // 链表头指针
function useState(initialState) {
let hook;
if (isMount) {
// 首次挂载:创建 hook 节点并加入链表
hook = {
state: initialState,
next: null,
queue: [] // 存储 setState 的更新队列
};
if (!firstWorkInProgressHook) {
firstWorkInProgressHook = hook;
} else {
workInProgressHook.next = hook;
}
workInProgressHook = hook;
} else {
// 更新阶段:按顺序取出已有的 hook
hook = workInProgressHook;
workInProgressHook = workInProgressHook.next;
}
// 基础状态值
let baseState = hook.state;
// 处理更新队列
hook.queue.forEach(action => {
baseState = typeof action === 'function' ? action(baseState) : action;
});
hook.queue = [];
// 设置新状态的方法
const setState = (action) => {
hook.queue.push(action);
// 触发重新渲染的逻辑(实际React中会调度更新)
};
return [baseState, setState];
}
详细解析
React Hooks 核心原理:
-
链表结构: 每个组件内部维护一个 hooks 链表,React 通过链表按顺序记录每个 hook 的状态,确保每次渲染顺序一致。
-
Fiber 节点关联 : 组件对应的 Fiber 节点通过
memoizedState属性指向 hooks 链表的头节点,实现状态与组件的关联。 -
规则限制: hooks 只能在组件顶层调用,不能在条件语句/循环/嵌套函数中调用 ------ 因为链表是按顺序遍历的,位置必须固定。
-
更新机制 :
setState会将更新函数加入队列,React 批量处理时执行这些函数得到新状态,然后触发重新渲染。
1.2 React18并发
题目
请解释 React 18 的 并发渲染(Concurrent Rendering) 机制,以及它带来的核心变化和实际应用场景。
参考答案
并发渲染的核心概念:
-
可中断渲染: React 18 之前,渲染是不可中断的整块执行;并发模式下,渲染可以被高优先级更新打断。
-
时间切片 (Time Slicing) : 通过
scheduler包将渲染工作分成多个小任务,在浏览器空闲时间执行,避免阻塞主线程。 -
Suspense + Streaming SSR : 服务端流式渲染,支持
loading状态逐步展示内容。
关键 API:
jsx
import { createRoot } from 'react-dom/client';
import { Suspense } from 'react';
// 1. 并发根节点
const root = createRoot(document.getElementById('root'));
root.render(<App />);
// 2. Suspense 流式加载
<Suspense fallback={<Spinner />}>
<ProductDetails productId={id} /> {/* 可中断加载 */}
</Suspense>
// 3. useTransition - 标记非紧急更新
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [activeTab, setActiveTab] = useState('posts');
const handleClick = (tab) => {
startTransition(() => {
setActiveTab(tab); // 标记为可中断的低优先级更新
});
};
}
// 4. useDeferredValue - 延迟更新非关键 UI
import { useDeferredValue } from 'react';
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
// 立即响应用户输入,延迟更新搜索结果
return (
<div>
<RealTimeSuggestions query={query} />
<HeavySearchResults query={deferredQuery} />
</div>
);
}
详细解析
并发渲染解决的问题:
| 场景 | React 17 | React 18 |
|---|---|---|
| 大量列表渲染时输入框响应 | 输入被阻塞卡顿 | 输入始终流畅 |
| Tab 切换时 loading 状态 | 整页 loading | 保持旧内容,逐步显示 |
| 服务端首屏渲染 | 等待完整 HTML | 流式返回,边加载边显示 |
为什么重要: 在 AI 聊天应用(如对话式 UI)中,用户输入必须即时响应,即使后台正在渲染大量消息历史,并发渲染确保了 UI 的流畅性。
1.3 组件库设计
题目
设计一个可定制的通用 Button 组件,需要支持:
- 多种变体(primary/secondary/danger)
- 多种尺寸(small/medium/large)
- 加载状态
- 图标支持(左侧/右侧)
- 禁用状态
- 遵循开闭原则,便于扩展
参考答案
tsx
import React, { forwardRef } from 'react';
import { Spinner } from './Spinner';
import './Button.css';
// Props 类型定义
type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
type ButtonSize = 'small' | 'medium' | 'large';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant; // 变体
size?: ButtonSize; // 尺寸
loading?: boolean; // 加载状态
leftIcon?: React.ReactNode; // 左侧图标
rightIcon?: React.ReactNode; // 右侧图标
fullWidth?: boolean; // 全宽
}
// 复合组件模式 - 扩展性强
const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
children,
variant = 'primary',
size = 'medium',
loading = false,
leftIcon,
rightIcon,
fullWidth = false,
disabled,
className = '',
...props
}, ref) => {
const isDisabled = disabled || loading;
return (
<button
ref={ref}
className={`btn btn--${variant} btn--${size} ${fullWidth ? 'btn--full' : ''} ${className}`}
disabled={isDisabled}
{...props}
>
{/* 加载状态 */}
{loading && (
<span className="btn__spinner">
<Spinner size="small" />
</span>
)}
{/* 图标 + 文字 */}
{!loading && leftIcon && <span className="btn__icon btn__icon--left">{leftIcon}</span>}
<span className="btn__text">{children}</span>
{!loading && rightIcon && <span className="btn__icon btn__icon--right">{rightIcon}</span>}
</button>
);
});
Button.displayName = 'Button';
// 导出按钮组复合组件
Button.Group = ({ children, className = '' }) => (
<div className={`btn-group ${className}`}>{children}</div>
);
export { Button };
CSS 变量扩展方式:
css
/* 使用 CSS 变量便于主题定制 */
.btn {
--btn-bg: var(--color-primary);
--btn-text: var(--color-white);
--btn-padding: var(--spacing-md);
background: var(--btn-bg);
color: var(--btn-text);
padding: var(--btn-padding);
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
}
/* 变体扩展 - 新增变体无需修改组件代码 */
.btn--secondary {
--btn-bg: transparent;
--btn-text: var(--color-primary);
border: 1px solid var(--color-primary);
}
详细解析
设计要点:
-
forwardRef: 暴露 DOM 引用,便于父组件获取按钮元素做聚焦等操作。
-
复合组件模式 :
Button.Group挂在 Button 上,形成 API 家族,使用时<Button.Group><Button /></Button.Group>。 -
CSS 变量: 将颜色、间距等设计 token 抽离为 CSS 变量,支持主题定制,无需修改组件代码。
-
Props 扩展 : 使用
...props传递原生 button 属性(onClick、type、aria-* 等),保持原生语义。 -
开闭原则: 新增变体只需添加 CSS 类,不改 TSX;新增功能(如 Tooltip 包装)可通过 composition 实现。
1.4 conversation UI
题目
在 AI 对话应用(如 ChatGPT、Claude)中,设计一个支持 流式输出(Streaming) 的消息展示组件,需要考虑:
- 打字机效果(逐字显示)
- Markdown 渲染(支持代码高亮)
- 消息复制功能
- 加载中断功能
- 思考过程展示(如 Claude 的 thinking)
参考答案
tsx
import React, { useState, useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { Copy, Square, Loader } from 'lucide-react';
interface MessageProps {
role: 'user' | 'assistant';
content: string;
isStreaming?: boolean;
isThinking?: boolean;
thinkingContent?: string;
onCancel?: () => void;
}
function Message({ role, content, isStreaming, isThinking, thinkingContent, onCancel }: MessageProps) {
const [displayedContent, setDisplayedContent] = useState('');
const [copied, setCopied] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// 流式更新:当 content 变化时,逐步更新显示内容
useEffect(() => {
if (!isStreaming) {
setDisplayedContent(content);
return;
}
// 模拟打字机效果
let index = 0;
const timer = setInterval(() => {
setDisplayedContent(content.slice(0, index + 1));
index++;
if (index >= content.length) clearInterval(timer);
}, 20);
return () => clearInterval(timer);
}, [content, isStreaming]);
// 自动滚动到底部
useEffect(() => {
containerRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [displayedContent]);
const handleCopy = async () => {
await navigator.clipboard.writeText(content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className={`message message--${role}`} ref={containerRef}>
<div className="message__content">
{/* Markdown 渲染 */}
<ReactMarkdown
components={{
code({ node, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !isStreaming && match ? (
<SyntaxHighlighter language={match[1]} {...props}>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>{children}</code>
);
}
}}
>
{displayedContent}
</ReactMarkdown>
{/* 思考过程(流式输出时不显示) */}
{!isStreaming && isThinking && thinkingContent && (
<details className="message__thinking">
<summary>Thinking...</summary>
<ReactMarkdown>{thinkingContent}</ReactMarkdown>
</details>
)}
</div>
{/* 操作按钮 */}
{!isStreaming && (
<div className="message__actions">
<button onClick={handleCopy} title="复制">
{copied ? '已复制' : <Copy size={14} />}
</button>
</div>
)}
{/* 流式加载控制 */}
{isStreaming && (
<button className="message__stop" onClick={onCancel} title="停止生成">
<Square size={14} /> 停止
</button>
)}
{isStreaming && (
<span className="message__streaming-indicator">
<Loader className="spin" size={14} />
</span>
)}
</div>
);
}
export { Message };
详细解析
关键技术点:
-
流式数据处理 : 使用
useEffect监听 content 变化,通过setInterval模拟打字机效果。生产环境通常通过EventSource或ReadableStream接收 SSE 数据。 -
Markdown 渲染 : 使用
react-markdown+react-syntax-highlighter处理代码高亮,流式输出时暂不渲染代码块避免闪烁。 -
滚动管理 :
scrollIntoView({ behavior: 'smooth' })确保新消息自动滚动到视口内。 -
思考过程 : Claude 等模型的 thinking 内容通过
<details>折叠,支持展开/收起。 -
中断生成 : 提供
onCancel回调,实际实现中需要中断fetch请求或 abort controller。
二、JavaScript/TypeScript (T1)
2.1 event loop
题目
请分析以下代码的执行顺序,并说明 JavaScript 事件循环(Event Loop)的工作机制:
javascript
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
Promise.resolve().then(() => {
setTimeout(() => console.log('4'), 0);
console.log('5');
});
console.log('6');
// 请问输出顺序是什么?为什么?
参考答案
输出顺序 : 1 → 6 → 3 → 5 → 2 → 4
执行过程分析:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | console.log('1') |
同步代码,直接执行 |
| 2 | setTimeout(() => console.log('2'), 0) |
宏任务,加入 macrotask queue |
| 3 | Promise.resolve().then() |
微任务,加入 microtask queue |
| 4 | Promise.resolve().then() |
微任务,加入 microtask queue |
| 5 | console.log('6') |
同步代码,直接执行 |
| 6 | 检查微任务队列 | 依次执行: 3 → 5 |
| 7 | setTimeout(() => console.log('4'), 0) |
宏任务,加入队列 |
| 8 | 检查宏任务队列 | 执行: 2 → 4 |
详细解析
Event Loop 三层结构:
javascript
┌─────────────────────────────────────────────────────┐
│ Call Stack │
│ (同步代码执行在此) │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ Microtask Queue │
│ Promise.then / MutationObserver / queueMicrotask │
│ ✓ 每次循环清空所有微任务 │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ Macrotask Queue │
│ setTimeout / setInterval / I/O / UI render │
│ 每次循环执行一个宏任务 │
└─────────────────────────────────────────────────────┘
核心规则:
- 同步代码先执行
- 所有微任务执行完毕后,才执行一个宏任务
- 微任务可以添加新的微任务,新的微任务会在当前微任务队列清空前执行
关键理解 : setTimeout(..., 0) 并不保证立即执行,只是将回调放入下一个宏任务队列的队首。
2.2 promise
题目
请手写一个 Promise.myAll 方法,实现类似 Promise.all 的功能,并处理以下边界情况:
- 所有 Promise 都成功
- 任意一个 Promise 失败
- 传入空数组
- 包含非 Promise 值
参考答案
javascript
Promise.myAll = function(promises) {
return new Promise((resolve, reject) => {
// 边界情况:空数组直接返回
if (!promises || promises.length === 0) {
resolve([]);
return;
}
const results = new Array(promises.length);
let completedCount = 0;
const total = promises.length;
promises.forEach((promise, index) => {
// 使用 Promise.resolve 处理非 Promise 值
Promise.resolve(promise).then(
(value) => {
results[index] = value;
completedCount++;
// 所有都完成后返回结果数组
if (completedCount === total) {
resolve(results);
}
},
(error) => {
// 任意一个失败,立即 reject
reject(error);
}
);
});
});
};
// 测试用例
Promise.myAll([
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
]).then(console.log); // [1, 2, 3]
Promise.myAll([
Promise.resolve(1),
Promise.reject('error'),
Promise.resolve(3)
]).catch(console.error); // 'error'
Promise.myAll([]).then(console.log); // []
Promise.myAll([1, 2, 3]).then(console.log); // [1, 2, 3] (自动包装)
详细解析
核心实现逻辑:
-
Promise.resolve()包装 : 确保传入的值无论是 Promise 还是普通值,都能用.then()处理。 -
计数器机制 :
completedCount记录已完成数量,用于判断是否全部完成。 -
结果数组位置固定 : 使用
results[index] = value确保结果顺序与输入顺序一致,而非按完成顺序。 -
短路失败 : 一旦任意 Promise reject,立即调用
reject(),不会等待其他 Promise。
与原生 Promise.all 的区别:
- 原生
Promise.all在 reject 后会取消剩余 Promise 的执行(但实际已在运行) - 手写版可在此基础上添加
Promise.race竞速机制优化
2.3 闭包
题目
请解释以下代码的输出结果,并说明 JavaScript 闭包的工作原理:
javascript
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
参考答案
输出结果:
- 第一个循环:
3,3,3 - 第二个循环:
0,1,2
执行过程解析:
var 版本(闭包陷阱):
javascript
// 实际等效于:
var i; // 变量提升到函数作用域
for (i = 0; i < 3; i++) {
// setTimeout 闭包捕获的是同一个变量 i
setTimeout(() => console.log(i), 100);
}
// 100ms 后,setTimeout 回调执行时,i 已经是 3
let 版本(块级作用域):
javascript
// let 创建块级作用域,每次迭代都有独立的 i
for (let i = 0; i < 3; i++) {
// 每次迭代创建新的作用域,闭包捕获的是该作用域的 i
setTimeout(() => console.log(i), 100);
}
// 每个 setTimeout 捕获的 i 分别是 0, 1, 2
修复 var 版本的几种方式:
javascript
// 方法1:使用 IIFE 创建新作用域
for (var i = 0; i < 3; i++) {
((index) => {
setTimeout(() => console.log(index), 100);
})(i);
}
// 方法2:使用 setTimeout 的第三个参数
for (var i = 0; i < 3; i++) {
setTimeout((index) => console.log(index), 100, i);
}
// 方法3:封装为函数
const log = (index) => setTimeout(() => console.log(index), 100);
for (var i = 0; i < 3; i++) {
log(i);
}
详细解析
闭包的核心原理:
-
词法作用域 (Lexical Scope): 函数在定义时决定其作用域,而非调用时。
-
闭包 = 函数 + 词法环境的引用: 闭包让函数能够访问其外部作用域的变量,即使外部函数已执行完毕。
-
var vs let:
var是函数作用域,循环结束后变量仍然存在let是块级作用域,每次迭代创建独立绑定
-
内存泄漏注意: 闭包会持有外部变量的引用,可能导致不需要的变量无法被 GC 回收。
2.4 TS泛型
题目
实现一个类型安全的 pick 函数,从对象中选取指定属性:
typescript
interface User {
id: number;
name: string;
email: string;
age: number;
}
// 实现后能这样使用:
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
// TODO: 实现
}
// 使用示例
const user: User = { id: 1, name: 'Tom', email: 'tom@test.com', age: 25 };
const result = pick(user, ['id', 'name']);
// result 类型应为: { id: number; name: string; }
参考答案
typescript
interface User {
id: number;
name: string;
email: string;
age: number;
}
// 核心实现
function pick<T extends object, K extends keyof T>(
obj: T,
keys: K[]
): { [P in K]: T[P] } {
const result = {} as { [P in K]: T[P] };
keys.forEach(key => {
if (key in obj) {
result[key] = obj[key];
}
});
return result;
}
// 使用示例
const user: User = { id: 1, name: 'Tom', email: 'tom@test.com', age: 25 };
// 类型推导正确
const result1 = pick(user, ['id', 'name']);
// result1: { id: number; name: string; }
// 错误使用:编译器报错
const result2 = pick(user, ['id', 'invalid']); // ❌ TypeScript Error
// 返回值可直接使用,类型安全
result1.id; // ✅ number
result1.name; // ✅ string
result1.email; // ❌ Property 'email' does not exist
// 进阶:添加可选参数的版本
function pickOptional<T extends object, K extends keyof T>(
obj: T,
keys: K[]
): Partial<Pick<T, K>> {
return keys.reduce((acc, key) => {
if (key in obj) {
acc[key] = obj[key];
}
return acc;
}, {} as Partial<Pick<T, K>>);
}
详细解析
泛型约束解析:
| 约束 | 含义 |
|---|---|
T extends object |
T 必须是对象类型(非原始值) |
K extends keyof T |
K 必须是 T 的键名之一 |
{ [P in K]: T[P] } |
映射类型,遍历 K 生成新类型 |
keyof 运算符: 取出对象类型的所有键名组成联合类型。
映射类型语法:
{ [P in K]: T[P] }={ id: T['id'], name: T['name'] }={ id: number, name: string }
实际应用场景:
- API 响应数据筛选
- 表单提交字段控制
- 状态管理中提取特定字段
三、Next.js (T1)
3.1 App Router
题目
对比 Next.js 的 Pages Router 和 App Router,说明 App Router 的核心优势和适用场景。
参考答案
核心对比:
| 特性 | Pages Router | App Router |
|---|---|---|
| 渲染方式 | 服务端/客户端分离 | 服务端组件默认 |
| 布局系统 | _app.js / _document.js |
嵌套 layout.tsx |
| 导航 | useRouter |
useRouter + Link |
| 数据获取 | getServerSideProps |
async Server Component |
| 路由组 | (group) |
(group) |
| 加载状态 | - | loading.tsx |
| 错误处理 | error.js |
error.tsx |
App Router 核心优势:
tsx
// app/posts/[id]/page.tsx
// 这是一个 Server Component(默认)
// 无需 useEffect / useState,直接使用 async/await
import { db } from '@/lib/db';
import { notFound } from 'next/navigation';
// 服务端直接获取数据
async function getPost(id: string) {
const post = await db.post.findUnique({ where: { id } });
if (!post) notFound(); // 内置 notFound 处理
return post;
}
export default async function PostPage({ params }: { params: { id: string } }) {
const post = await getPost(params.id);
return (
<article>
<h1>{post.title}</h1>
{/* 直接渲染,无需等待 API */}
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
嵌套布局示例:
less
app/
├── layout.tsx // 根布局(导航栏、Footer)
├── page.tsx // 首页
├── posts/
│ ├── layout.tsx // 嵌套布局(侧边栏)
│ ├── page.tsx // 文章列表
│ └── [id]/
│ ├── page.tsx // 文章详情
│ └── loading.tsx // 加载状态
└── loading.tsx // 根级加载状态
详细解析
App Router 的核心优势:
-
React Server Components (RSC): 默认服务端组件,减少客户端 JS bundle 大小,数据直接在服务端获取。
-
嵌套布局: 布局文件自动嵌套,无需手动管理组件状态共享。
-
Streaming SSR : 结合
<Suspense>实现流式渲染,首屏先显示骨架屏,内容逐步加载。 -
更细粒度的加载/错误处理 : 每个路由段可独立配置
loading.tsx、error.tsx。 -
Server Actions: 服务端函数可直接调用,简化表单处理和数据变更。
3.2 SSR
题目
解释 Next.js 中的 SSR(服务端渲染) 与 CSR(客户端渲染) 的区别,以及如何选择渲染策略。
参考答案
渲染策略对比:
| 策略 | 首屏速度 | SEO | 交互性 | 服务器压力 |
|---|---|---|---|---|
| SSR | 快 | 好 | 慢(需水合) | 高 |
| CSR | 慢 | 差 | 快 | 低 |
| SSG | 最快 | 好 | 慢(需水合) | 无(构建时生成) |
| ISR | 快 | 好 | 快 | 低 |
Next.js 渲染策略选择:
tsx
// 1. 静态生成 (SSG) - 适合内容固定不变的页面
// 编译时生成,后续请求直接返回缓存
// app/about/page.tsx
export const dynamic = 'force-static'; // 或缺失此行,默认 SSG
// 2. ISR - 适合内容频繁变化但不需要实时数据的页面
// 增量重新生成,定期更新缓存
export const revalidate = 60; // 每 60 秒重新生成一次
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 60 } // 单个 API 请求的缓存策略
});
return res.json();
}
// 3. SSR - 适合需要实时数据或个性化内容的页面
export const dynamic = 'force-dynamic';
async function getUserProfile(userId: string) {
const res = await fetch(`https://api.example.com/users/${userId}`, {
cache: 'no-store' // 不缓存,实时请求
});
return res.json();
}
// 4. 客户端渲染 - 适合高度交互的组件
'use client';
import { useEffect, useState } from 'react';
function Dashboard() {
const [data, setData] = useState(null);
useEffect(() => {
// 客户端获取数据
fetch('/api/dashboard').then(res => res.json()).then(setData);
}, []);
if (!data) return <Skeleton />;
return <div>{data.content}</div>;
}
详细解析
选择建议:
-
静态内容(文档、博客、营销页): 使用 SSG,构建时生成,首屏最快。
-
动态但变化不频繁(电商列表、新闻): 使用 ISR,定期增量更新。
-
个性化数据(用户主页、仪表盘): 使用 SSR,每次请求实时获取。
-
高度交互(聊天、游戏、实时协作): CSR + 客户端数据获取。
App Router 中的新选择:
- React Server Components: 默认服务端渲染,无水合开销,更好的性能
loading.tsx: 配合 Suspense 实现更好的加载体验
3.3 Server Component
题目
在 Next.js App Router 中,Server Components 和 Client Components 的界限如何划分?各自适用什么场景?
参考答案
组件类型划分:
tsx
// ============================================
// Server Components(默认,无需声明)
// ============================================
// - 直接访问数据库、文件系统
// - 使用 secrets/API keys(不暴露给客户端)
// - 减少客户端 JavaScript bundle
// - 无限执行时间
// - 不能使用: useState, useEffect, 事件监听
// app/page.tsx - Server Component(默认)
import { db } from '@/lib/db';
import ClientComponent from './ClientComponent'; // 导入客户端组件
export default async function Page() {
// 直接访问数据库,无需 API 路由
const posts = await db.post.findMany({
where: { published: true },
take: 10
});
return (
<div>
<h1>Latest Posts</h1>
{/* 传递数据给客户端组件 */}
<ClientComponent initialPosts={posts} />
</div>
);
}
// ============================================
// Client Components(必须声明 'use client')
// ============================================
// - 需要交互(onClick, onChange)
// - 需要状态(useState, useReducer)
// - 需要生命周期(useEffect)
// - 使用浏览器 API
// components/Dashboard.tsx
'use client';
import { useState, useEffect } from 'react';
interface Post {
id: number;
title: string;
}
export function Dashboard({ initialPosts }: { initialPosts: Post[] }) {
const [filter, setFilter] = useState('');
// 客户端过滤,无需重新请求服务器
const filteredPosts = initialPosts.filter(p =>
p.title.includes(filter)
);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter posts..."
/>
{filteredPosts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
边界设计原则:
arduino
Server Component Server Component
│ │
│ ┌── props (可序列化) ──┤
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────┐
│ Client Component │
│ 'use client' │
│ - 状态、交互、副作用 │
└─────────────────────────────────────┘
详细解析
关键规则:
-
只能向下传递: Server Component 可以导入 Client Component,但 Client Component 不能导入 Server Component(可以 props 接收)。
-
Props 必须可序列化: 传递给 Client Component 的 props 必须是 JSON 可序列化的。
-
组合策略: 在 Server Component 中组合多个 Client Component,每个 Client Component 独立水合。
-
性能收益: Server Components 不增加客户端 JS bundle,交互少的页面几乎没有客户端代码。
3.4 streaming
题目
解释 Next.js App Router 中的 Streaming SSR 机制,以及它如何提升用户体验?
参考答案
Streaming SSR 工作原理:
tsx
// app/blog/[slug]/page.tsx
import { Suspense } from 'react';
import { getPost, getRelatedPosts } from '@/lib/posts';
// 博客内容 - 较慢的数据库查询
async function PostContent({ slug }: { slug: string }) {
const post = await getPost(slug);
return (
<article>
<h1>{post.title}</h1>
<div className="prose">{post.content}</div>
</article>
);
}
// 相关推荐 - 快速的缓存查询
async function RelatedPosts({ tags }: { tags: string[] }) {
const posts = await getRelatedPosts(tags);
return (
<aside>
<h3>Related Posts</h3>
{posts.map(p => <Link key={p.id} href={`/blog/${p.id}`}>{p.title}</Link>)}
</aside>
);
}
export default async function BlogPage({ params }: { params: { slug: string } }) {
return (
<main>
{/* 内容较慢,使用 Suspense 包裹 */}
<Suspense fallback={<PostSkeleton />}>
<PostContent slug={params.slug} />
</Suspense>
{/* 推荐较快,独立流式传输 */}
<Suspense fallback={<RelatedSkeleton />}>
<RelatedPosts tags={['tech']} />
</Suspense>
</main>
);
}
加载骨架屏:
tsx
# app/blog/[slug]/PostSkeleton.tsx
export function PostSkeleton() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/2 mb-4" />
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded" />
<div className="h-4 bg-gray-200 rounded w-5/6" />
<div className="h-4 bg-gray-200 rounded w-4/6" />
</div>
</div>
);
}
渲染时序:
ini
t=0ms ├─────────────────────┬─────────────────────┐
│ HTML Shell │ │
│ (立即返回) │ │
t=100ms ├─────────────────────┼─────────────────────┤
│ PostSkeleton │ RelatedPosts │
│ (展开) │ (立即完成) │
t=500ms ├─────────────────────┼─────────────────────┤
│ PostContent │ │
│ (数据返回后渲染) │ │
t=600ms └─────────────────────┴─────────────────────┘
详细解析
Streaming 的优势:
-
TTFB (Time to First Byte) 更短: HTML shell 立即返回,无需等待所有数据就绪。
-
并行数据获取: 多个 Suspense 边界可以并行获取数据。
-
分级加载体验: 快的部分先展示,慢的部分用骨架屏过渡。
-
用户体验提升: 用户感知到的加载时间大幅缩短,减少跳出率。
适用场景:
- 博客文章(正文慢,侧边栏快)
- 产品详情(描述慢,评价快)
- 仪表盘(图表慢,概览快)
四、前端工程化 (T1)
4.1 webpack/vite
题目
对比 Webpack 和 Vite 的构建原理,说明为什么 Vite 在开发环境具有显著的性能优势,以及它们各自的适用场景。
参考答案
核心对比:
| 维度 | Webpack | Vite |
|---|---|---|
| 开发服务器 | DevServer + 打包构建 | 原生 ESM + esbuild |
| 热更新 | 重新打包相关模块 | 按需更新(HMR) |
| 生产构建 | webpack + terser | Rollup |
| 启动速度 | O(n) 随模块数增长 | O(1) 恒定时间 |
| 缓存 | 持久化缓存需配置 | 自动基于缓存 |
Webpack 工作原理:
javascript
// webpack.config.js
module.exports = {
entry: './src/main.js',
output: {
filename: '[name].[contenthash].js',
path: '/dist'
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
}
}
}
}
};
Vite 工作原理:
javascript
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
// 生产构建使用 Rollup
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom']
}
}
}
},
optimizeDeps: {
// 依赖预构建
include: ['react', 'react-dom', 'lodash']
}
});
详细解析
Vite 性能优势原理:
arduino
┌─────────────────────────────────────────────────────┐
│ Webpack Dev Server │
│ │
│ 修改文件 → 重新构建整个模块依赖图 → 重新打包 → 发送 │
│ 慢:每次修改都需要完整构建 │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ Vite Dev Server │
│ │
│ 启动:esbuild 预构建依赖(Node_modules 打包) │
│ 运行:浏览器直接请求源文件(原生 ESM) │
│ 修改:仅重新请求单个修改文件 + HMR │
│ 快:无需打包,按需加载 │
└─────────────────────────────────────────────────────┘
Vite 适用场景:
- 新项目、中小型项目(开发体验优先)
- React/Vue/Svelte 等现代框架
- 需要快速启动的 CI/CD 流程
Webpack 适用场景:
- 大型复杂项目(有完善的持久化缓存配置)
- 需要精细控制构建流程
- 老项目迁移(生态丰富)
4.2 微前端
题目
解释 微前端(Micro Frontends) 的概念和实现方式,并以 Qiankun 为例说明子应用接入的关键步骤。
参考答案
微前端核心思想: 将前端应用拆分为多个独立可部署的子应用,每个子应用可以由不同团队使用不同技术栈开发。
qiankun 接入示例:
javascript
// 1. 主应用配置 (main-app/src/main.js)
import { registerMicroApps, start, setDefaultMountApp } from 'qiankun';
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
registerMicroApps([
{
name: 'react-sub', // 子应用名称
entry: '//localhost:3001', // 子应用入口
container: '#sub-app-container', // 挂载容器
activeRule: '/react', // 激活路由
props: { // 传递给子应用的数据
mainRouter: history
}
},
{
name: 'vue-sub',
entry: '//localhost:3002',
container: '#sub-app-container',
activeRule: '/vue'
}
]);
// 设置默认进入的子应用
setDefaultMountApp('/react');
// 启动微前端
start({
prefetch: 'all', // 预加载策略
sandbox: {
strictStyleIsolation: true // 样式隔离
}
});
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
javascript
// 2. React 子应用配置 (react-sub/src/main.js)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
/**
* 导出生命周期函数供主应用调用
*/
let history;
export async function bootstrap() {
console.log('React 子应用 bootstrap');
}
export async function mount(props) {
console.log('React 子应用 mount', props);
history = props.mainRouter;
ReactDOM.createRoot(document.getElementById('root')).render(
<App />
);
}
export async function unmount() {
console.log('React 子应用 unmount');
// 清理工作
}
javascript
// 3. 子应用 webpack 配置 (react-sub/webpack.config.js)
const { name } = require('./package.json');
module.exports = {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`
}
};
详细解析
微前端的价值:
| 优势 | 说明 |
|---|---|
| 技术栈无关 | 各子应用可使用 React/Vue/Angular |
| 独立开发 | 团队独立迭代,无代码冲突 |
| 独立部署 | 子应用单独发布,快速迭代 |
| 增量升级 | 逐步替换旧模块,降低风险 |
挑战与解决方案:
- 样式冲突: 使用 CSS Modules 或 Shadow DOM 隔离
- 状态共享 : 使用
initGlobalState或 Event Bus - 公共依赖: 提取为 shared chunk,主应用加载
- 路由管理: 主应用统一管理,子应用监听路由变化
4.3 monorepo
题目
什么是 Monorepo?使用 Monorepo 管理前端项目有哪些优势和挑战?请以 pnpm workspace 为例说明配置方法。
参考答案
Monorepo 核心概念: 将多个相关项目(packages)放在同一个代码仓库中统一管理。
pnpm workspace 配置:
yaml
# pnpm-workspace.yaml
packages:
- 'packages/*' # 所有 packages 目录下的包
- 'apps/*' # 所有应用
- '!packages/docs' # 排除特定目录
项目结构:
csharp
my-monorepo/
├── pnpm-workspace.yaml # workspace 配置
├── pnpm-lock.yaml
├── .eslintrc.js
├── tsconfig.base.json # 基础 TypeScript 配置
│
├── apps/
│ ├── web/ # 主应用
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── admin/ # 管理后台
│
└── packages/
├── ui/ # UI 组件库
│ ├── package.json
│ └── src/
│ ├── Button/
│ └── Input/
├── utils/ # 工具函数库
└── shared/ # 共享类型/常量
json
// apps/web/package.json
{
"name": "@my-app/web",
"version": "1.0.0",
"dependencies": {
"@my-app/ui": "workspace:*", // 使用 workspace 协议
"@my-app/utils": "workspace:*",
"react": "^18.0.0"
}
}
bash
# 安装依赖 - 自动链接 workspace 包
pnpm install
# 安装到根目录
pnpm add -w -D eslint
# 安装到指定包
pnpm --filter @my-app/web add react-router-dom
# 构建所有包(依赖顺序)
pnpm run -r build
详细解析
Monorepo 优势:
| 优势 | 说明 |
|---|---|
| 代码复用 | 多个项目共享组件、工具 |
| 一致性 | 统一依赖版本,避免版本碎片 |
| 原子提交 | 跨包改动可一次性提交 |
| 简化依赖管理 | 统一的 node_modules |
挑战与解决方案:
- 构建性能 : 使用
turbo或nx增量构建,只构建受影响的包 - CI/CD : 配置
turbo.json跳过无变更包的构建 - 代码规范: 统一 ESLint/Prettier 配置到根目录
- 版本管理 : 使用
changesets管理包版本和 changelog
4.4 pnpm/turbo
题目
组合使用 pnpm + Turbo 构建高性能的 Monorepo 工具链,请说明配置方法和增量构建原理。
参考答案
Turbo 配置文件:
json
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"], // ^ 表示依赖的包先构建
"outputs": ["dist/**", ".next/**"] // 缓存输出
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"],
"inputs": ["src/**", "*.config.js"]
},
"lint": {
"outputs": []
},
"dev": {
"cache": false, // 开发模式不缓存
"persistent": true // 常驻进程
}
}
}
package.json 脚本:
json
// apps/web/package.json
{
"scripts": {
"dev": "turbo run dev --filter=@my-app/web",
"build": "turbo run build --filter=@my-app/web",
"lint": "turbo run lint",
"test": "turbo run test"
}
}
增量构建原理:
bash
修改 packages/ui/Button/index.tsx
↓
Turbo 检测依赖图
↓
┌─────────────────────────────────────┐
│ 受影响包分析: │
│ - @my-app/ui → 需要重新构建 ✓ │
│ - @my-app/web → 依赖 ui,需要构建 ✓ │
│ - @my-app/admin → 不受影响,跳过 ✓ │
└─────────────────────────────────────┘
↓
使用本地/远程缓存恢复未变更输出
↓
实际只构建 @my-app/ui 和 @my-app/web
Remote Cache(可选):
bash
# 使用 Vercel Remote Cache 或自建
TURBO_TOKEN=xxx TURBO_TEAM=my-team pnpm build
详细解析
Turbo 核心特性:
- 任务调度 : 根据
pipeline定义的任务依赖和顺序执行 - 远程缓存: 相同的输入产生相同的输出,可跨机器共享缓存
- 影响分析: 智能检测哪些包需要重新构建
- 并行执行: 无依赖的任务可并行执行
性能收益示例:
| 场景 | 无缓存构建 | Turbo 缓存 | 节省时间 |
|---|---|---|---|
| 100 包全量构建 | 10 分钟 | 10 分钟 | 0 |
| 修改 1 个底层包 | 10 分钟 | ~30 秒 | ~95% |
| Pull Request CI | 10 分钟 | ~1 分钟 | ~90% |
五、前端性能优化 (T1)
5.1 懒加载
题目
在现代前端应用中,有哪些懒加载的实现方式?请分别说明路由懒加载、组件懒加载和资源懒加载的实现方法。
参考答案
1. 路由懒加载:
tsx
// React Router v6
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
);
}
tsx
// Vue Router
const routes = [
{
path: '/',
component: () => import('./views/Home.vue')
},
{
path: '/about',
component: () => import('./views/About.vue')
}
];
2. 组件懒加载:
tsx
// React - Suspense + lazy
const HeavyChart = lazy(() => import('./components/HeavyChart'));
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart data={chartData} />
</Suspense>
</div>
);
}
// Vue - defineAsyncComponent
import { defineAsyncComponent } from 'vue';
const HeavyModal = defineAsyncComponent(() =>
import('./components/HeavyModal.vue')
);
3. 图片懒加载:
tsx
// Intersection Observer API
function LazyImage({ src, alt }) {
const [isVisible, setIsVisible] = useState(false);
const imgRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ rootMargin: '100px' } // 提前 100px 开始加载
);
observer.observe(imgRef.current);
return () => observer.disconnect();
}, []);
return (
<img
ref={imgRef}
src={isVisible ? src : placeholder}
alt={alt}
loading="lazy" // 原生属性作为降级方案
/>
);
}
4. 动态导入非路由模块:
tsx
// 根据条件动态加载
async function loadFeature() {
if (user.isPro) {
const { ProEditor } = await import('./ProEditor');
return <ProEditor />;
}
return <BasicEditor />;
}
详细解析
懒加载的核心价值:
| 优化点 | 说明 |
|---|---|
| 首屏加载 | 减少初始 JS bundle 大小 |
| TTI | 用户可交互时间提前 |
| 带宽节省 | 按需加载,节省流量 |
| 缓存效率 | 非首屏代码独立缓存 |
最佳实践:
- 首屏关键路由必须同步加载
- 非首屏路由使用
React.lazy+Suspense - 图片使用
loading="lazy"或 Intersection Observer - 大型库(如图表)使用动态 import
5.2 虚拟滚动
题目
实现一个高性能的 虚拟滚动列表,处理百万级数据渲染场景。请说明核心原理并实现关键代码。
参考答案
虚拟滚动核心原理:
makefile
┌─────────────────────────────────────────┐
│ Viewport (可视区域) │
│ ┌─────────────────────────────────┐ │
│ │ Item 15 │ │
│ │ Item 16 ←── 可见区域 │ │
│ │ Item 17 │ │
│ │ Item 18 │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
只渲染: Item 15, 16, 17, 18(可能多渲染几个缓冲项)
总数据: 1,000,000 条
React 虚拟列表实现:
tsx
import React, { useState, useEffect, useRef, useCallback } from 'react';
interface VirtualListProps<T> {
items: T[];
height: number;
itemHeight: number;
renderItem: (item: T, index: number) => React.ReactNode;
}
function VirtualList<T>({ items, height, itemHeight, renderItem }: VirtualListProps<T>) {
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
// 计算可见范围
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - 2);
const endIndex = Math.min(
items.length - 1,
Math.ceil((scrollTop + height) / itemHeight) + 2
);
// 只渲染可见区域的项
const visibleItems = [];
for (let i = startIndex; i <= endIndex; i++) {
visibleItems.push(
<div
key={i}
style={{
position: 'absolute',
top: `${i * itemHeight}px`,
height: `${itemHeight}px`,
left: 0,
right: 0
}}
>
{renderItem(items[i], i)}
</div>
);
}
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
setScrollTop(e.currentTarget.scrollTop);
}, []);
// 总高度占位,保持滚动条正确
const totalHeight = items.length * itemHeight;
return (
<div
ref={containerRef}
style={{ height: `${height}px`, overflow: 'auto', position: 'relative' }}
onScroll={handleScroll}
>
{/* 占位元素 */}
<div style={{ height: `${totalHeight}px`, position: 'relative' }}>
{/* 可视内容 */}
{visibleItems}
</div>
</div>
);
}
// 使用示例
function App() {
const items = Array.from({ length: 1_000_000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
email: `user${i}@example.com`
}));
return (
<VirtualList
items={items}
height={600}
itemHeight={50}
renderItem={(item) => (
<div className="flex items-center gap-4 p-2 border-b">
<span className="w-20">{item.id}</span>
<span>{item.name}</span>
<span className="text-gray-500">{item.email}</span>
</div>
)}
/>
);
}
详细解析
虚拟滚动关键点:
-
固定高度假设: 为了简化计算,实际使用中列表项高度通常需要固定或预知。
-
缓冲区域 :
±2缓冲项防止快速滚动时出现空白。 -
滚动监听 : 使用
onScroll或addEventListener监听滚动事件。 -
位置计算 : 使用
transform: translateY()替代top可获得更好的重绘性能。
进阶优化:
- 使用
will-change: transform提示浏览器优化 - 对滚动事件使用
throttle/debounce或requestAnimationFrame - 考虑使用
@tanstack/react-virtual等成熟库
5.3 长列表优化
题目
针对一个新闻 Feed 流场景,请设计完整的性能优化方案,包括首屏加载、数据分页、滚动优化等。
参考答案
完整 Feed 流优化方案:
tsx
// 1. 无限滚动 + 分页
function Feed() {
const [posts, setPosts] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
const observerRef = useRef();
// 使用 Intersection Observer 触发加载
const lastPostRef = useCallback(node => {
if (loading) return;
if (observerRef.current) observerRef.current.disconnect();
observerRef.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasMore) {
setPage(p => p + 1);
}
});
if (node) observerRef.current.observe(node);
}, [loading, hasMore]);
// 数据获取
useEffect(() => {
async function fetchPosts() {
setLoading(true);
const data = await api.getPosts({ page, limit: 20 });
setPosts(prev => [...prev, ...data.posts]);
setHasMore(data.hasMore);
setLoading(false);
}
fetchPosts();
}, [page]);
return (
<div className="feed">
{posts.map((post, index) => (
<PostCard
key={post.id}
post={post}
ref={index === posts.length - 1 ? lastPostRef : null}
/>
))}
{loading && <LoadingSpinner />}
{!hasMore && <EndOfFeed />}
</div>
);
}
// 2. 图片懒加载 + 渐进加载
function PostImage({ src, alt }) {
const [loaded, setLoaded] = useState(false);
const [error, setError] = useState(false);
return (
<div className="relative bg-gray-200">
{/* 低质量占位图 (BlurHash/LQIP) */}
{!loaded && <div className="absolute inset-0 bg-gray-300 animate-pulse" />}
<img
src={error ? fallbackSrc : src}
alt={alt}
loading="lazy"
onLoad={() => setLoaded(true)}
onError={() => setError(true)}
className={`transition-opacity duration-300 ${loaded ? 'opacity-100' : 'opacity-0'}`}
/>
</div>
);
}
// 3. 时间分桶 - 减少渲染节点
function GroupedFeed({ posts }) {
// 按日期分组
const grouped = posts.reduce((acc, post) => {
const date = dayjs(post.createdAt).format('YYYY-MM-DD');
(acc[date] || (acc[date] = [])).push(post);
return acc;
}, {});
return (
<div>
{Object.entries(grouped).map(([date, datePosts]) => (
<div key={date}>
<DateHeader date={date} />
{datePosts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
))}
</div>
);
}
// 4. 虚拟滚动适配
function VirtualizedFeed({ posts }) {
return (
<VirtualList
items={posts}
height={window.innerHeight}
itemHeight={estimatedHeight} // 估算高度
renderItem={(post) => <PostCard post={post} />}
overscan={5}
/>
);
}
详细解析
Feed 流优化矩阵:
| 优化点 | 技术方案 | 收益 |
|---|---|---|
| 首屏加载 | Skeleton + 预加载 | 感知加载时间↓ |
| 分页 | Cursor/Offset 分页 | 内存占用↓ |
| 无限滚动 | Intersection Observer | 自动化加载 |
| 图片 | LQIP + Lazy Load | 首屏流量↓, FCP↑ |
| 渲染 | 时间分桶/虚拟列表 | 渲染节点↓ |
| 缓存 | SWR/React Query | 重复请求↓ |
关键指标监控:
javascript
// 监控 Feed 性能
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log({
type: entry.name,
duration: entry.duration,
size: entry.transferSize
});
}
});
observer.observe({ entryTypes: ['largest-contentful-paint', 'first-input'] });
5.4 performance分析
题目
如何使用 Chrome DevTools 进行前端性能分析?请说明 Performance 面板的核心指标和分析方法。
参考答案
Performance 面板分析流程:
css
┌────────────────────────────────────────────────────────────┐
│ Performance 面板 │
├────────────────────────────────────────────────────────────┤
│ ┌─ 控制栏 ─────────────────────────────────────────────┐ │
│ │ [●录制] [⟳ 重新加载] [清除] Screenshots ▼ ▼ 3G │ │
│ └───────────────────────────────────────────────────────┘ │
│ ┌─ 概览区 ─────────────────────────────────────────────┐ │
│ │ FPS ─────────────────────────────────────────────── │ │
│ │ CPU ─────────────────────────────────────────────── │ │
│ │ NET ─────────────────────────────────────────────── │ │
│ └───────────────────────────────────────────────────────┘ │
│ ┌─ 火焰图 ────────────────────────────────────────────┐ │
│ │ ████████████████ Main Thread (V8 JS) │ │
│ │ └─ JS Evaluate ── Function Call ── GC ── Layout │ │
│ └───────────────────────────────────────────────────────┘ │
│ ┌─ 网络时间线 ────────────────────────────────────────┐ │
│ │ [===html===][===css===][===js===][===img===] │ │
│ └───────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
核心指标解读:
| 指标 | 含义 | 优化目标 |
|---|---|---|
| FP (First Paint) | 首次绘制 | < 1.8s |
| FCP (First Contentful Paint) | 首次内容绘制 | < 1.8s |
| LCP (Largest Contentful Paint) | 最大内容绘制 | < 2.5s |
| CLS (Cumulative Layout Shift) | 布局偏移 | < 0.1 |
| TTI (Time to Interactive) | 可交互时间 | < 3.8s |
火焰图分析方法:
javascript
// 1. 识别长任务 (Long Tasks > 50ms)
Long Task ████████████████████████████████
└─ 超过 50ms 的主线程任务
// 2. 定位卡顿原因
Main Thread:
└─ Task: 200ms
└─ Function: heavyCalculation (50ms)
└─ Function: renderComponent (30ms)
└─ Function: setState (120ms) ← 状态更新触发大量重渲染
└─ Component.render (80ms)
└─ ChildComponent.render (40ms)
// 3. 优化策略
// - 拆分长任务为多个小任务
// - 使用 requestIdleCallback
// - 减少不必要的 setState
代码级性能分析:
javascript
// 使用 Performance API 精确测量
performance.mark('operation-start');
// ... 执行耗时操作 ...
performance.mark('operation-end');
performance.measure('operation duration', 'operation-start', 'operation-end');
// 分析 React 组件渲染
import { ReactDevToolsProfiler } from 'react';
// 或使用 useMemo/useCallback 减少重渲染
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);
详细解析
Performance 面板使用步骤:
- 录制前: 勾选 "Screenshots" 和 "Web Vitals",选择适当网络节流
- 录制中: 执行待分析的操作(滚动、点击、数据加载等)
- 录制后: 分析火焰图,找出耗时任务
常见性能问题模式:
| 火焰图特征 | 问题 | 解决方案 |
|---|---|---|
| 大量短任务堆积 | 频繁 setState | useDeferredValue |
| 单个长任务 | 复杂计算 | Web Worker / 任务拆分 |
| Layout thrashing | 读写交替触发重排 | 批量读写 |
| 长时间 GC | 内存分配过多 | 对象池/避免频繁创建 |
Web Vitals 自动化监控:
javascript
import { getCLS, getFID, getLCP } from 'web-vitals';
function sendToAnalytics({ name, value, id }) {
// 上报至数据分析服务
navigator.sendBeacon('/analytics', JSON.stringify({ name, value, id }));
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);