react-render-optimization

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React 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
const
):
tsx
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
useMemo
vs a plain
const
:
  • Plain
    const
    — boolean flags, string formatting, simple arithmetic, object property access,
    .length
    checks. These are essentially free and
    useMemo
    overhead is not worth it.
  • useMemo
    — filtering/sorting arrays, building data structures,
    JSON.parse
    , expensive transformations, anything that iterates collections or involves O(n) work.
The rule: if the expression returns a primitive or is a single property access, skip
useMemo
. If it iterates or transforms data, wrap it.
React Compiler note: If React Compiler is enabled, it auto-memoizes expressions and you can skip manual
useMemo
calls.

影响程度:高 —— 消除一整类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} />)}
    </>
  )
}
推荐——在渲染时计算(简单推导使用普通
const
):
tsx
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} />)}
    </>
  )
}
何时使用
useMemo
vs 普通
const
  • 普通
    const
    —— 布尔标志、字符串格式化、简单算术、对象属性访问、
    .length
    检查。这些操作几乎没有成本,使用
    useMemo
    的开销得不偿失。
  • useMemo
    —— 过滤/排序数组、构建数据结构、
    JSON.parse
    、昂贵的转换操作、任何需要遍历集合或涉及O(n)复杂度的工作。
规则:如果表达式返回原始值或只是单一属性访问,跳过
useMemo
。如果需要遍历或转换数据,就用它包裹。
React 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
isLoggedIn
rather than the entire user object,
hasItems
rather than the full cart array, etc.

影响程度:高 —— 防止无关变化导致的重渲染。
如果你的组件只关心派生的布尔值(如“是否为移动端?”),不要订阅持续变化的原始值。
避免——每次尺寸变化都重渲染:
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>
}
这种方式适用于多种场景:订阅
isLoggedIn
而非整个用户对象,订阅
hasItems
而非完整的购物车数组等。

3. 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 manual
memo()
wrapping less necessary. But extracting components for early returns is still valuable.

影响程度:高 —— 支持提前返回并跳过渲染。
当父组件有快速路径(加载中、错误、空状态)时,如果昂贵的子组件和父组件在同一组件内,它们仍会执行计算。将它们提取出来,这样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
useState
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.
Avoid —
buildIndex()
runs every render:
tsx
const [index, setIndex] = useState(buildSearchIndex(items))
Prefer — runs only on mount:
tsx
const [index, setIndex] = useState(() => buildSearchIndex(items))
Use lazy init for:
JSON.parse
,
localStorage
reads, building data structures, heavy transformations. Skip it for simple primitives like
useState(0)
or
useState(false)
.

影响程度:中 —— 避免每次渲染都执行无用计算。
useState
接收一个函数调用作为初始值时,该函数会在每次渲染时执行,尽管结果只会被使用一次。改为传递函数引用。
避免——
buildIndex()
在每次渲染时都运行:
tsx
const [index, setIndex] = useState(buildSearchIndex(items))
推荐——仅在挂载时运行:
tsx
const [index, setIndex] = useState(() => buildSearchIndex(items))
对以下操作使用惰性初始化:
JSON.parse
localStorage
读取、构建数据结构、重型转换。对于简单的原始值如
useState(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
count
changes:
tsx
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), [])

影响程度:中 —— 从依赖数组中移除状态变量。
当回调仅需要前一个状态来计算下一个状态时,使用函数式形式。这会将状态变量从依赖数组中移除,并生成稳定的回调标识。
避免——当
count
变化时回调也变化:
tsx
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
theme
changes:
tsx
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>
}

影响程度:中 —— 避免依赖变化导致副作用重复执行。
如果副作用是由用户操作(点击、提交、拖拽)触发的,直接在事件处理程序中运行它。将其建模为状态+副作用会导致无关依赖变化时触发重渲染。
避免——当
theme
变化时副作用重复执行:
tsx
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

7. 使用
useRef
存储瞬态、高频变化的值

Impact: 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

8. 使用
startTransition
处理非紧急更新

Impact: MEDIUM — Keeps high-priority updates (typing, clicking) responsive.
Wrap non-urgent state updates in
startTransition
so React can interrupt them for urgent work. This is especially useful for search filtering, tab switching, and list re-sorting.
Avoid — 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} />
    </>
  )
}

影响程度:中 —— 保持高优先级更新(输入、点击)的响应性。
将非紧急状态更新包裹在
startTransition
中,这样React可以中断它们以处理紧急工作。这在搜索过滤、标签切换和列表重新排序时尤其有用。
避免——输入时列表过滤阻塞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
useSearchParams()
if you only read the value inside an event handler. Read it on demand instead.
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>
}

影响程度:中 —— 避免订阅仅在回调中读取的状态。
如果仅在事件处理程序中读取值,不要调用
useSearchParams()
这样的hooks。改为按需读取。
避免——每次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
memo()
from being defeated by new object/array literals.
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

11. 为长列表使用CSS
content-visibility

Impact: HIGH — 5-10x faster initial render for long scrollable content.
Apply
content-visibility: auto
to off-screen items so the browser skips their layout and paint until they scroll into view.
css
.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.,
react-window
,
@tanstack/react-virtual
) for truly massive lists.

影响程度:高 —— 长滚动内容的初始渲染速度提升5-10倍。
对屏幕外的元素应用
content-visibility: auto
,这样浏览器会跳过它们的布局和绘制,直到它们滚动到视图中。
css
.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-virtual
)处理超大规模列表。

12. 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
useEffect
— components remount in development and in concurrent features. Use a module-level guard.
Avoid — 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 (
main.tsx
), outside any component.

影响程度:低-中 —— 避免在严格模式和重新挂载时重复初始化。
应用级别的初始化(如分析、权限检查、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.tsx
)的模块级别初始化,放在任何组件外部。

14. 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+,
useEffectEvent
provides this pattern as a built-in:
tsx
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+,
useEffectEvent
内置了这种模式:
tsx
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 —
Row
is a new type every render:
tsx
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 —
Row
defined at module scope:
tsx
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
useMemo
,
useCallback
, or any other hook. Always define components at module scope or as static properties.

影响程度:高 —— 导致每次渲染时重新挂载、状态丢失和无效的DOM操作。
当你在另一个组件的渲染函数内部定义组件时,React会在每次渲染时创建一个新的组件类型。这意味着整个子树会卸载并重新挂载——丢失所有状态、DOM节点和副作用的清理/设置。
避免——每次渲染
Row
都是新类型:
tsx
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>
}
推荐——
Row
定义在模块作用域:
tsx
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>
}
这也适用于在
useMemo
useCallback
或任何其他hook内部定义的组件。始终在模块作用域或作为静态属性定义组件。

17. Use
useDeferredValue
for Expensive Derived Renders

17. 使用
useDeferredValue
处理昂贵的派生渲染

Impact: HIGH — Keeps the UI responsive while expensive subtrees re-render in the background.
useDeferredValue
tells React to defer re-rendering components that depend on a fast-changing value. Unlike
useTransition
(which wraps the state update),
useDeferredValue
wraps the consumption — useful when you don't control the state setter.
Avoid — 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
useDeferredValue
vs
useTransition
:
  • useTransition
    — you control the state setter and can wrap it in
    startTransition
  • useDeferredValue
    — the value comes from props, a parent, or a library you don't control

影响程度:高 —— 在后台重新渲染昂贵子树时保持UI响应。
useDeferredValue
告诉React延迟重新渲染依赖快速变化值的组件。与
useTransition
(包裹状态更新)不同,
useDeferredValue
包裹值的消费——当你不控制状态设置器时很有用。
避免——每次按键都阻塞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>
  )
}
何时使用
useDeferredValue
vs
useTransition
  • useTransition
    —— 你控制状态设置器,可以用
    startTransition
    包裹它
  • useDeferredValue
    —— 值来自props、父组件或你无法控制的库

18. Use Explicit Checks in Conditional Rendering

18. 在条件渲染中使用显式检查

Impact: MEDIUM — Prevents rendering
0
,
NaN
, or empty strings to the DOM.
The
&&
operator in JSX short-circuits on falsy values — but
0
,
NaN
, and
""
are falsy yet still render as visible text nodes.
Avoid — renders
0
to the DOM when count is zero:
tsx
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
0
,
NaN
, or
""
— array lengths, string values, numeric props. Always use an explicit boolean expression (
> 0
,
!== ''
,
!= null
) rather than relying on truthiness.

影响程度:中 —— 防止将
0
NaN
或空字符串渲染到DOM中。
JSX中的
&&
运算符在遇到假值时短路——但
0
NaN
""
是假值,但仍会渲染为可见的文本节点。
避免——当count为0时渲染
0
到DOM:
tsx
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>
}
这适用于任何可能为
0
NaN
""
的值——数组长度、字符串值、数值props。始终使用显式布尔表达式(
> 0
!== ''
!= null
),而非依赖真值判断。

19. 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
user.name
or
user.avatar
changes:
tsx
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
useQuery
returns
{ data, status, fetchStatus }
and your effect only cares about
status
, destructure first.

影响程度:中 —— 防止无关对象属性变化导致Effect重新执行。
当Effect只需要对象的一个属性时,在依赖数组之前提取它。传递整个对象会导致任何属性变化时都重新执行Effect。
避免——当
user.name
user.avatar
变化时Effect重新执行:
tsx
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的返回值。如果
useQuery
返回
{ data, status, fetchStatus }
而你的Effect只关心
status
,先解构它。

20. 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
total
re-renders components that only need
average
:
tsx
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计算多个不相关的值时,其中一个值的变化会导致所有消费者重渲染——即使有些消费者只读取未变化的值。
避免——
total
变化会导致仅需要
average
的组件重渲染:
tsx
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.,
offsetHeight
,
getBoundingClientRect()
) after writing to the DOM forces the browser to recalculate layout synchronously. In a loop, this creates layout thrashing.
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
useLayoutEffect
or
useEffect
callbacks that measure and mutate DOM elements. When you need to read layout inside an animation frame, use
requestAnimationFrame
to batch:
tsx
useLayoutEffect(() => {
  const measurements = items.map(el => el.getBoundingClientRect())

  requestAnimationFrame(() => {
    items.forEach((el, i) => {
      el.style.transform = `translateY(${measurements[i].top}px)`
    })
  })
}, [items])

影响程度:高 —— 防止阻塞主线程的强制同步布局。
在写入DOM后读取布局属性(如
offsetHeight
getBoundingClientRect()
)会迫使浏览器同步重新计算布局。在循环中这样做会导致布局抖动。
避免——每次迭代都强制重新计算布局:
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中,这最常发生在
useLayoutEffect
useEffect
回调中,这些回调会测量并修改DOM元素。当你需要在动画帧内读取布局时,使用
requestAnimationFrame
进行批量处理:
tsx
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.,
<svg>
or
<path>
) triggers a full SVG repaint. Wrap the SVG in a
<div>
and animate the wrapper instead.
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
transform
on a wrapper element rather than animating SVG attributes like
cx
,
cy
, or
d
directly.

影响程度:中 —— 避免在每一动画帧重绘整个SVG。
直接在SVG元素(如
<svg>
<path>
)上设置动画属性会触发整个SVG的重绘。将SVG包裹在
<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动画。在包装器元素上使用
transform
,而非直接为SVG属性如
cx
cy
d
设置动画。

23. 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
suppressHydrationWarning
on those specific elements.
tsx
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、特定用户代理的渲染。在这些特定元素上使用
suppressHydrationWarning
tsx
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
preload()
and
preinit()
from
react-dom
— imperative resource hints that work in any React app. In Vite SPAs (which don't get framework-level prefetching), these are especially valuable.
tsx
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-dom
中新增了
preload()
preinit()
——命令式资源提示,适用于任何React应用。在Vite单页应用(没有框架级别的预取支持)中,这些功能尤其有价值。
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

25. 为路由导航使用
useTransition

Impact: MEDIUM — Keeps the current page interactive while the next route loads.
In Vite SPAs with
React.lazy()
routes, clicking a navigation link can freeze the UI while the chunk loads and the component renders. Wrapping navigation in
startTransition
lets React show the old page until the new one is ready.
tsx
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
isPending
to show a subtle loading indicator on the current page.

影响程度:中 —— 在加载下一个路由时保持当前页面的交互性。
在使用
React.lazy()
路由的Vite单页应用中,点击导航链接可能会在加载代码块和渲染组件时冻结UI。将导航包裹在
startTransition
中,让React在新页面准备好之前显示旧页面。
tsx
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>
  )
}
这可以避免懒加载路由之间的空白屏幕闪烁,并通过
isPending
在当前页面显示微妙的加载指示器。

Source

来源

Patterns from patterns.dev — framework-agnostic React performance guidance for the broader web engineering community.
这些模式来自patterns.dev——面向广大Web工程社区的、与框架无关的React性能指南。",