react-render-optimization
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact Render Optimization
React渲染性能优化
Practical patterns for eliminating unnecessary re-renders, reducing rendering cost, and keeping React UIs responsive. These patterns apply to any React application — whether you're using Vite, Next.js, Remix, or a custom setup.
以下是实用的优化模式,可消除不必要的重渲染、降低渲染成本并保持React UI的响应性。这些模式适用于任何React应用——无论你使用Vite、Next.js、Remix还是自定义配置。
When to Use
适用场景
Reference these patterns when:
- Components re-render more often than expected
- UI feels sluggish during typing, scrolling, or interactions
- Profiler shows wasted renders in the component tree
- Building performance-sensitive features (dashboards, editors, lists)
- Reviewing or refactoring existing React components
在以下场景中参考这些模式:
- 组件重渲染次数超出预期
- 输入、滚动或交互时UI反应迟缓
- 性能分析器显示组件树中存在无效渲染
- 构建对性能敏感的功能(如仪表盘、编辑器、列表)
- 审查或重构现有React组件
Instructions
使用说明
- Apply these patterns during code generation, review, and refactoring. When you see an anti-pattern, suggest the corrected version with an explanation.
- 在代码生成、审查和重构过程中应用这些模式。当发现反模式时,建议提供修正后的版本并附上解释。
Details
详细内容
Overview
概述
React re-renders a component whenever its state changes, a parent re-renders, or context it consumes updates. Most re-renders are harmless, but when they trigger expensive computation, deep trees, or layout thrashing they become visible to users.
The patterns below are ordered by impact — address the biggest wins first before reaching for micro-optimizations.
当组件的状态变化、父组件重渲染或其消费的上下文更新时,React会重新渲染该组件。大多数重渲染是无害的,但当它们触发昂贵的计算、深层组件树渲染或布局抖动时,用户就会察觉到性能问题。
以下模式按影响程度排序——先解决影响最大的问题,再处理微优化。
1. Compute Derived Values During Render — Don't Store Them
1. 在渲染时计算派生值,而非存储它们
Impact: HIGH — Eliminates an entire category of bugs and unnecessary state.
Storing values that can be computed from existing state or props creates synchronization problems and extra re-renders. Compute them inline instead.
Avoid — redundant state that drifts:
tsx
function ProductList({ products }: { products: Product[] }) {
const [search, setSearch] = useState('')
const [filtered, setFiltered] = useState(products)
useEffect(() => {
setFiltered(products.filter(p =>
p.name.toLowerCase().includes(search.toLowerCase())
))
}, [products, search])
return (
<>
<input value={search} onChange={e => setSearch(e.target.value)} />
{filtered.map(p => <ProductCard key={p.id} product={p} />)}
</>
)
}Prefer — derive during render (cheap derivations use plain ):
consttsx
function ProductList({ products }: { products: Product[] }) {
const [search, setSearch] = useState('')
// Cheap derivation — plain const, no useMemo needed
const hasSearch = search.length > 0
const normalizedSearch = search.toLowerCase()
// Expensive derivation — useMemo is justified when iterating large arrays
const filtered = useMemo(
() => products.filter(p =>
p.name.toLowerCase().includes(normalizedSearch)
),
[products, normalizedSearch]
)
return (
<>
<input value={search} onChange={e => setSearch(e.target.value)} />
{hasSearch && <ClearButton />}
{filtered.map(p => <ProductCard key={p.id} product={p} />)}
</>
)
}When to use vs a plain :
useMemoconst- Plain — boolean flags, string formatting, simple arithmetic, object property access,
constchecks. These are essentially free and.lengthoverhead is not worth it.useMemo - — filtering/sorting arrays, building data structures,
useMemo, expensive transformations, anything that iterates collections or involves O(n) work.JSON.parse
The rule: if the expression returns a primitive or is a single property access, skip . If it iterates or transforms data, wrap it.
useMemoReact Compiler note: If React Compiler is enabled, it auto-memoizes expressions and you can skip manualcalls.useMemo
影响程度:高 —— 消除一整类bug和不必要的状态。
存储可从现有状态或props计算得出的值会导致同步问题和额外的重渲染。改为在渲染时直接计算。
避免——易失步的冗余状态:
tsx
function ProductList({ products }: { products: Product[] }) {
const [search, setSearch] = useState('')
const [filtered, setFiltered] = useState(products)
useEffect(() => {
setFiltered(products.filter(p =>
p.name.toLowerCase().includes(search.toLowerCase())
))
}, [products, search])
return (
<>
<input value={search} onChange={e => setSearch(e.target.value)} />
{filtered.map(p => <ProductCard key={p.id} product={p} />)}
</>
)
}推荐——在渲染时计算(简单推导使用普通):
consttsx
function ProductList({ products }: { products: Product[] }) {
const [search, setSearch] = useState('')
// 简单推导——使用普通const,无需useMemo
const hasSearch = search.length > 0
const normalizedSearch = search.toLowerCase()
// 复杂推导——当处理大型数组时,使用useMemo是合理的
const filtered = useMemo(
() => products.filter(p =>
p.name.toLowerCase().includes(normalizedSearch)
),
[products, normalizedSearch]
)
return (
<>
<input value={search} onChange={e => setSearch(e.target.value)} />
{hasSearch && <ClearButton />}
{filtered.map(p => <ProductCard key={p.id} product={p} />)}
</>
)
}何时使用 vs 普通:
useMemoconst- 普通—— 布尔标志、字符串格式化、简单算术、对象属性访问、
const检查。这些操作几乎没有成本,使用.length的开销得不偿失。useMemo - —— 过滤/排序数组、构建数据结构、
useMemo、昂贵的转换操作、任何需要遍历集合或涉及O(n)复杂度的工作。JSON.parse
规则:如果表达式返回原始值或只是单一属性访问,跳过。如果需要遍历或转换数据,就用它包裹。
useMemoReact Compiler说明: 如果启用了React Compiler,它会自动记忆化表达式,你可以跳过手动调用。useMemo
2. Subscribe to Coarse-Grained State, Not Raw Values
2. 订阅粗粒度状态,而非原始值
Impact: HIGH — Prevents re-renders on irrelevant changes.
If your component only cares about a derived boolean (e.g., "is mobile?"), don't subscribe to the raw value that changes continuously.
Avoid — re-renders on every pixel:
tsx
function Sidebar() {
const width = useWindowWidth() // fires on every resize
const isMobile = width < 768
return <nav className={isMobile ? 'mobile' : 'desktop'}>...</nav>
}Prefer — re-renders only when the boolean flips:
tsx
function Sidebar() {
const isMobile = useMediaQuery('(max-width: 767px)')
return <nav className={isMobile ? 'mobile' : 'desktop'}>...</nav>
}This applies broadly: subscribe to rather than the entire user object, rather than the full cart array, etc.
isLoggedInhasItems影响程度:高 —— 防止无关变化导致的重渲染。
如果你的组件只关心派生的布尔值(如“是否为移动端?”),不要订阅持续变化的原始值。
避免——每次尺寸变化都重渲染:
tsx
function Sidebar() {
const width = useWindowWidth() // 每次窗口 resize 都会触发
const isMobile = width < 768
return <nav className={isMobile ? 'mobile' : 'desktop'}>...</nav>
}推荐——仅当布尔值切换时才重渲染:
tsx
function Sidebar() {
const isMobile = useMediaQuery('(max-width: 767px)')
return <nav className={isMobile ? 'mobile' : 'desktop'}>...</nav>
}这种方式适用于多种场景:订阅而非整个用户对象,订阅而非完整的购物车数组等。
isLoggedInhasItems3. Extract Expensive Subtrees into Memoized Components
3. 将昂贵的子树提取为记忆化组件
Impact: HIGH — Enables early returns and skip-rendering.
When a parent has fast paths (loading, error, empty), expensive children still compute if they live in the same component. Extract them so React can skip their render entirely.
Avoid — avatar computation runs even during loading:
tsx
function Profile({ user, loading }: Props) {
const avatar = useMemo(() => processAvatar(user), [user])
if (loading) return <Skeleton />
return <div><img src={avatar} /></div>
}Prefer — computation skipped when loading:
tsx
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const avatar = useMemo(() => processAvatar(user), [user])
return <img src={avatar} />
})
function Profile({ user, loading }: Props) {
if (loading) return <Skeleton />
return <div><UserAvatar user={user} /></div>
}React Compiler note: The compiler auto-memoizes, making manualwrapping less necessary. But extracting components for early returns is still valuable.memo()
影响程度:高 —— 支持提前返回并跳过渲染。
当父组件有快速路径(加载中、错误、空状态)时,如果昂贵的子组件和父组件在同一组件内,它们仍会执行计算。将它们提取出来,这样React可以完全跳过它们的渲染。
避免——即使在加载中也会执行头像计算:
tsx
function Profile({ user, loading }: Props) {
const avatar = useMemo(() => processAvatar(user), [user])
if (loading) return <Skeleton />
return <div><img src={avatar} /></div>
}推荐——加载时跳过计算:
tsx
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const avatar = useMemo(() => processAvatar(user), [user])
return <img src={avatar} />
})
function Profile({ user, loading }: Props) {
if (loading) return <Skeleton />
return <div><UserAvatar user={user} /></div>
}React Compiler说明: 编译器会自动记忆化,减少手动使用包裹的需求。但为了提前返回而提取组件仍然很有价值。memo()
4. Use Lazy State Initialization
4. 使用惰性状态初始化
Impact: MEDIUM — Avoids wasted computation on every render.
When receives a function call as its initial value, that call executes on every render even though the result is only used once. Pass a function reference instead.
useStateAvoid — runs every render:
buildIndex()tsx
const [index, setIndex] = useState(buildSearchIndex(items))Prefer — runs only on mount:
tsx
const [index, setIndex] = useState(() => buildSearchIndex(items))Use lazy init for: , reads, building data structures, heavy transformations. Skip it for simple primitives like or .
JSON.parselocalStorageuseState(0)useState(false)影响程度:中 —— 避免每次渲染都执行无用计算。
当接收一个函数调用作为初始值时,该函数会在每次渲染时执行,尽管结果只会被使用一次。改为传递函数引用。
useState避免——在每次渲染时都运行:
buildIndex()tsx
const [index, setIndex] = useState(buildSearchIndex(items))推荐——仅在挂载时运行:
tsx
const [index, setIndex] = useState(() => buildSearchIndex(items))对以下操作使用惰性初始化:、读取、构建数据结构、重型转换。对于简单的原始值如或,则无需使用。
JSON.parselocalStorageuseState(0)useState(false)5. Use Functional setState for Stable Callbacks
5. 使用函数式setState获取稳定回调
Impact: MEDIUM — Removes state variables from dependency arrays.
When a callback only needs the previous state to compute the next state, use the functional form. This eliminates the state variable from the dependency array and produces a stable callback identity.
Avoid — callback changes when changes:
counttsx
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(count + 1), [count])Prefer — callback is always stable:
tsx
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(c => c + 1), [])影响程度:中 —— 从依赖数组中移除状态变量。
当回调仅需要前一个状态来计算下一个状态时,使用函数式形式。这会将状态变量从依赖数组中移除,并生成稳定的回调标识。
避免——当变化时回调也变化:
counttsx
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(count + 1), [count])推荐——回调始终稳定:
tsx
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(c => c + 1), [])6. Put Interaction Logic in Event Handlers, Not Effects
6. 将交互逻辑放在事件处理程序中,而非Effects
Impact: MEDIUM — Avoids re-running side effects on dependency changes.
If a side effect is triggered by a user action (click, submit, drag), run it in the event handler. Modeling it as state + effect causes re-runs when unrelated dependencies change.
Avoid — effect re-runs when changes:
themetsx
function Form() {
const [submitted, setSubmitted] = useState(false)
const theme = useContext(ThemeContext)
useEffect(() => {
if (submitted) {
post('/api/register')
showToast('Registered', theme)
}
}, [submitted, theme])
return <button onClick={() => setSubmitted(true)}>Submit</button>
}Prefer — logic in the handler:
tsx
function Form() {
const theme = useContext(ThemeContext)
function handleSubmit() {
post('/api/register')
showToast('Registered', theme)
}
return <button onClick={handleSubmit}>Submit</button>
}影响程度:中 —— 避免依赖变化导致副作用重复执行。
如果副作用是由用户操作(点击、提交、拖拽)触发的,直接在事件处理程序中运行它。将其建模为状态+副作用会导致无关依赖变化时触发重渲染。
避免——当变化时副作用重复执行:
themetsx
function Form() {
const [submitted, setSubmitted] = useState(false)
const theme = useContext(ThemeContext)
useEffect(() => {
if (submitted) {
post('/api/register')
showToast('Registered', theme)
}
}, [submitted, theme])
return <button onClick={() => setSubmitted(true)}>Submit</button>
}推荐——逻辑放在处理程序中:
tsx
function Form() {
const theme = useContext(ThemeContext)
function handleSubmit() {
post('/api/register')
showToast('Registered', theme)
}
return <button onClick={handleSubmit}>Submit</button>
}7. Use useRef
for Transient, High-Frequency Values
useRef7. 使用useRef
存储瞬态、高频变化的值
useRefImpact: MEDIUM — Prevents re-renders on rapid updates.
Values that change very frequently (mouse position, scroll offset, interval ticks) but don't need to drive re-renders should live in a ref. Update the DOM directly when needed.
Avoid — re-renders on every mouse move:
tsx
function Cursor() {
const [x, setX] = useState(0)
useEffect(() => {
const handler = (e: MouseEvent) => setX(e.clientX)
window.addEventListener('mousemove', handler)
return () => window.removeEventListener('mousemove', handler)
}, [])
return <div style={{ transform: `translateX(${x}px)` }} />
}Prefer — zero re-renders:
tsx
function Cursor() {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const handler = (e: MouseEvent) => {
if (ref.current) {
ref.current.style.transform = `translateX(${e.clientX}px)`
}
}
window.addEventListener('mousemove', handler)
return () => window.removeEventListener('mousemove', handler)
}, [])
return <div ref={ref} />
}影响程度:中 —— 防止快速更新导致的重渲染。
变化非常频繁的值(如鼠标位置、滚动偏移、定时器刻度)但不需要驱动重渲染的,应该存储在ref中。必要时直接更新DOM。
避免——每次鼠标移动都重渲染:
tsx
function Cursor() {
const [x, setX] = useState(0)
useEffect(() => {
const handler = (e: MouseEvent) => setX(e.clientX)
window.addEventListener('mousemove', handler)
return () => window.removeEventListener('mousemove', handler)
}, [])
return <div style={{ transform: `translateX(${x}px)` }} />
}推荐——零重渲染:
tsx
function Cursor() {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const handler = (e: MouseEvent) => {
if (ref.current) {
ref.current.style.transform = `translateX(${e.clientX}px)`
}
}
window.addEventListener('mousemove', handler)
return () => window.removeEventListener('mousemove', handler)
}, [])
return <div ref={ref} />
}8. Use startTransition
for Non-Urgent Updates
startTransition8. 使用startTransition
处理非紧急更新
startTransitionImpact: MEDIUM — Keeps high-priority updates (typing, clicking) responsive.
Wrap non-urgent state updates in so React can interrupt them for urgent work. This is especially useful for search filtering, tab switching, and list re-sorting.
startTransitionAvoid — typing blocks while list filters:
tsx
function Search({ items }: { items: Item[] }) {
const [query, setQuery] = useState('')
const [filtered, setFiltered] = useState(items)
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value)
setFiltered(items.filter(i => i.name.includes(e.target.value)))
}
return (
<>
<input value={query} onChange={handleChange} />
<List items={filtered} />
</>
)
}Prefer — input stays responsive:
tsx
import { useState, useTransition } from 'react'
function Search({ items }: { items: Item[] }) {
const [query, setQuery] = useState('')
const [filtered, setFiltered] = useState(items)
const [isPending, startTransition] = useTransition()
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value)
startTransition(() => {
setFiltered(items.filter(i => i.name.includes(e.target.value)))
})
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<List items={filtered} />
</>
)
}影响程度:中 —— 保持高优先级更新(输入、点击)的响应性。
将非紧急状态更新包裹在中,这样React可以中断它们以处理紧急工作。这在搜索过滤、标签切换和列表重新排序时尤其有用。
startTransition避免——输入时列表过滤阻塞UI:
tsx
function Search({ items }: { items: Item[] }) {
const [query, setQuery] = useState('')
const [filtered, setFiltered] = useState(items)
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value)
setFiltered(items.filter(i => i.name.includes(e.target.value)))
}
return (
<>
<input value={query} onChange={handleChange} />
<List items={filtered} />
</>
)
}推荐——输入保持响应性:
tsx
import { useState, useTransition } from 'react'
function Search({ items }: { items: Item[] }) {
const [query, setQuery] = useState('')
const [filtered, setFiltered] = useState(items)
const [isPending, startTransition] = useTransition()
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value)
startTransition(() => {
setFiltered(items.filter(i => i.name.includes(e.target.value)))
})
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<List items={filtered} />
</>
)
}9. Defer State Reads to the Point of Use
9. 延迟状态读取到使用点
Impact: MEDIUM — Avoids subscriptions to state you only read in callbacks.
Don't call hooks like if you only read the value inside an event handler. Read it on demand instead.
useSearchParams()Avoid — component re-renders on every URL change:
tsx
function ShareButton({ id }: { id: string }) {
const [searchParams] = useSearchParams()
const handleShare = () => {
const ref = searchParams.get('ref')
share(id, { ref })
}
return <button onClick={handleShare}>Share</button>
}Prefer — reads on demand, no subscription:
tsx
function ShareButton({ id }: { id: string }) {
const handleShare = () => {
const params = new URLSearchParams(window.location.search)
share(id, { ref: params.get('ref') })
}
return <button onClick={handleShare}>Share</button>
}影响程度:中 —— 避免订阅仅在回调中读取的状态。
如果仅在事件处理程序中读取值,不要调用这样的hooks。改为按需读取。
useSearchParams()避免——每次URL变化组件都重渲染:
tsx
function ShareButton({ id }: { id: string }) {
const [searchParams] = useSearchParams()
const handleShare = () => {
const ref = searchParams.get('ref')
share(id, { ref })
}
return <button onClick={handleShare}>Share</button>
}推荐——按需读取,无订阅:
tsx
function ShareButton({ id }: { id: string }) {
const handleShare = () => {
const params = new URLSearchParams(window.location.search)
share(id, { ref: params.get('ref') })
}
return <button onClick={handleShare}>Share</button>
}10. Use Stable References for Default Props
10. 为默认Props使用稳定引用
Impact: MEDIUM — Prevents from being defeated by new object/array literals.
memo()Passing or as default prop values creates new references every render, defeating memoization on child components.
[]{}Avoid — new array each render:
tsx
function Dashboard({ tabs = [] }: { tabs?: Tab[] }) {
return <TabBar tabs={tabs} /> {/* TabBar re-renders every time */}
}Prefer — stable reference:
tsx
const EMPTY_TABS: Tab[] = []
function Dashboard({ tabs = EMPTY_TABS }: { tabs?: Tab[] }) {
return <TabBar tabs={tabs} />
}影响程度:中 —— 防止因新的对象/数组字面量失效。
memo()传递或作为默认prop值会在每次渲染时创建新的引用,导致子组件的记忆化失效。
[]{}避免——每次渲染都创建新数组:
tsx
function Dashboard({ tabs = [] }: { tabs?: Tab[] }) {
return <TabBar tabs={tabs} /> {/* TabBar每次都重渲染 */}
}推荐——稳定引用:
tsx
const EMPTY_TABS: Tab[] = []
function Dashboard({ tabs = EMPTY_TABS }: { tabs?: Tab[] }) {
return <TabBar tabs={tabs} />
}11. CSS content-visibility
for Long Lists
content-visibility11. 为长列表使用CSS content-visibility
content-visibilityImpact: HIGH — 5-10x faster initial render for long scrollable content.
Apply to off-screen items so the browser skips their layout and paint until they scroll into view.
content-visibility: autocss
.list-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px; /* estimated height */
}tsx
function MessageList({ messages }: { messages: Message[] }) {
return (
<div style={{ overflowY: 'auto', height: '100vh' }}>
{messages.map(msg => (
<div key={msg.id} className="list-item">
<MessageCard message={msg} />
</div>
))}
</div>
)
}For 1,000 items, the browser skips layout and paint for ~990 off-screen items. Combine with virtualization (e.g., , ) for truly massive lists.
react-window@tanstack/react-virtual影响程度:高 —— 长滚动内容的初始渲染速度提升5-10倍。
对屏幕外的元素应用,这样浏览器会跳过它们的布局和绘制,直到它们滚动到视图中。
content-visibility: autocss
.list-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px; /* 预估高度 */
}tsx
function MessageList({ messages }: { messages: Message[] }) {
return (
<div style={{ overflowY: 'auto', height: '100vh' }}>
{messages.map(msg => (
<div key={msg.id} className="list-item">
<MessageCard message={msg} />
</div>
))}
</div>
)
}对于1000个元素,浏览器会跳过约990个屏幕外元素的布局和绘制。结合虚拟化(如、)处理超大规模列表。
react-window@tanstack/react-virtual12. Hoist Static JSX Outside Components
12. 将静态JSX提升到组件外部
Impact: LOW — Avoids re-creating identical elements.
JSX elements that never change can be lifted to module scope. React reuses the same object reference across renders.
Avoid — recreated every render:
tsx
function Page() {
return (
<main>
<footer>
<p>Copyright 2026 Acme Inc.</p>
</footer>
</main>
)
}Prefer — created once:
tsx
const footer = (
<footer>
<p>Copyright 2026 Acme Inc.</p>
</footer>
)
function Page() {
return <main>{footer}</main>
}Most impactful for large SVG elements which are expensive to recreate.
React Compiler note: The compiler auto-hoists static JSX, making this manual optimization unnecessary.
影响程度:低 —— 避免重复创建相同的元素。
永远不会变化的JSX元素可以提升到模块作用域。React会在渲染之间重用相同的对象引用。
避免——每次渲染都重新创建:
tsx
function Page() {
return (
<main>
<footer>
<p>Copyright 2026 Acme Inc.</p>
</footer>
</main>
)
}推荐——仅创建一次:
tsx
const footer = (
<footer>
<p>Copyright 2026 Acme Inc.</p>
</footer>
)
function Page() {
return <main>{footer}</main>
}对于大型SVG元素,这种优化的影响最大,因为它们的创建成本很高。
React Compiler说明: 编译器会自动提升静态JSX,使得手动优化不再必要。
13. Initialize Expensive Operations Once Per App
13. 每个应用仅初始化一次昂贵操作
Impact: LOW-MEDIUM — Avoids duplicate init in Strict Mode and remounts.
App-wide initialization (analytics, auth checks, service workers) should not live in — components remount in development and in concurrent features. Use a module-level guard.
useEffectAvoid — runs twice in dev, again on remount:
tsx
function App() {
useEffect(() => {
initAnalytics()
checkAuth()
}, [])
return <Router />
}Prefer — once per app load:
tsx
let initialized = false
function App() {
useEffect(() => {
if (initialized) return
initialized = true
initAnalytics()
checkAuth()
}, [])
return <Router />
}Or initialize at the module level in your entry file (), outside any component.
main.tsx影响程度:低-中 —— 避免在严格模式和重新挂载时重复初始化。
应用级别的初始化(如分析、权限检查、Service Worker)不应放在中——在开发环境和并发特性中,组件会重新挂载。使用模块级别的守卫。
useEffect避免——在开发环境中运行两次,重新挂载时再次运行:
tsx
function App() {
useEffect(() => {
initAnalytics()
checkAuth()
}, [])
return <Router />
}推荐——每个应用加载仅运行一次:
tsx
let initialized = false
function App() {
useEffect(() => {
if (initialized) return
initialized = true
initAnalytics()
checkAuth()
}, [])
return <Router />
}或者在入口文件()的模块级别初始化,放在任何组件外部。
main.tsx14. Store Event Handlers in Refs for Stable Subscriptions
14. 将事件处理程序存储在Ref中以获得稳定订阅
Impact: LOW — Prevents effect re-subscriptions.
When a custom hook subscribes to an event and accepts a callback, store the callback in a ref so the subscription doesn't tear down and recreate on every render.
Avoid — re-subscribes when handler changes:
tsx
function useWindowEvent(event: string, handler: (e: Event) => void) {
useEffect(() => {
window.addEventListener(event, handler)
return () => window.removeEventListener(event, handler)
}, [event, handler])
}Prefer — stable subscription:
tsx
function useWindowEvent(event: string, handler: (e: Event) => void) {
const saved = useRef(handler)
useEffect(() => { saved.current = handler }, [handler])
useEffect(() => {
const listener = (e: Event) => saved.current(e)
window.addEventListener(event, listener)
return () => window.removeEventListener(event, listener)
}, [event])
}If using React 19+, provides this pattern as a built-in:
useEffectEventtsx
import { useEffectEvent } from 'react'
function useWindowEvent(event: string, handler: (e: Event) => void) {
const onEvent = useEffectEvent(handler)
useEffect(() => {
window.addEventListener(event, onEvent)
return () => window.removeEventListener(event, onEvent)
}, [event])
}影响程度:低 —— 防止副作用重新订阅。
当自定义hook订阅事件并接受回调时,将回调存储在ref中,这样订阅就不会在每次渲染时销毁并重新创建。
避免——当处理程序变化时重新订阅:
tsx
function useWindowEvent(event: string, handler: (e: Event) => void) {
useEffect(() => {
window.addEventListener(event, handler)
return () => window.removeEventListener(event, handler)
}, [event, handler])
}推荐——稳定订阅:
tsx
function useWindowEvent(event: string, handler: (e: Event) => void) {
const saved = useRef(handler)
useEffect(() => { saved.current = handler }, [handler])
useEffect(() => {
const listener = (e: Event) => saved.current(e)
window.addEventListener(event, listener)
return () => window.removeEventListener(event, listener)
}, [event])
}如果使用React 19+,内置了这种模式:
useEffectEventtsx
import { useEffectEvent } from 'react'
function useWindowEvent(event: string, handler: (e: Event) => void) {
const onEvent = useEffectEvent(handler)
useEffect(() => {
window.addEventListener(event, onEvent)
return () => window.removeEventListener(event, onEvent)
}, [event])
}15. Prevent Hydration Flicker for Client-Only Data
15. 防止仅客户端数据导致的水合闪烁
Impact: MEDIUM — Eliminates flash of wrong content during SSR hydration.
When rendering depends on client-only data (localStorage, cookies), an inline script can set the correct value before React hydrates — avoiding both SSR errors and a visible flash.
tsx
function ThemeRoot({ children }: { children: React.ReactNode }) {
return (
<>
<div id="app-root">{children}</div>
<script
dangerouslySetInnerHTML={{
__html: `(function(){
try {
var t = localStorage.getItem('theme') || 'light';
document.getElementById('app-root').dataset.theme = t;
} catch(e) {}
})();`,
}}
/>
</>
)
}This approach works in any SSR setup — Next.js, Remix, or a custom Vite SSR pipeline.
影响程度:中 —— 消除SSR水合过程中错误内容的闪烁。
当渲染依赖仅客户端数据(如localStorage、cookies)时,内联脚本可以在React水合前设置正确的值——避免SSR错误和可见的闪烁。
tsx
function ThemeRoot({ children }: { children: React.ReactNode }) {
return (
<>
<div id="app-root">{children}</div>
<script
dangerouslySetInnerHTML={{
__html: `(function(){
try {
var t = localStorage.getItem('theme') || 'light';
document.getElementById('app-root').dataset.theme = t;
} catch(e) {}
})();`,
}}
/>
</>
)
}这种方法适用于任何SSR设置——Next.js、Remix或自定义Vite SSR流水线。
16. Never Define Components Inside Components
16. 永远不要在组件内部定义组件
Impact: HIGH — Causes remounting, state loss, and wasted DOM work every render.
When you define a component inside another component's render, React creates a new component type on every render. This means the entire subtree unmounts and remounts — losing all state, DOM nodes, and effect cleanup/setup.
Avoid — is a new type every render:
Rowtsx
function Table({ data }: { data: Item[] }) {
// This creates a NEW component type on every render
function Row({ item }: { item: Item }) {
const [selected, setSelected] = useState(false)
return <tr onClick={() => setSelected(!selected)}>{item.name}</tr>
}
return <table>{data.map(item => <Row key={item.id} item={item} />)}</table>
}Prefer — defined at module scope:
Rowtsx
function Row({ item }: { item: Item }) {
const [selected, setSelected] = useState(false)
return <tr onClick={() => setSelected(!selected)}>{item.name}</tr>
}
function Table({ data }: { data: Item[] }) {
return <table>{data.map(item => <Row key={item.id} item={item} />)}</table>
}This also applies to components defined inside , , or any other hook. Always define components at module scope or as static properties.
useMemouseCallback影响程度:高 —— 导致每次渲染时重新挂载、状态丢失和无效的DOM操作。
当你在另一个组件的渲染函数内部定义组件时,React会在每次渲染时创建一个新的组件类型。这意味着整个子树会卸载并重新挂载——丢失所有状态、DOM节点和副作用的清理/设置。
避免——每次渲染都是新类型:
Rowtsx
function Table({ data }: { data: Item[] }) {
// 每次渲染都会创建一个新的组件类型
function Row({ item }: { item: Item }) {
const [selected, setSelected] = useState(false)
return <tr onClick={() => setSelected(!selected)}>{item.name}</tr>
}
return <table>{data.map(item => <Row key={item.id} item={item} />)}</table>
}推荐——定义在模块作用域:
Rowtsx
function Row({ item }: { item: Item }) {
const [selected, setSelected] = useState(false)
return <tr onClick={() => setSelected(!selected)}>{item.name}</tr>
}
function Table({ data }: { data: Item[] }) {
return <table>{data.map(item => <Row key={item.id} item={item} />)}</table>
}这也适用于在、或任何其他hook内部定义的组件。始终在模块作用域或作为静态属性定义组件。
useMemouseCallback17. Use useDeferredValue
for Expensive Derived Renders
useDeferredValue17. 使用useDeferredValue
处理昂贵的派生渲染
useDeferredValueImpact: HIGH — Keeps the UI responsive while expensive subtrees re-render in the background.
useDeferredValueuseTransitionuseDeferredValueAvoid — every keystroke blocks the UI:
tsx
function SearchPage({ query }: { query: string }) {
// Expensive: filters and renders 10,000 items on every keystroke
const results = filterItems(query)
return <ResultsList items={results} />
}Prefer — input stays responsive, results update in background:
tsx
import { useDeferredValue, useMemo } from 'react'
function SearchPage({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query)
const isStale = query !== deferredQuery
const results = useMemo(() => filterItems(deferredQuery), [deferredQuery])
return (
<div style={{ opacity: isStale ? 0.7 : 1 }}>
<ResultsList items={results} />
</div>
)
}When to use vs :
useDeferredValueuseTransition- — you control the state setter and can wrap it in
useTransitionstartTransition - — the value comes from props, a parent, or a library you don't control
useDeferredValue
影响程度:高 —— 在后台重新渲染昂贵子树时保持UI响应。
useDeferredValueuseTransitionuseDeferredValue避免——每次按键都阻塞UI:
tsx
function SearchPage({ query }: { query: string }) {
// 昂贵操作:每次按键都过滤并渲染10000个项目
const results = filterItems(query)
return <ResultsList items={results} />
}推荐——输入保持响应,结果在后台更新:
tsx
import { useDeferredValue, useMemo } from 'react'
function SearchPage({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query)
const isStale = query !== deferredQuery
const results = useMemo(() => filterItems(deferredQuery), [deferredQuery])
return (
<div style={{ opacity: isStale ? 0.7 : 1 }}>
<ResultsList items={results} />
</div>
)
}何时使用 vs :
useDeferredValueuseTransition- —— 你控制状态设置器,可以用
useTransition包裹它startTransition - —— 值来自props、父组件或你无法控制的库
useDeferredValue
18. Use Explicit Checks in Conditional Rendering
18. 在条件渲染中使用显式检查
Impact: MEDIUM — Prevents rendering , , or empty strings to the DOM.
0NaNThe operator in JSX short-circuits on falsy values — but , , and are falsy yet still render as visible text nodes.
&&0NaN""Avoid — renders to the DOM when count is zero:
0tsx
function NotificationBadge({ count }: { count: number }) {
return <div>{count && <Badge>{count}</Badge>}</div>
// When count is 0, renders: <div>0</div>
}Prefer — explicit boolean check:
tsx
function NotificationBadge({ count }: { count: number }) {
return <div>{count > 0 && <Badge>{count}</Badge>}</div>
}
// Or use a ternary for clarity
function NotificationBadge({ count }: { count: number }) {
return <div>{count > 0 ? <Badge>{count}</Badge> : null}</div>
}This applies to any value that might be , , or — array lengths, string values, numeric props. Always use an explicit boolean expression (, , ) rather than relying on truthiness.
0NaN""> 0!== ''!= null影响程度:中 —— 防止将、或空字符串渲染到DOM中。
0NaNJSX中的运算符在遇到假值时短路——但、和是假值,但仍会渲染为可见的文本节点。
&&0NaN""避免——当count为0时渲染到DOM:
0tsx
function NotificationBadge({ count }: { count: number }) {
return <div>{count && <Badge>{count}</Badge>}</div>
// 当count为0时,渲染为:<div>0</div>
}推荐——显式布尔检查:
tsx
function NotificationBadge({ count }: { count: number }) {
return <div>{count > 0 && <Badge>{count}</Badge>}</div>
}
// 或者使用三元表达式以提高清晰度
function NotificationBadge({ count }: { count: number }) {
return <div>{count > 0 ? <Badge>{count}</Badge> : null}</div>
}这适用于任何可能为、或的值——数组长度、字符串值、数值props。始终使用显式布尔表达式(、、),而非依赖真值判断。
0NaN""> 0!== ''!= null19. Narrow Effect Dependencies to Primitives
19. 将Effect依赖项缩小为原始值
Impact: MEDIUM — Prevents effects from re-running when unrelated object properties change.
When an effect only needs one property from an object, extract it before the dependency array. Passing the whole object causes re-runs whenever any property changes.
Avoid — effect re-runs when or changes:
user.nameuser.avatartsx
function UserStatus({ user }: { user: User }) {
useEffect(() => {
updatePresence(user.id)
}, [user]) // re-runs on ANY user property change
}Prefer — only re-runs when the ID changes:
tsx
function UserStatus({ user }: { user: User }) {
const { id } = user
useEffect(() => {
updatePresence(id)
}, [id])
}This also applies to hook return values. If returns and your effect only cares about , destructure first.
useQuery{ data, status, fetchStatus }status影响程度:中 —— 防止无关对象属性变化导致Effect重新执行。
当Effect只需要对象的一个属性时,在依赖数组之前提取它。传递整个对象会导致任何属性变化时都重新执行Effect。
避免——当或变化时Effect重新执行:
user.nameuser.avatartsx
function UserStatus({ user }: { user: User }) {
useEffect(() => {
updatePresence(user.id)
}, [user]) // 任何user属性变化都会重新执行
}推荐——仅当ID变化时才重新执行:
tsx
function UserStatus({ user }: { user: User }) {
const { id } = user
useEffect(() => {
updatePresence(id)
}, [id])
}这也适用于hook的返回值。如果返回而你的Effect只关心,先解构它。
useQuery{ data, status, fetchStatus }status20. Split Combined Hook Computations
20. 拆分组合式Hook的计算
Impact: MEDIUM — Prevents re-renders for consumers that only need part of a hook's output.
When a custom hook computes multiple unrelated values, a change in one forces re-renders in all consumers — even those that only read the unchanged value.
Avoid — changing re-renders components that only need :
totalaveragetsx
function useStats(items: number[]) {
return useMemo(() => ({
total: items.reduce((a, b) => a + b, 0),
average: items.reduce((a, b) => a + b, 0) / items.length,
max: Math.max(...items),
}), [items])
}Prefer — split into focused hooks:
tsx
function useTotal(items: number[]) {
return useMemo(() => items.reduce((a, b) => a + b, 0), [items])
}
function useAverage(items: number[]) {
return useMemo(() => items.reduce((a, b) => a + b, 0) / items.length, [items])
}
function useMax(items: number[]) {
return useMemo(() => Math.max(...items), [items])
}Components call only the hook they need. If a single component needs all three, combining them there is fine — the split prevents unnecessary coupling at the hook level.
影响程度:中 —— 防止仅需要部分hook输出的消费者重渲染。
当自定义hook计算多个不相关的值时,其中一个值的变化会导致所有消费者重渲染——即使有些消费者只读取未变化的值。
避免——变化会导致仅需要的组件重渲染:
totalaveragetsx
function useStats(items: number[]) {
return useMemo(() => ({
total: items.reduce((a, b) => a + b, 0),
average: items.reduce((a, b) => a + b, 0) / items.length,
max: Math.max(...items),
}), [items])
}推荐——拆分为聚焦的hooks:
tsx
function useTotal(items: number[]) {
return useMemo(() => items.reduce((a, b) => a + b, 0), [items])
}
function useAverage(items: number[]) {
return useMemo(() => items.reduce((a, b) => a + b, 0) / items.length, [items])
}
function useMax(items: number[]) {
return useMemo(() => Math.max(...items), [items])
}组件只调用它们需要的hook。如果单个组件需要所有三个,在组件内部组合它们即可——拆分避免了hook层面的不必要耦合。
21. Avoid Layout Thrashing with Batched DOM Reads/Writes
21. 使用批量DOM读取/写入避免布局抖动
Impact: HIGH — Prevents forced synchronous layouts that block the main thread.
Reading a layout property (e.g., , ) after writing to the DOM forces the browser to recalculate layout synchronously. In a loop, this creates layout thrashing.
offsetHeightgetBoundingClientRect()Avoid — forces layout recalculation on every iteration:
tsx
function resizeCards(cards: HTMLElement[]) {
cards.forEach(card => {
const height = card.offsetHeight // READ (forces layout)
card.style.minHeight = `${height + 20}px` // WRITE (invalidates layout)
})
}Prefer — batch all reads, then all writes:
tsx
function resizeCards(cards: HTMLElement[]) {
// Read phase
const heights = cards.map(card => card.offsetHeight)
// Write phase
cards.forEach((card, i) => {
card.style.minHeight = `${heights[i] + 20}px`
})
}In React, this most commonly occurs in or callbacks that measure and mutate DOM elements. When you need to read layout inside an animation frame, use to batch:
useLayoutEffectuseEffectrequestAnimationFrametsx
useLayoutEffect(() => {
const measurements = items.map(el => el.getBoundingClientRect())
requestAnimationFrame(() => {
items.forEach((el, i) => {
el.style.transform = `translateY(${measurements[i].top}px)`
})
})
}, [items])影响程度:高 —— 防止阻塞主线程的强制同步布局。
在写入DOM后读取布局属性(如、)会迫使浏览器同步重新计算布局。在循环中这样做会导致布局抖动。
offsetHeightgetBoundingClientRect()避免——每次迭代都强制重新计算布局:
tsx
function resizeCards(cards: HTMLElement[]) {
cards.forEach(card => {
const height = card.offsetHeight // 读取(强制布局)
card.style.minHeight = `${height + 20}px` // 写入(使布局失效)
})
}推荐——批量所有读取,然后批量所有写入:
tsx
function resizeCards(cards: HTMLElement[]) {
// 读取阶段
const heights = cards.map(card => card.offsetHeight)
// 写入阶段
cards.forEach((card, i) => {
card.style.minHeight = `${heights[i] + 20}px`
})
}在React中,这最常发生在或回调中,这些回调会测量并修改DOM元素。当你需要在动画帧内读取布局时,使用进行批量处理:
useLayoutEffectuseEffectrequestAnimationFrametsx
useLayoutEffect(() => {
const measurements = items.map(el => el.getBoundingClientRect())
requestAnimationFrame(() => {
items.forEach((el, i) => {
el.style.transform = `translateY(${measurements[i].top}px)`
})
})
}, [items])22. Animate SVG Wrappers, Not SVG Elements Directly
22. 为SVG包装器设置动画,而非直接为SVG元素设置
Impact: MEDIUM — Avoids repainting the entire SVG on every animation frame.
Animating properties on an SVG element itself (e.g., or ) triggers a full SVG repaint. Wrap the SVG in a and animate the wrapper instead.
<svg><path><div>Avoid — repaints entire SVG tree:
tsx
<motion.svg animate={{ rotate: 360 }} style={{ width: 200, height: 200 }}>
<ComplexChart />
</motion.svg>Prefer — only the wrapper repaints:
tsx
<motion.div animate={{ rotate: 360 }} style={{ width: 200, height: 200 }}>
<svg viewBox="0 0 200 200">
<ComplexChart />
</svg>
</motion.div>This also applies to CSS animations. Use on a wrapper element rather than animating SVG attributes like , , or directly.
transformcxcyd影响程度:中 —— 避免在每一动画帧重绘整个SVG。
直接在SVG元素(如或)上设置动画属性会触发整个SVG的重绘。将SVG包裹在中,然后为包装器设置动画。
<svg><path><div>避免——重绘整个SVG树:
tsx
<motion.svg animate={{ rotate: 360 }} style={{ width: 200, height: 200 }}>
<ComplexChart />
</motion.svg>推荐——仅重绘包装器:
tsx
<motion.div animate={{ rotate: 360 }} style={{ width: 200, height: 200 }}>
<svg viewBox="0 0 200 200">
<ComplexChart />
</svg>
</motion.div>这也适用于CSS动画。在包装器元素上使用,而非直接为SVG属性如、或设置动画。
transformcxcyd23. Suppress Expected Hydration Mismatches
23. 抑制预期的水合不匹配
Impact: LOW-MEDIUM — Silences known-safe warnings without hiding real bugs.
Some content is intentionally different between server and client — timestamps, random IDs, user-agent-specific rendering. Use on those specific elements.
suppressHydrationWarningtsx
function Comment({ createdAt }: { createdAt: Date }) {
return (
<article>
<p>{comment.body}</p>
<time suppressHydrationWarning>
{formatRelativeTime(createdAt)} {/* "2 minutes ago" differs server vs client */}
</time>
</article>
)
}Apply sparingly and only on leaf elements. Never suppress warnings on container elements — it masks real mismatches in children.
影响程度:低-中 —— 静默已知安全的警告,同时不隐藏真实的bug。
某些内容在服务器和客户端之间故意不同——如时间戳、随机ID、特定用户代理的渲染。在这些特定元素上使用。
suppressHydrationWarningtsx
function Comment({ createdAt }: { createdAt: Date }) {
return (
<article>
<p>{comment.body}</p>
<time suppressHydrationWarning>
{formatRelativeTime(createdAt)} {/* "2分钟前"在服务器和客户端不同 */}
</time>
</article>
)
}谨慎使用,仅在叶子元素上应用。永远不要在容器元素上抑制警告——这会掩盖子元素中真实的不匹配。
24. React DOM Resource Hints for Vite SPAs
24. 为Vite单页应用使用React DOM资源提示
Impact: HIGH — Lets the browser start loading critical resources earlier without framework support.
React 19 adds and from — imperative resource hints that work in any React app. In Vite SPAs (which don't get framework-level prefetching), these are especially valuable.
preload()preinit()react-domtsx
import { preload, preinit } from 'react-dom'
function App() {
// Preload a font before it's needed
preload('/fonts/inter-var.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' })
// Preinit a critical CSS file (loads + applies it)
preinit('/critical.css', { as: 'style' })
return <RouterProvider router={router} />
}On navigation — preload the next page's data and code:
tsx
function ProductLink({ id }: { id: string }) {
const handleHover = () => {
// Preload the image the next page will need
preload(`/api/products/${id}/image.webp`, { as: 'image' })
// Prefetch the route code
import('./pages/ProductDetail')
}
return <Link to={`/products/${id}`} onMouseEnter={handleHover}>View</Link>
}These are no-ops if the resource is already loaded, so calling them eagerly is safe. For Vite apps without a meta-framework, this is the primary mechanism for resource prioritization.
影响程度:高 —— 让浏览器在没有框架支持的情况下更早开始加载关键资源。
React 19从中新增了和——命令式资源提示,适用于任何React应用。在Vite单页应用(没有框架级别的预取支持)中,这些功能尤其有价值。
react-dompreload()preinit()tsx
import { preload, preinit } from 'react-dom'
function App() {
// 在需要之前预加载字体
preload('/fonts/inter-var.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' })
// 预初始化关键CSS文件(加载并应用)
preinit('/critical.css', { as: 'style' })
return <RouterProvider router={router} />
}在导航时——预加载下一页的数据和代码:
tsx
function ProductLink({ id }: { id: string }) {
const handleHover = () => {
// 预加载下一页需要的图片
preload(`/api/products/${id}/image.webp`, { as: 'image' })
// 预取路由代码
import('./pages/ProductDetail')
}
return <Link to={`/products/${id}`} onMouseEnter={handleHover}>查看</Link>
}如果资源已加载,这些操作是无操作的,因此提前调用是安全的。对于没有元框架的Vite应用,这是资源优先级排序的主要机制。
25. Use useTransition
for Route Navigation
useTransition25. 为路由导航使用useTransition
useTransitionImpact: MEDIUM — Keeps the current page interactive while the next route loads.
In Vite SPAs with routes, clicking a navigation link can freeze the UI while the chunk loads and the component renders. Wrapping navigation in lets React show the old page until the new one is ready.
React.lazy()startTransitiontsx
import { useTransition } from 'react'
import { useNavigate } from 'react-router-dom'
function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
const navigate = useNavigate()
const [isPending, startTransition] = useTransition()
const handleClick = (e: React.MouseEvent) => {
e.preventDefault()
startTransition(() => {
navigate(to)
})
}
return (
<a
href={to}
onClick={handleClick}
style={{ opacity: isPending ? 0.7 : 1 }}
>
{children}
</a>
)
}This prevents the blank-screen flash between lazy-loaded routes and gives you to show a subtle loading indicator on the current page.
isPending影响程度:中 —— 在加载下一个路由时保持当前页面的交互性。
在使用路由的Vite单页应用中,点击导航链接可能会在加载代码块和渲染组件时冻结UI。将导航包裹在中,让React在新页面准备好之前显示旧页面。
React.lazy()startTransitiontsx
import { useTransition } from 'react'
import { useNavigate } from 'react-router-dom'
function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
const navigate = useNavigate()
const [isPending, startTransition] = useTransition()
const handleClick = (e: React.MouseEvent) => {
e.preventDefault()
startTransition(() => {
navigate(to)
})
}
return (
<a
href={to}
onClick={handleClick}
style={{ opacity: isPending ? 0.7 : 1 }}
>
{children}
</a>
)
}这可以避免懒加载路由之间的空白屏幕闪烁,并通过在当前页面显示微妙的加载指示器。
isPendingSource
来源
Patterns from patterns.dev — framework-agnostic React performance guidance for the broader web engineering community.
这些模式来自patterns.dev——面向广大Web工程社区的、与框架无关的React性能指南。",