从一次点赞操作,看 RN 列表的渲染扩散路径

@[toc]

为什么一个小交互,能拖垮整个列表

在 RN 项目里,有一种性能问题特别"冤":

  • 点的是一个 item 的 ❤️
  • 卡的是整个列表
  • Debug 半天发现 FlatList 配置都没问题

最后大家会陷入一种无力感:

"就点个赞,怎么会影响这么大?"

这篇文章,我们就只盯这一件事
一次点赞操作,在 RN 里到底会触发什么?

一、先说结论:点赞不是问题,扩散才是问题

点赞这个动作本身非常轻:

text 复制代码
点击 → setLiked(true)

但在 RN 项目中,它经常会被无意中放大成:

text 复制代码
点击
→ state 更新
→ 父组件 render
→ FlatList render
→ N 个 Item render
→ JS 线程拥堵
→ 滑动掉帧

问题不在点赞,

而在你让这次更新"扩散到了不该扩散的地方"

二、我们先构建一个"非常真实"的业务场景

这是我在多个项目里反复见到的结构。

典型列表结构

text 复制代码
ListScreen
 ├─ Header
 ├─ FlatList
 │   └─ Item (点赞)

数据结构

ts 复制代码
{
  id: number
  title: string
  liked: boolean
}

三、错误示例:点赞状态放在列表父组件

我们直接看代码,这种写法你一定很熟。

ListScreen

tsx 复制代码
function ListScreen() {
  const [list, setList] = useState(mockList)

  const onLike = (id: number) => {
    setList(prev =>
      prev.map(item =>
        item.id === id
          ? { ...item, liked: !item.liked }
          : item
      )
    )
  }

  return (
    <FlatList
      data={list}
      renderItem={({ item }) => (
        <Item item={item} onLike={onLike} />
      )}
    />
  )
}

Item

tsx 复制代码
function Item({ item, onLike }) {
  return (
    <TouchableOpacity onPress={() => onLike(item.id)}>
      <Text>{item.title}</Text>
      <Text>{item.liked ? '❤️' : '🤍'}</Text>
    </TouchableOpacity>
  )
}

四、从"点击"开始,渲染链路是如何扩散的?

我们一层一层拆。

第一步:点赞触发 state 更新

ts 复制代码
setList(prev => ...)

这是一个全量 list 更新,即使你只改了一个 item。

第二步:ListScreen render

state 在父组件:

  • 父组件必定重新 render
  • FlatList 作为子组件,也会重新 render

第三步:renderItem 函数重新创建

tsx 复制代码
renderItem={({ item }) => (
  <Item item={item} onLike={onLike} />
)}

注意两个点:

  • renderItem 是一个新函数
  • onLike 引用是新的

即使 item 没变,props 也不稳定

第四步:所有可见 Item 重新 render

即便你加了:

ts 复制代码
React.memo(Item)

也很容易失效,因为:

  • item 是新对象
  • onLike 是新引用

第五步:JS 线程被"无意义 render"占满

这一步非常关键。

点赞发生时:

  • JS 线程在算 diff
  • 同时用户可能在滑动
  • 滚动需要 JS 配合

掉帧就出现了。

五、为什么 Web 项目里,这种写法问题不大?

同样的结构,在 Web 中:

  • 滚动是浏览器原生完成的
  • React render 的压力没那么直观
  • 用户不会频繁"快速点赞 + 滑动"

所以问题被"掩盖"了。

RN 没有。

六、真正的问题:你把"局部状态"设计成了"全局更新"

点赞的本质是什么?

一个 Item 的局部 UI 状态变化

但你给它的代价是:

整个列表的数据更新

这就是扩散的根源。

七、正确做法一:把点赞状态下沉到 Item

改造思路

  • 列表数据保持"稳定结构"
  • 点赞只影响当前 Item

改造后的 Item

tsx 复制代码
function Item({ item }) {
  const [liked, setLiked] = useState(item.liked)

  return (
    <TouchableOpacity onPress={() => setLiked(v => !v)}>
      <Text>{item.title}</Text>
      <Text>{liked ? '❤️' : '🤍'}</Text>
    </TouchableOpacity>
  )
}

效果变化

  • 父组件不 render
  • FlatList 不 render
  • 只有当前 Item render

那数据同步怎么办?

这是很多人会立刻问的问题。

答案是:

UI 状态 ≠ 业务最终状态

你可以在合适的时机(如失焦、批量提交)再同步。

八、正确做法二:用"局部 store",而不是全局 store

如果你确实需要跨组件共享点赞状态。

错误思路

"那我放 Redux / Zustand 吧。"

结果是:

  • 一个点赞
  • 全列表订阅者都被通知

更合理的方式:item 级别 store

ts 复制代码
const useLikeStore = create(() => ({
  likedMap: new Map<number, boolean>()
}))

在 Item 内部订阅:

tsx 复制代码
const liked = useLikeStore(s => s.likedMap.get(id))

只订阅你需要的那一块。

九、RN 项目里,"渲染扩散"是比"渲染次数"更重要的指标

不要只问:

"render 了几次?"

要问:

"这次 render,影响了谁?"

一次点赞:

  • 只影响一个 Item → 正常
  • 影响整个列表 → 危险

十、一个非常实用的自检问题

下次你写交互时,问自己一句话:

如果这个 state 变了,最坏情况下会导致多少组件重新 render?

如果答案是:

  • "我也不太清楚"
  • "可能是整个页面"

那这个 state 的位置,大概率放错了。

十一、工程实践总结

从一次点赞,我们能总结出 RN 列表的三条铁律:

  1. 能不让父组件更新,就别让它更新
  2. 列表 item 的 UI 状态,尽量局部化
  3. 避免"数据正确,但渲染路径错误"

十二、最后一句总结

RN 列表性能问题,
往往不是"算不动",
而是"扩散得太远"。

当你学会控制渲染扩散路径,你会发现:

  • 列表顺了
  • 滑动稳了
  • 很多"玄学卡顿"自己消失了
相关推荐
Trouvaille ~1 小时前
【Linux】TCP Socket编程实战(一):API详解与单连接Echo Server
linux·运维·服务器·网络·c++·tcp/ip·socket
liann1191 小时前
3.1_网络——基础
网络·安全·web安全·http·网络安全
独行soc1 小时前
2026年渗透测试面试题总结-17(题目+回答)
android·网络·安全·web安全·渗透测试·安全狮
辣香牛肉面1 小时前
Wireshark v4.6.2 开源免费网络嗅探抓包工具中文便携版
网络·测试工具·wireshark
全栈工程师修炼指南1 小时前
Nginx | stream 四层反向代理:SSL、PREREAD 阶段模块指令浅析与实践
运维·网络·网络协议·nginx·ssl
M158227690551 小时前
TCP转LORA产品说明及应用案例
网络·网络协议·tcp/ip
旖旎夜光1 小时前
Linux(13)(中)
linux·网络
来可电子CAN青年2 小时前
CAN总线远距离传输老断网?Fx灯不闪别慌,这几招让你的通信“稳如泰山”!
网络
独行soc2 小时前
2026年渗透测试面试题总结-18(题目+回答)
android·网络·安全·web安全·渗透测试·安全狮
云小逸2 小时前
【nmap源码解析】Nmap OS识别核心模块深度解析:osscan2.cc源码剖析(1)
开发语言·网络·学习·nmap