乐于分享
好东西不私藏

AI 生成的前端代码,一半在无效渲染?

AI 生成的前端代码,一半在无效渲染?

前段时间让 AI 帮我写一个可编辑表格组件。

React 版本的,每输入一个字符,整个表格闪一下。Vue 版本的,打开页面后请求发了五六遍。

AI 修了三轮——改 memo、改 key、改依赖数组——最后它放弃了,告诉我:"建议手动检查渲染逻辑。"

这件事让我意识到一个问题:AI 写代码的速度在飞涨,但对"响应式"这件事的理解,它连门都没入。

这篇文章不讨论"AI 会不会取代前端",咱们落地一点——拿 AI 最容易翻车的三个"无效渲染"场景,看看它到底在哪里摔倒,以及看懂原理的人为什么能把它扶起来。


雷区 1:渲染粒度 —— 更新一个人,重绘一张表

React 翻车现场

我让 AI 生成一个员工列表,支持点选行高亮。

AI 很快给出了这段代码:

function EmployeeList() {  const [employees, setEmployees] = useState(data);  const [selectedId, setSelectedId] = useState(null);  return (    <ul>      {employees.map(emp => (        <lionClick={() => setSelectedId(emp.id)}            className={selectedId === emp.id ? 'active' : ''}>          {emp.name} — {emp.dept}        </li>      ))}    </ul>  );}

看一遍逻辑没问题。但跑起来发现:每次点击高亮一行,整张列表 100 行全部重渲染。 原因很简单——selectedId 一变,函数组件整体重新执行,所有 <li> 全部重建。

AI 不会告诉你:React 的默认行为是自上而下全量渲染

修复也不复杂,但难点在于你得知道"这里需要防一下":

const EmployeeItem = React.memo(({ emp, isActive, onClick }) => (  <lionClick={onClick}      className={isActive ? 'active: ''}>    {emp.name} — {emp.dept}  </li>));function EmployeeList() {  // ...state 不变  return (    <ul>      {employees.map(emp => (        <EmployeeItem          key={emp.id}          emp={emp}          isActive={selectedId === emp.id}          onClick={() => setSelectedId(emp.id)}        />      ))}    </ul>  );}

React.memo 告诉 React:"props 没变就别重渲染这个子组件。"就这么一层薄薄的防护,100 行的列表从"全量刷新"变成了"仅高亮那行变动"。

Vue 翻车现场

同样的功能让 AI 写 Vue 版本,它给出的是:

<script setup>
const selectedId = ref(null)
const employees = ref(data)
const activeEmployee = computed(() => {
  return employees.value.find(e => e.id === selectedId.value)
})
</script>

<template>
  <ul>
    <li v-for="emp in employees" :key="emp.id"
        @click="selectedId = emp.id"
        :class="{ active: selectedId === emp.id }">
      {{ emp.name }} — {{ emp.dept }}
    </li>
  </ul>
</template>

这段代码在渲染层面比 React 的初版好——Vue 的 v-for 自带细粒度更新,不会整表重绘。但 AI 在这里踩了另一个坑:它写了一个 computed 叫 activeEmployee,实际又没用上。这是个"幽灵计算"——每次 selectedId 变化,computed 都要遍历一遍员工数组。

更隐蔽的问题是:如果 AI 把获取员工数据的请求写在了 computed 里(有人真遇到过),那每次选中行都会触发一次 API 调用。

原理挖一挖

React 和 Vue 的渲染哲学本质不同:

  • React
    :组件是一棵函数树。状态变了 → 整棵树重新执行(re-render)→ Virtual DOM diff → 找出真正要改的 DOM。优化思路是"减少重新执行的节点"(memo、useMemo)。
  • Vue
    :组件是一张依赖网。状态变了 → 精确通知依赖了它的节点 → 直接更新对应 DOM。优化思路是"避免依赖链过长"(合理拆分 computed、避免 deep watch)。

两种哲学各有优劣,但 AI 对这两种哲学的理解都是零。它只知道"模板",不知道"代价"。


雷区 2:列表渲染 —— 缺了 key,整表重绘

React & Vue 翻车现场(症状一样)

这个雷区太经典了。AI 生成列表时,key 的写法几乎永远是:

// React - AI 最爱{items.map((item, index) => <likey={index}>{item.name}</li>)}<!-- Vue - AI 最爱 --><liv-for="(item, index) in items":key="index">{{ item.name }}</li>

如果列表是静态的,index 做 key 确实能用。但一旦列表会增删改排,index 做 key 的后果是灾难性的——本来只需要删除一行,结果因为 key 错位,整个列表的 DOM 全部重建。

这就是为什么很多人反馈"AI 写的列表交互一多就卡",根源就在这里。

修复极其简单:

// React{items.map(item => <likey={item.id}>{item.name}</li>)}<!-- Vue --><liv-for="item in items":key="item.id">{{ item.name }}</li>

原理挖一挖

key 不是"性能优化的可选配置项",它是 Virtual DOM 的复用标识

diff 算法在对比新旧两棵虚拟节点树时,靠 key 来判断"这是同一个节点挪了位置"还是"这是新插入的节点"。

用 index 做 key,等于告诉 diff 算法:"我放弃唯一标识,你用位置来猜吧。"增删操作一多,一猜就错。

AI 不会告诉你:key 用对了,diff 是 O(n);key 用错了,diff 退化成 O(n³)。


雷区 3:副作用管理 —— 依赖数组里的幽灵

React 翻车现场

让 AI 加一个功能:"当筛选条件变化时,重新请求数据"。

它写出来的代码大概长这样:

const [filters, setFilters] = useState({ dept''level'' });useEffect(() => {  fetchEmployees(filters).then(setEmployees);}, []); // 依赖数组为空?!

AI 的逻辑是:"我说了在筛选变化时重新请求,所以 useEffect 里写了,但为什么依赖数组是空的?"

因为 AI 在生成代码时是按"模式匹配"而非"逻辑推理"来写的。它记住了"useEffect 传空数组 = 只在挂载时执行"这个模式,却没有推理出这个场景下 filters 应该放在依赖里。

正确的修复:

useEffect(() => {  fetchEmployees(filters).then(setEmployees);}, [filters]); // ✅ 依赖 filters,变化时重新请求更隐蔽的变体是 AI 写出了 stale closure:useEffect(() => {  const timer = setInterval(() => {    console.log(filters); // 永远是最初的值  }, 3000);  return () => clearInterval(timer);}, []); // filters 没在依赖里!

Vue 翻车现场

Vue 版本的问题更隐蔽。AI 可能写出:

watch(filters, (newVal) => {  fetchEmployees(newVal).then(setEmployees)}, { deeptrueimmediatetrue })

这段代码单看没问题。但 AI 接着又在同一组件里写了另一个 watch:

watch(employees, (newVal) => {  filters.dept = newVal[0]?.dept  // 在 watch 里改 watch 的源头})

两个 watch 互相触发 → 死循环。Vue 不会崩溃,但你的页面会一直发请求直到用户关掉标签页。

原理挖一挖

React 的 useEffect:它是一个"声明式的副作用契约"。依赖数组是契约里的条款——你告诉 React"这些变量变了才执行我"。AI 不理解的恰恰是"为什么要把变量列出来",它看不到闭包捕获的陷阱。

Vue 的 watch:它是"显式的观察者"。问题不在于 watch 本身,而在于"观察者模式中最忌讳的事情——在回调里修改被观察的对象"。AI 没有"副作用纯度"的概念。


AI 时代的"不变量"

回顾这三个雷区:

雷区
AI 翻车原因
你需要懂的
渲染粒度
不知道全量渲染 vs 精确更新的代价
组件执行模型(React 函数树 / Vue 依赖网)
列表 key
把 key 当成"随便填的参数"
Virtual DOM 复用机制
副作用管理
不理解依赖数组 ≠ 配置项
闭包捕获、观察者模式的反模式

AI 能在一分钟内生成一个完整组件。但它理解不了"为什么这个组件跑起来又卡又慢"——因为它没有心智模型。

前端的价值正在从"写"转向"审"(review + 架构决策)。 你不需要比 AI 写得快,但你需要知道 AI 写的东西哪些是对的、哪些是错的。看懂渲染,是你的定价权。

下次 AI 给你生成了一段代码,别直接复制粘贴。花 30 秒看一下:memo 加了吗?key 对吗?依赖填齐了吗?

它不懂的事,你来兜底。


本文是"AI 时代的硬通货"系列第一期。如果你也想聊聊哪些前端底层原理让你觉得"懂了就是不一样",欢迎留言。