关于 react-render-optimization
面向 React 开发者的渲染优化指南,提供减少不必要重渲染和降低渲染成本的实践模式。内容涵盖状态派生、延迟更新和长列表优化,适用于性能问题排查与组件重构。
主要功能
- 01在渲染时计算派生值
- 02订阅粗粒度状态
- 03拆分并记忆化昂贵子树
- 04使用延迟状态初始化
- 05以 ref 保存高频临时值
- 06用 startTransition 延后非紧急更新
适用场景
- 01排查组件意外重渲染
- 02改善输入和滚动时的响应
- 03优化仪表盘、编辑器或列表
- 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 性能指导。