React: 错误边界与调试
最后更新:2026-08-26
Tom 负责的电商后台在线上出现了"白屏"事故——用户反馈订单详情页打开后页面完全空白,控制台报错
Cannot read properties of undefined (reading 'name')。原因是一个子组件在渲染阶段抛出了未捕获的 JavaScript 错误,导致整个 React 组件树崩溃。Tom 需要在应用中建立错误边界体系,确保部分组件崩溃不影响整体页面可用性。
1. 你将学到
- Error Boundary 的原理与最佳实践
- React DevTools Profiler 火焰图分析方法
- Profiler 组件测量组件渲染耗时
- 常见性能问题诊断与修复策略
2. 概念图解
下面的图展示了 Error Boundary 的捕获流程和 Profiler 的分析链路:
flowchart TD
A[React 组件树] --> B[Error Boundary]
B --> C{子组件渲染是否出错?}
C -->|否| D[正常渲染]
C -->|是| E[getDerivedStateFromError]
E --> F[更新 state.hasError = true]
F --> G[渲染 fallback UI]
G --> H{是否点击重试?}
H -->|是| I[重置 state]
I --> A
H -->|否| J[保持降级 UI]
K[React DevTools Profiler] --> L[录制组件渲染]
L --> M[火焰图分析]
M --> N["识别耗时组件(黄色/红色)"]
N --> O[React.memo / useMemo 优化]
style B fill:#e3f2fd,stroke:#1565c0
style E fill:#fff3e0,stroke:#e65100
style G fill:#e8f5e9,stroke:#2e7d32
style K fill:#f3e5f5,stroke:#7b1fa2
3. 一个真实场景
| 错误处理层级 | 捕获范围 | 恢复策略 | 适用场景 |
|---|---|---|---|
| try/catch | 单个异步操作 | 重试/降级 | API 请求、Promise 操作 |
| Error Boundary | 子组件树渲染错误 | 降级 UI + 重试按钮 | 组件白屏防护 |
| 全局 unhandledrejection | 未捕获的 Promise 错误 | 日志上报 | 兜底监控 |
| window.onerror | 全局同步错误 | 日志上报 | 兜底监控 |
| React DevTools | 开发时调试 | 定位问题 | 性能/渲染问题排查 |
Tom 的订单详情页包含以下组件结构:
OrderPage
├── OrderHeader (订单号、状态)
├── OrderItems (商品列表)
│ └── OrderItem × N(单个商品,含价格计算)
├── ShippingInfo (物流信息)
└── PaymentInfo (支付信息)
线上事故的原因是:某个订单的商品数据中 price 字段缺失,OrderItem 组件访问 item.price.toFixed(2) 时抛出了 TypeError,导致整个 OrderPage 白屏。
正确做法是:用 Error Boundary 包裹 OrderItems 区域,即使商品列表渲染失败,订单头部和支付信息依然可以正常显示。另外,Tom 还需要了解如何用 React DevTools Profiler 定位性能瓶颈。
(1) Error Boundary — 组件级安全网
Error Boundary 是 React 提供的声明式错误捕获机制。当子树中的任何组件在渲染阶段、生命周期方法或构造函数中抛出错误时,Error Boundary 可以捕获该错误并显示备用 UI,而不是让整个应用白屏。
注意: Error Boundary 目前只能用 class 组件实现(React 官方计划在未来版本中提供 Hook 版本)。
▶ 示例 1:通用 ErrorBoundary 组件
import { Component, ErrorInfo, ReactNode } from 'react'
interface ErrorBoundaryProps {
children: ReactNode
/** 自定义降级 UI */
fallback?: ReactNode
/** 错误回调(上报 Sentry 等) */
onError?: (error: Error, errorInfo: ErrorInfo) => void
}
interface ErrorBoundaryState {
hasError: boolean
error: Error | null
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { hasError: false, error: null }
}
// 静态方法:根据错误更新 state
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error }
}
// 生命周期:捕获错误后做副作用(日志上报)
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary 捕获到错误:', error.message)
console.error('组件栈:', errorInfo.componentStack)
// 上报到错误监控服务
if (this.props.onError) {
this.props.onError(error, errorInfo)
}
// 实际项目可集成 Sentry:
// Sentry.captureException(error, { extra: errorInfo })
}
handleReset = () => {
this.setState({ hasError: false, error: null })
}
render() {
if (this.state.hasError) {
// 使用自定义 fallback 或默认降级 UI
if (this.props.fallback) {
return this.props.fallback
}
return (
<div
role="alert"
style={{
padding: '32px 24px',
margin: 16,
background: '#fff2f0',
border: '1px solid #ffccc7',
borderRadius: 8,
textAlign: 'center',
}}
>
<h2 style={{ color: '#ff4d4f', margin: '0 0 12px' }}>
组件出错了
</h2>
<p style={{ color: '#666', marginBottom: 8, fontSize: 14 }}>
{this.state.error?.message || '发生了未知错误'}
</p>
<button
onClick={this.handleReset}
style={{
padding: '6px 20px',
background: '#ff4d4f',
color: '#fff',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
fontSize: 14,
}}
>
重试
</button>
</div>
)
}
return this.props.children
}
}
export default ErrorBoundary
在实际项目中使用 ErrorBoundary
// 分层包裹 — 每个独立区域有自己的 ErrorBoundary
function OrderPage({ orderId }: { orderId: string }) {
return (
<div>
{/* 订单头信息:即使下面出错了也能正常显示 */}
<ErrorBoundary fallback={<p>订单加载失败</p>}>
<OrderHeader orderId={orderId} />
</ErrorBoundary>
{/* 商品列表:自己出错不影响其他区域 */}
<ErrorBoundary
onError={(err) => {
// 上报商品列表渲染错误
fetch('/api/log-error', {
method: 'POST',
body: JSON.stringify({ error: err.message, orderId }),
})
}}
>
<OrderItems orderId={orderId} />
</ErrorBoundary>
{/* 支付信息 */}
<ErrorBoundary>
<PaymentInfo orderId={orderId} />
</ErrorBoundary>
</div>
)
}
▶ 示例 2:带错误恢复的 UserProfile 组件
实际业务中,有时候仅显示降级 UI 不够——用户可能需要刷新局部数据。下面是一个带"重试"功能的错误边界用法:
import { useState } from 'react'
import ErrorBoundary from './ErrorBoundary'
// 模拟会出错的数据获取
function fetchUserData(userId: number) {
return fetch(`/api/users/${userId}`).then(res => {
if (!res.ok) throw new Error('用户数据获取失败')
return res.json()
})
}
// 可能有渲染错误的数据展示组件
function UserInfo({ userId }: { userId: number }) {
const [user, setUser] = useState<any>(null)
const [loading, setLoading] = useState(true)
useState(() => {
fetchUserData(userId)
.then(setUser)
.finally(() => setLoading(false))
})
if (loading) return <p>加载中...</p>
// 如果 user 数据结构异常,这里可能报错
return (
<div>
<h3>{user.name}</h3> {/* 可能:Cannot read properties of undefined */}
<p>{user.profile.bio}</p> {/* 可能:Cannot read properties of undefined */}
</div>
)
}
// 外层容器:带重试 key 机制
function UserProfile({ userId }: { userId: number }) {
const [retryKey, setRetryKey] = useState(0)
return (
<ErrorBoundary
key={retryKey} // 改变 key 会卸载并重新挂载子树
fallback={
<div style={{ padding: 24, textAlign: 'center' }}>
<p>用户信息加载异常</p>
<button onClick={() => setRetryKey(k => k + 1)}>
重试加载
</button>
</div>
}
>
<UserInfo userId={userId} />
</ErrorBoundary>
)
}
关键技巧: key={retryKey} 让 ErrorBoundary 在点击重试时卸载并重新创建子树,从而重置所有子组件的状态。
(2) Error Boundary 不能捕获的错误
Error Boundary 不是万能的,以下四类错误它无法捕获:
| 错误类型 | 原因 | 解决方案 |
|---|---|---|
| 事件处理中的错误 | 事件处理器不在渲染阶段执行 | 用 try/catch 包裹事件处理逻辑 |
| 异步代码中的错误 | setTimeout / Promise 回调不在 React 渲染周期内 | 用 try/catch 或 Promise.catch |
| 服务端渲染(SSR)中的错误 | Error Boundary 只在客户端生效 | SSR 端用 try/catch 包裹渲染 |
| Error Boundary 自身的错误 | 自身抛出错误无法捕获自己 | 在最外层再套一个 Error Boundary |
事件处理 + 异步代码的正确错误处理
function PaymentForm() {
async function handleSubmit() {
try {
const result = await submitPayment()
// 成功处理
} catch (error) {
// 异步错误在这里捕获,Error Boundary 管不到这里
console.error('支付失败:', error)
// 显示错误 UI(比如设置 state)
setError(error instanceof Error ? error.message : '支付失败')
}
}
// 事件中的错误也要用 try/catch
function handleClick() {
try {
processPayment()
} catch (error) {
setError('处理失败,请重试')
}
}
}
(3) 使用 React DevTools Profiler 分析性能
React DevTools 的 Profiler 标签是分析组件渲染性能的核心工具。它生成"火焰图",直观展示每个组件的渲染耗时。
使用步骤
1. 打开浏览器 DevTools → Components 标签页
2. 切换到 Profiler 子标签
3. 点击蓝色录制按钮(开始录制)
4. 在页面上进行操作(点击、滚动等)
5. 点击停止按钮(结束录制)
6. 查看火焰图
火焰图解读方法
┌────────────────────────────────────────────┐
│ App (0.3ms) │
│ ├── Navbar (0.2ms) │
│ ├── OrderPage (2.1ms) │
│ │ ├── OrderHeader (0.4ms) ── 灰色 │
│ │ ├── OrderItems (1.5ms) ── 黄色 │
│ │ │ └── OrderItem × 20 (各 0.3ms) │
│ │ └── PaymentInfo (0.2ms) ── 灰色 │
│ └── Footer (0.1ms) │
└────────────────────────────────────────────┘
- 灰色:没有重新渲染(理想状态)
- 蓝色:重新渲染但耗时正常
- 黄色/红色:渲染耗时较长,需要关注
▶ 示例 3:用 Profiler 组件测量渲染耗时
React 内置的 <Profiler> 组件可以在代码中精确测量某个组件的渲染耗时,适合做性能指标的自动监控:
import { Profiler } from 'react'
type ProfilerPhase = 'mount' | 'update' | 'nested-update'
interface ProfileMetrics {
id: string
phase: ProfilerPhase
actualDuration: number // 本次渲染实际耗时(毫秒)
baseDuration: number // 子树最坏情况下的耗时
startTime: number // 渲染开始时间戳
commitTime: number // 提交到 DOM 的时间戳
interactions: Set<any> // 相关的交互追踪
}
// 性能监控回调
function onRenderCallback(
id: string,
phase: ProfilerPhase,
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number,
) {
// 记录到性能日志
if (actualDuration > 16) { // 超过 16ms = 掉帧阈值(60fps)
console.warn(
`[性能告警] ${id} 在 ${phase} 阶段耗时 ${actualDuration.toFixed(1)}ms,` +
`超过 16ms 帧预算!`
)
// 上报到性能监控系统
// reportPerformance({ id, phase, actualDuration, baseDuration })
}
// 开发环境输出到控制台
if (process.env.NODE_ENV === 'development') {
console.table({
'组件': id,
'阶段': phase,
'实际耗时(ms)': actualDuration.toFixed(1),
'基准耗时(ms)': baseDuration.toFixed(1),
})
}
}
// 大数据列表——潜在的性能瓶颈
function ProductList({ products }: { products: Product[] }) {
return (
<Profiler id="ProductList" onRender={onRenderCallback}>
<div style={{ display: 'grid', gap: 16, gridTemplateColumns: 'repeat(3, 1fr)' }}>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</Profiler>
)
}
常见的性能优化策略
// 1. React.memo — 避免不必要的重渲染
const ProductCard = React.memo(function ProductCard({
product,
}: {
product: Product
}) {
return (
<div style={{ border: '1px solid #eee', padding: 16, borderRadius: 8 }}>
<img src={product.image} alt={product.name} width="100%" />
<h4>{product.name}</h4>
<p>${product.price}</p>
</div>
)
})
// 2. useMemo — 缓存昂贵的计算结果
function OrderSummary({ items }: { items: OrderItem[] }) {
const totalPrice = useMemo(() => {
return items.reduce((sum, item) => {
// 假设这里做了复杂的汇率换算
return sum + convertCurrency(item.price, item.currency)
}, 0)
}, [items])
return <p>总计:${totalPrice.toFixed(2)}</p>
}
// 3. useCallback — 稳定的函数引用
function OrderList({ orders, onSelect }: {
orders: Order[]
onSelect: (id: string) => void
}) {
// ✅ 用 useCallback 保持引用稳定
const handleSelect = useCallback((id: string) => {
onSelect(id)
}, [onSelect])
return orders.map(order => (
<OrderRow key={order.id} order={order} onSelect={handleSelect} />
))
}
(4) 使用 React DevTools Components 面板调试
除了 Profiler,React DevTools 的 Components 面板也是日常调试的利器:
| 功能 | 用途 | 操作 |
|---|---|---|
| 组件树浏览 | 查看组件层级关系 | 点击 DevTools → Components |
| Props/State 实时查看 | 检查组件的当前状态 | 选中组件,查看右侧面板 |
| 直接修改 State | 测试不同状态下的 UI | 双击 state 值,直接编辑 |
| 搜索组件 | 快速定位组件 | Ctrl+F 输入组件名 |
| 跳转到源代码 | 查看组件实现 | 点击 <> 图标 |
// DevTools Components 面板示例
<OrderPage>
<ErrorBoundary>
<OrderHeader
orderNumber="ORD-2026-0001" ← Props 实时显示
status="shipped" ← 可直接编辑测试
/>
</ErrorBoundary>
<ErrorBoundary>
<OrderItems>
<OrderItem product={...} /> ← State 展开查看
<OrderItem product={...} />
</OrderItems>
</ErrorBoundary>
</OrderPage>
▶ 示例 4:API 请求错误处理——带重试的数据获取
function useFetchWithRetry(url, maxRetries = 3) {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const [loading, setLoading] = useState(true)
const [retries, setRetries] = useState(0)
const fetchData = useCallback(async () => {
setLoading(true)
setError(null)
try {
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const json = await res.json()
setData(json)
} catch (err) {
if (retries < maxRetries) {
setRetries(r => r + 1)
setTimeout(fetchData, 1000 * (retries + 1))
} else {
setError(err.message)
}
} finally {
setLoading(false)
}
}, [url, retries, maxRetries])
useEffect(() => { fetchData() }, [url])
return { data, error, loading, retries, refetch: () => { setRetries(0); fetchData() } }
}
function UserList() {
const { data: users, error, loading, retries, refetch } = useFetchWithRetry('/api/users')
if (loading) return <p>Loading... {retries > 0 && `(retry ${retries})`}</p>
if (error) return (
<div style={{ padding: 20, textAlign: 'center' }}>
<p style={{ color: '#ff4d4f' }}>Error: {error}</p>
<button onClick={refetch} style={{ padding: '8px 16px', cursor: 'pointer' }}>Retry</button>
</div>
)
return (
<ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
)
}
▶ 示例 5:全局错误监控——Sentry 集成
// lib/errorReporting.ts
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN
function initErrorReporting() {
if (typeof window === 'undefined') return
if (!SENTRY_DSN) return
// Sentry.init({ dsn: SENTRY_DSN, ... })
// 简化示例:用全局事件监听模拟
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled Promise:', event.reason)
reportError({
type: 'unhandledrejection',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
timestamp: new Date().toISOString(),
})
})
window.addEventListener('error', (event) => {
console.error('Global Error:', event.error)
reportError({
type: 'window.error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
timestamp: new Date().toISOString(),
})
})
}
function reportError(payload) {
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}).catch(() => {})
}
// app/layout.tsx
function RootLayout({ children }) {
useEffect(() => { initErrorReporting() }, [])
return <html><body>{children}</body></html>
}
❓ 常见问题
useMemo / useCallback 保持引用稳定,或者给 React.memo 传入第二个参数——自定义比较函数 React.memo(Comp, (prev, next) => deepEqual(prev, next))。<Profiler> 组件会影响生产环境性能吗?<Profiler> 组件在生产环境会有少量性能开销。建议只在开发环境使用,或者用环境变量控制:{process.env.NODE_ENV === 'development' && <Profiler>...}。需要线上性能监控时,可以考虑专用的性能监控库(如 web-vitals)或 Sentry 的性能追踪功能。getDerivedStateFromError 和 componentDidCatch 两个生命周期,这两个只有 class 组件支持。React 团队表示未来可能提供 Hook 版本,但目前(React 18/19)只能用 class 组件。你可以写一个 class Error Boundary 组件,然后用函数组件包装它来处理错误恢复逻辑。📖 小节
- Error Boundary 是 React 的声明式错误捕获方案,防止局部崩溃导致整个应用白屏
- Error Boundary 只能用 class 组件实现,通过
getDerivedStateFromError和componentDidCatch两个生命周期协作 - 改变 Error Boundary 的
key可以实现子树的重置(重新挂载) - Error Boundary 不能捕获事件处理、异步代码、SSR 和自身的错误
- React DevTools Profiler 通过火焰图展示渲染耗时,黄色/红色组件是需要优化的目标
- React.memo / useMemo / useCallback 是 React 性能优化的"三件套"
📝 作业
- 创建一个 ErrorBoundary 组件,在
OrderPage中分层使用:OrderHeader、OrderItems、PaymentInfo各用独立的 ErrorBoundary 包裹。手动制造一个渲染错误(如传入错误的 Props),验证只有出错的区域显示降级 UI,其他区域正常显示。 - 使用
Profiler组件测量一个包含 100 个列表项的组件渲染耗时。用React.memo优化后再次测量,对比两次的actualDuration差异,确认优化效果。 - 在 Chrome 中打开 React DevTools Profiler,录制一次页面交互操作(如搜索、过滤、排序),火焰图中找出渲染耗时最长的组件,分析原因并用
useMemo/useCallback优化。