从一次点赞操作,看 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 列表性能问题,
往往不是"算不动",
而是"扩散得太远"。

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

  • 列表顺了
  • 滑动稳了
  • 很多"玄学卡顿"自己消失了
相关推荐
神的孩子都在歌唱2 小时前
ARP 与 MAC 地址解析:局域网通信的第一步
网络·macos
tzhou644522 小时前
Docker核心功能解析:网络、资源控制、数据卷与镜像构建
网络·docker·eureka
init_23612 小时前
MPLS跨域optionA 配置案例
java·开发语言·网络
G_H_S_3_2 小时前
【网络运维】KVM基础使用
linux·运维·网络·kvm
lkbhua莱克瓦242 小时前
面向编程3-UDP通信程序
java·网络·网络协议·udp
小尧嵌入式2 小时前
CANOpen协议
服务器·网络·c++·windows
代码游侠2 小时前
学习笔记——网络基础
linux·c语言·网络·笔记·学习·算法
少云清2 小时前
【接口测试】2_代码实现 _设置http请求语法
网络·网络协议·http
前端不太难11 小时前
从 Navigation State 反推架构腐化
前端·架构·react