mcpField
R
react-render-optimization

react-render-optimization

github.com/patternsdev开发2571.1k 次安装28 周前更新
代码仓库

关于 react-render-optimization

面向 React 开发者的渲染优化指南,提供减少不必要重渲染和降低渲染成本的实践模式。内容涵盖状态派生、延迟更新和长列表优化,适用于性能问题排查与组件重构。

主要功能

  1. 01在渲染时计算派生值
  2. 02订阅粗粒度状态
  3. 03拆分并记忆化昂贵子树
  4. 04使用延迟状态初始化
  5. 05以 ref 保存高频临时值
  6. 06用 startTransition 延后非紧急更新

适用场景

  1. 01排查组件意外重渲染
  2. 02改善输入和滚动时的响应
  3. 03优化仪表盘、编辑器或列表
  4. 04依据 Profiler 结果重构组件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/patternsdev/skills --skill react-render-optimization

安装后在 Claude Code 中输入/react-render-optimization确认已加载。

详细介绍

React 渲染优化

目录

用于消除不必要的重新渲染、降低渲染开销并保持 React 界面响应灵敏的实用模式。这些模式适用于任何 React 应用——无论你使用的是 Vite、Next.js、Remix,还是自定义配置。

适用场景

在以下情况中参考这些模式:

  • 组件重新渲染的频率高于预期
  • 输入、滚动或交互时界面感觉迟钝
  • Profiler 显示组件树中存在无效渲染
  • 构建对性能要求较高的功能(仪表盘、编辑器、列表)
  • 审查或重构现有 React 组件

说明

  • 在生成、审查和重构代码时应用这些模式。发现反模式时,给出修正后的版本并说明原因。

详情

概述

当组件状态变化、父组件重新渲染,或其使用的上下文更新时,React 就会重新渲染该组件。大多数重新渲染都无害,但如果它们导致昂贵的计算、深层组件树或布局抖动,用户就会察觉到卡顿。

以下模式按影响程度排序——先处理收益最大的优化,再考虑微优化。


1. 在渲染期间计算派生值——不要存储它们

影响:高——消除一类错误和不必要的状态。

存储可由现有状态或 props 计算得出的值,会造成同步问题并触发额外的重新渲染。应直接在代码中计算。

避免——冗余状态可能发生偏差:

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

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} />)}
    </>
  )
}

何时使用 useMemo,何时使用普通的 const:

  • 普通的 const——布尔标记、字符串格式化、简单算术、对象属性访问、.length 检查。这些操作几乎不耗资源,使用 useMemo 带来的开销并不值得。
  • useMemo——筛选或排序数组、构建数据结构、JSON.parse、耗时的转换,以及任何需要遍历集合或涉及 O(n) 工作的操作。

规则:如果表达式返回基本类型,或仅访问单个属性,就不要使用 useMemo。如果它会遍历或转换数据,就用 useMemo 包裹。

**React Compiler 说明:**如果启用了 React Compiler,它会自动对表达式进行记忆化,因此可以跳过手动调用 useMemo。


2. 订阅粗粒度状态,而不是原始值

影响:高——避免因无关变化而重新渲染。

如果组件只关心派生出的布尔值(例如“是否为移动设备?”),就不要订阅持续变化的原始值。

避免——每变化一个像素就重新渲染:

function Sidebar() {
  const width = useWindowWidth() // fires on every resize
  const isMobile = width < 768
  return <nav className={isMobile ? 'mobile' : 'desktop'}>...</nav>
}

推荐——仅在布尔值切换时重新渲染:

function Sidebar() {
  const isMobile = useMediaQuery('(max-width: 767px)')
  return <nav className={isMobile ? 'mobile' : 'desktop'}>...</nav>
}

这适用于许多场景:订阅 isLoggedIn,而不是整个用户对象;订阅 hasItems,而不是完整的购物车数组,等等。


3. 将开销较大的子树提取为记忆化组件

影响:高——支持提前返回并跳过渲染。

当父组件存在快速返回路径(加载中、错误、空状态)时,如果开销较大的子组件仍位于同一个组件中,它们依然会进行计算。将它们提取出来,让 React 能够完全跳过其渲染。

避免——即使在加载期间也会计算头像:

function Profile({ user, loading }: Props) {
  const avatar = useMemo(() => processAvatar(user), [user])

  if (loading) return <Skeleton />
  return <div><img src={avatar} /></div>
}

推荐——加载期间跳过计算:

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. 使用惰性状态初始化

影响:中 — 避免每次渲染时进行无用计算。

当 useState 将函数调用作为初始值时,即使结果只使用一次,该调用也会在每次渲染时执行。请改为传入函数引用。

避免 — buildIndex() 每次渲染都会运行:

const [index, setIndex] = useState(buildSearchIndex(items))

建议 — 仅在挂载时运行:

const [index, setIndex] = useState(() => buildSearchIndex(items))

以下情况适合使用惰性初始化:JSON.parse、读取 localStorage、构建数据结构、执行开销较大的转换。对于 useState(0) 或 useState(false) 这样的简单基本类型值,则无需使用。


5. 使用函数式 setState 保持回调稳定

影响:中 — 从依赖数组中移除状态变量。

如果回调只需要上一个状态来计算下一个状态,请使用函数式写法。这样可以从依赖数组中移除状态变量,并使回调的引用保持稳定。

避免 — count 变化时回调也会变化:

const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(count + 1), [count])

建议 — 回调始终保持稳定:

const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(c => c + 1), [])

6. 将交互逻辑放在事件处理程序中,而不是 Effect 中

影响:中 — 避免因依赖项变化而重复运行副作用。

如果副作用由用户操作触发(点击、提交、拖动),请在事件处理程序中运行。将其建模为状态 + Effect,会导致无关依赖项变化时重复运行。

避免 — theme 变化时 Effect 会重复运行:

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>
}

建议 — 将逻辑放在处理程序中:

function Form() {
  const theme = useContext(ThemeContext)

  function handleSubmit() {
    post('/api/register')
    showToast('Registered', theme)
  }

  return <button onClick={handleSubmit}>Submit</button>
}

7. 使用 useRef 保存瞬时、高频变化的值

影响:中 — 防止快速更新导致重复渲染。

对于变化频率很高(鼠标位置、滚动偏移量、定时器间隔计数),但不需要触发重新渲染的值,应将其保存在 ref 中。需要时直接更新 DOM。

避免 — 每次鼠标移动都会重新渲染:

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)` }} />
}

建议 — 不会触发重新渲染:

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. 对非紧急更新使用 startTransition

影响:中 — 保持高优先级更新(输入、点击)的响应速度。

将非紧急状态更新放入 startTransition 中,以便 React 能在有紧急任务时中断这些更新。这对于搜索筛选、切换选项卡和重新排序列表尤其有用。

避免 — 列表筛选时输入会卡顿:

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} />
    </>
  )
}

建议 — 输入保持流畅:

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. 在使用时再读取状态

影响:中 — 避免订阅只在回调中读取的状态。

如果只在事件处理程序中读取值,就不要调用 useSearchParams() 之类的 Hook。请改为按需读取。

避免 — URL 每次变化都会导致组件重新渲染:

function ShareButton({ id }: { id: string }) {
  const [searchParams] = useSearchParams()

  const handleShare = () => {
    const ref = searchParams.get('ref')
    share(id, { ref })
  }

  return <button onClick={handleShare}>Share</button>
}

建议 — 按需读取,不进行订阅:

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. 为默认属性使用稳定引用

影响:中 — 防止新的对象/数组字面量导致 memo() 失效。

将 [] 或 {} 作为默认属性值传入,每次渲染都会创建新的引用,从而使子组件的记忆化失效。

避免 — 每次渲染都会创建新数组:

function Dashboard({ tabs = [] }: { tabs?: Tab[] }) {
  return <TabBar tabs={tabs} /> {/* TabBar re-renders every time */}
}

建议 — 使用稳定引用:

const EMPTY_TABS: Tab[] = []

function Dashboard({ tabs = EMPTY_TABS }: { tabs?: Tab[] }) {
  return <TabBar tabs={tabs} />
}

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

影响:高 — 长篇可滚动内容的初始渲染速度可提升 5-10 倍。

为屏幕外的项目应用 content-visibility: auto,让浏览器跳过其布局和绘制,直到这些项目滚动进入视口。

.list-item {
  content-visibility: auto;
  contain-intrinsic-size: 0 80px; /* estimated height */
}
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>
  )
}

对于 1,000 个项目,浏览器会跳过约 990 个屏幕外项目的布局和绘制。将其与虚拟化(例如 react-window、@tanstack/react-virtual)结合,可用于超大型列表。


12. 将静态 JSX 提取到组件外部

影响:低 — 避免重复创建相同元素。

不会变化的 JSX 元素可以提取到模块作用域。React 会在多次渲染中复用同一个对象引用。

避免 — 每次渲染都会重新创建:

function Page() {
  return (
    <main>
      <footer>
        <p>Copyright 2026 Acme Inc.</p>
      </footer>
    </main>
  )
}

建议 — 只创建一次:

const footer = (
  <footer>
    <p>Copyright 2026 Acme Inc.</p>
  </footer>
)

function Page() {
  return <main>{footer}</main>
}

这对大型 SVG 元素最有效,因为重新创建这些元素的开销较大。

React 编译器说明: 编译器会自动将静态 JSX 提取到外部,因此无需手动进行这项优化。


13. 每个应用仅初始化一次耗时操作

影响:低至中 — 避免在严格模式和重新挂载时重复初始化。

应用级初始化(分析、身份验证检查、服务工作线程)不应放在 useEffect 中——组件会在开发环境和并发功能中重新挂载。请使用模块级守卫。

避免 — 开发环境中运行两次,重新挂载时还会再次运行:

function App() {
  useEffect(() => {
    initAnalytics()
    checkAuth()
  }, [])
  return <Router />
}

推荐 — 每次应用加载时运行一次:

let initialized = false

function App() {
  useEffect(() => {
    if (initialized) return
    initialized = true
    initAnalytics()
    checkAuth()
  }, [])
  return <Router />
}

或者在入口文件(main.tsx)的模块级别进行初始化,放在任何组件之外。


14. 将事件处理程序存入 Refs,以保持订阅稳定

影响:低 — 避免 Effect 重新订阅。

当自定义 Hook 订阅事件并接收回调时,请将回调存入 ref,这样订阅就不会在每次渲染时拆除并重新创建。

避免 — 处理程序更改时重新订阅:

function useWindowEvent(event: string, handler: (e: Event) => void) {
  useEffect(() => {
    window.addEventListener(event, handler)
    return () => window.removeEventListener(event, handler)
  }, [event, handler])
}

推荐 — 保持订阅稳定:

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 内置提供了这种模式:

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. 避免仅限客户端的数据导致水合闪烁

影响:中 — 消除 SSR 水合期间错误内容的闪现。

当渲染依赖仅限客户端的数据(localStorage、cookies)时,内联脚本可以在 React 水合前设置正确的值,从而同时避免 SSR 错误和可见的闪烁。

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. 切勿在组件内部定义组件

影响:高 — 每次渲染都会导致重新挂载、状态丢失和不必要的 DOM 操作。

如果在另一个组件的渲染过程中定义组件,React 会在每次渲染时创建新的组件类型。这意味着整个子树都会卸载并重新挂载,导致所有状态、DOM 节点以及 Effect 的清理和设置都丢失或重做。

避免 — Row 每次渲染都是一种新类型:

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>
}

推荐 — 在模块作用域中定义 Row:

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. 对昂贵的派生渲染使用 useDeferredValue

影响:高 — 在昂贵的子树于后台重新渲染时,保持 UI 响应流畅。

useDeferredValue 会告知 React,延后重新渲染依赖于快速变化值的组件。与 useTransition(封装状态更新)不同,useDeferredValue 封装的是值的使用场景;当你无法控制状态设置器时,这种方式很有用。

避免 — 每次按键都会阻塞 UI:

function SearchPage({ query }: { query: string }) {
  // Expensive: filters and renders 10,000 items on every keystroke
  const results = filterItems(query)
  return <ResultsList items={results} />
}

推荐 — 输入保持响应,结果在后台更新:

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,何时使用 useTransition:

  • useTransition — 你可以控制状态设置器,并将其封装在 startTransition 中
  • useDeferredValue — 值来自 props、父组件或你无法控制的库

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

影响:中 — 避免将 0、NaN 或空字符串渲染到 DOM 中。

JSX 中的 && 运算符会在遇到假值时短路,但 0、NaN 和 "" 虽然是假值,仍会作为可见文本节点渲染。

避免 — count 为零时会将 0 渲染到 DOM 中:

function NotificationBadge({ count }: { count: number }) {
  return <div>{count && <Badge>{count}</Badge>}</div>
  // When count is 0, renders: <div>0</div>
}

推荐 — 显式进行布尔值检查:

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>
}

这适用于任何可能为 0、NaN 或 "" 的值,例如数组长度、字符串值和数值 props。始终使用显式布尔表达式(> 0、!== ''、!= null),不要依赖真值判断。


19. 将 Effect 依赖项缩小为基本类型

影响:中 — 防止无关对象属性变化时 Effect 重新运行。

如果 Effect 只需要对象中的一个属性,请在依赖项数组之外先提取该属性。传入整个对象会导致任何属性变化时 Effect 都重新运行。

避免 — user.name 或 user.avatar 变化时 Effect 会重新运行:

function UserStatus({ user }: { user: User }) {
  useEffect(() => {
    updatePresence(user.id)
  }, [user]) // re-runs on ANY user property change
}

推荐 — 只有 ID 变化时才重新运行:

function UserStatus({ user }: { user: User }) {
  const { id } = user
  useEffect(() => {
    updatePresence(id)
  }, [id])
}

这同样适用于 Hook 的返回值。如果 useQuery 返回 { data, status, fetchStatus },而你的 Effect 只关心 status,请先解构。


20. 拆分组合式 Hook 计算

影响:中 — 防止只需要 Hook 部分输出的使用方重新渲染。

当自定义 Hook 计算多个互不相关的值时,其中一个值变化会导致所有使用方重新渲染,即使它们读取的值并未变化。

避免 — total 变化会导致只需要 average 的组件重新渲染:

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])
}

推荐 — 拆分为专注于单一职责的 Hook:

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. 通过批量读取和写入 DOM 避免布局抖动

影响:高 — 防止强制同步布局阻塞主线程。

写入 DOM 后读取布局属性(例如 offsetHeight、getBoundingClientRect()),会迫使浏览器同步重新计算布局。在循环中,这会造成布局抖动。

避免 — 每次迭代都会强制重新计算布局:

function resizeCards(cards: HTMLElement[]) {
  cards.forEach(card => {
    const height = card.offsetHeight          // READ (forces layout)
    card.style.minHeight = `${height + 20}px` // WRITE (invalidates layout)
  })
}

推荐 — 先批量读取,再批量写入:

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`
  })
}

在 React 中,这种情况最常见于测量和修改 DOM 元素的 useLayoutEffect 或 useEffect 回调中。如果需要在动画帧内读取布局,请使用 requestAnimationFrame 进行批处理:

useLayoutEffect(() => {
  const measurements = items.map(el => el.getBoundingClientRect())

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

22. 为 SVG 外层包装元素添加动画,而非直接为 SVG 元素添加动画

影响:中 — 避免每个动画帧都重绘整个 SVG。

为 SVG 元素本身(例如 <svg> 或 <path>)的属性添加动画会触发整个 SVG 重绘。请将 SVG 包装在 <div> 中,为包装元素添加动画。

避免 — 重绘整个 SVG 树:

<motion.svg animate={{ rotate: 360 }} style={{ width: 200, height: 200 }}>
  <ComplexChart />
</motion.svg>

推荐 — 只重绘包装元素:

<motion.div animate={{ rotate: 360 }} style={{ width: 200, height: 200 }}>
  <svg viewBox="0 0 200 200">
    <ComplexChart />
  </svg>
</motion.div>

这同样适用于 CSS 动画。请在包装元素上使用 transform,而不要直接为 cx、cy 或 d 等 SVG 属性添加动画。


23. 抑制预期内的水合不匹配警告

影响:低至中 — 消除已知安全的警告,同时不掩盖真正的 Bug。

某些内容在服务端和客户端之间有意不同,例如时间戳、随机 ID、特定于用户代理的渲染结果。请在这些特定元素上使用 suppressHydrationWarning。

function Comment({ createdAt }: { createdAt: Date }) {
  return (
    <article>
      <p>{comment.body}</p>
      <time suppressHydrationWarning>
        {formatRelativeTime(createdAt)} {/* "2 minutes ago" differs server vs client */}
      </time>
    </article>
  )
}

请谨慎使用,且仅用于叶子元素。切勿在容器元素上抑制警告,否则会掩盖子元素中的真正不匹配。


24. 面向 Vite SPA 的 React DOM 资源提示

影响:高 — 无需框架支持,即可让浏览器更早开始加载关键资源。

React 19 在 react-dom 中新增了 preload() 和 preinit(),它们是可在任何 React 应用中使用的命令式资源提示。在 Vite SPA 中(这类应用没有框架级预取功能),它们尤其有价值。

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} />
}

导航时 — 预加载下一页的数据和代码:

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>
}

如果资源已加载,这些调用不会执行任何操作,因此提前调用是安全的。对于不使用元框架的 Vite 应用,这是优先安排资源加载顺序的主要机制。


25. 使用 useTransition 进行路由导航

影响:中等 — 在下一条路由加载期间,保持当前页面可交互。

在使用 React.lazy() 路由的 Vite 单页应用中,点击导航链接时,界面可能会在代码块加载和组件渲染期间卡住。使用 startTransition 包装导航后,React 会在新页面准备就绪前显示旧页面。

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,以便在当前页面显示一个不显眼的加载指示器。


来源

模式摘自 patterns.dev — 为更广泛的 Web 工程社区提供与框架无关的 React 性能指导。

评论

支持 Markdown