Skip to content

React高频渲染场景:数据更新频繁,如何保证页面的渲染性能

· 10 min

React 高频渲染场景:数据更新像“机关枪”,页面怎么不掉帧?#

我的实战背景很接地气:在做一个 AI 聊天室时,前端用 React,后端通过 text/event-stream 源源不断地推消息。数据少的时候一切安好;一旦消息多了、列表长了、更新还频繁,页面就开始“喘不上气”——丢帧、卡顿、主线程忙不过来。于是我动手做了几轮优化和压测,下面把过程、思路、代码和结论给各位交代清楚。


场景背景#


问题复现与瓶颈定位#


优化路线与压测结果#

以下方案朴实无华,主打我上我也行。(关键是思路,文末有压测演示)

方案 V1:纯节流 throttle(100ms 一次)#

示例(lodash.throttle):

const flush = throttle(
() => {
setMessageList((draft) => {
const last = draft[draft.length - 1]
last.message += bufferRef.current
})
bufferRef.current = ''
},
100,
{ leading: false, trailing: true }
)

方案 V2:消息队列 + 节流批量刷新#

核心片段:

const queueRef = useRef<string[]>([])
const push = (msg: string) => {
queueRef.current.push(msg)
flush() // 触发节流的合并刷新
}
const flush = throttle(
() => {
const batch = queueRef.current.join('')
queueRef.current.length = 0
setMessageList((draft) => {
const last = draft[draft.length - 1]
last.message += batch
})
},
100,
{ leading: false, trailing: true }
)

方案 V3:节流函数里加空闲帧渲染(requestAnimationFrame)#

核心片段:

const flush = throttle(
() => {
requestAnimationFrame(() => {
const batch = queueRef.current.join('')
queueRef.current.length = 0
setMessageList((draft) => {
const last = draft[draft.length - 1]
last.message += batch
})
chatMessageContainerRef.current?.scrollToBottom()
})
},
100,
{ leading: false, trailing: true }
)

进一步压测:每 1ms × 20 条消息,触发 600 次(共 12000 条)#


方案 4:把节流间隔从 100ms 提到 300ms(实测最优点)#


对比 Tip:仅“队列 + rAF”(不加节流)#


可抄用的示例代码(含压测入口)#

下面是一段整理过的测试/生产两用代码。它覆盖“队列 + 节流 + rAF”,并且对“把内容拼接到最后一条消息”的场景做了适配。

import React, {
useState,
useCallback,
useRef,
useMemo,
useEffect,
} from 'react@18'
import { createRoot } from 'react-dom@18/client'
import throttle from 'lodash.throttle'
type Message = { id: string; message: string }
export default function ChatPerfDemo() {
const [visible, setVisible] = useState<boolean>(true)
const [count, setCount] = useState<number>(0)
const [messageList, setMessageList] = useState<Message[]>([
{ id: 'init', message: '' },
])
const chatMessageContainerRef = useRef<{ scrollToBottom: () => void }>(null)
// 批量缓冲区(用 ref 避免频繁触发渲染)
const bufferRef = useRef<string>('')
// 节流 + rAF 的合并刷新
const flush = useMemo(
() =>
throttle(
() => {
requestAnimationFrame(() => {
if (!bufferRef.current) return
const batch = bufferRef.current
bufferRef.current = ''
setMessageList((draft) => {
const newDraft = [...draft]
const last = { ...newDraft[newDraft.length - 1] }
last.message += batch
newDraft[newDraft.length - 1] = last
return newDraft
})
chatMessageContainerRef.current?.scrollToBottom?.()
})
},
300, // 关键:根据场景选择 100ms / 300ms
{ leading: false, trailing: true }
),
[]
)
const play = useCallback(() => {
setVisible(false)
let round = 0
const timer = setInterval(() => {
round++
if (round > 6000) {
clearInterval(timer)
return
}
// 每次“进 20 条”
for (let j = 0; j < 20; j++) {
bufferRef.current += `~~~~~${round}:${j + 1}~~~~~~`
}
flush()
}, 1)
return () => clearInterval(timer)
}, [flush])
return (
<div>
<div>数据渲染时,进行输入、计算、动画测试,检测fps是否流畅</div>
<input type="text" placehoder="可输入" />
<div>
<button
onClick={() => {
setCount((v) => v + 1)
}}
>
点击+1
</button>
<span>{count}</span>
</div>
<div>
<div className="move"></div>
</div>
{visible && <button onClick={play}>开始渲染</button>}
<MessageList ref={chatMessageContainerRef} items={messageList} />
</div>
)
}
// 极简的消息列表
const MessageList = React.forwardRef<
{ scrollToBottom: () => void },
{ items: Message[] }
>(({ items }, ref) => {
const ulRef = useRef<HTMLUListElement>(null)
React.useImperativeHandle(ref, () => ({
scrollToBottom() {
const el = ulRef.current
if (!el) return
el.scrollTop = el.scrollHeight
},
}))
return (
<>
<div>当前渲染字符长度:{items[0].message.length || 0}</div>
<ul
ref={ulRef}
style={{
height: 400,
overflow: 'auto',
fontFamily: 'monospace',
padding: 8,
}}
>
{items?.map((m) => (
<li key={m.id} style={{ wordWrap: 'break-word' }}>
<MessageRow text={m.message} />
</li>
))}
</ul>
</>
)
})
// 列表项尽量 pure,避免重复渲染
const MessageRow = React.memo(({ text }: { text: string }) => {
return <span>{text}</span>
})
const app = document.getElementById('app')
const root = createRoot(app!)
root.render(<ChatPerfDemo />)

要点说明:


进一步可选优化(按需选)#


最终结论#


这套思路落进业务后,页面终于从“猛男硬上”变成“润物无声”。消息再密,UI 依旧稳。