用 Codex 帮你看 React 组件为什么越切越卡,第一步常常不是 Effect,而是 401:模型通道没配通,组件贴进去也问不动。先把通道接上 TaoToken——打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 建一把 Key,把 Codex 的 Base URL 填成 https://taotoken.net/api,末尾不要带 /v1;通道通了再回到组件里,让 Codex 一个 useEffect 一个 useEffect 地列清单。
真实场景往往是这样:页面第一次打开很顺,反复「进入 → 离开 → 再进入」几次之后开始掉帧;resize 拖一下触发三四次;WebSocket 推一条消息弹好几个 Toast;路由都切走了,请求还在跑。这不是 React 本身慢,而是副作用只创建、不销毁。Codex 完全有能力把这些翻出来——它能读组件、能对比 cleanup、能输出资源清单——前提是它得先能正常回话。401 挡住的是模型请求那一层,不是 React 那一层。
1. Codex 报 401,先分清是通道问题还是代码问题
1.1 401 出现在 Effect 排查之前,别急着改组件
Codex 发起请求时返回 401,含义很直接:这次请求没有通过鉴权。它和你的useEffect写得对不对没有任何关系,组件里有没有漏removeEventListener也不会让模型接口返回 401。很多人一看到报错就去翻组件代码,改了半天依赖数组,重启编辑器,报错依旧,最后才发现是 Key 没生效或者根本没配。
判断顺序建议固定下来:先确认 Codex 有没有读到配置,再确认配置里的 Key 是不是有效的,最后才看组件。顺序颠倒的话,你会拿一个连模型都调不通的环境去排查内存问题,等于在黑屋子里找东西。
还有一种情况是 Key 是对的,但环境变量只在某个终端窗口里export过,换一个窗口、换一个 IDE 内置终端,变量就没了。Codex 在 IDE 里跑和在终端里跑,读到的环境不一定一样,这也是 401 反复出现的原因之一。
1.2 在 TaoToken 建一把 Key,这一步只做一次
打开 TaoToken 注册并登录,进入控制台创建 API Key。这里的 Key 就是后面填进 Codex 的那把,本文统一用占位符YOUR_API_KEY表示,你自己替换成实际值即可。创建完先把它存在本地的密码管理器或者.env里,不要直接粘进和 Codex 的对话内容中,也不要把真实 Key 写进会提交到 Git 的文件。
同一个页面还能看到模型广场,模型 ID 就在那里列着。写配置时不要凭印象填一个「看起来像」的名字,模型 ID 以模型广场当时列表为准,列表里没有的写法一律不要写进配置文件,否则即便鉴权通过,也会收到模型不存在的报错。
准备材料其实就三样:一把有效的 API Key、一个从模型广场抄下来的模型 ID、一份 Codex 的配置文件。三样齐了,401 这一类问题基本不会再出现第二次。
2. 把 Codex 的 model_provider 指向 taoToken 的 API 地址
2.1 ~/.codex/config.toml 的最小可用写法
Codex 用的是config.toml,不是 Claude Code 那套环境变量。macOS 和 Linux 下路径是~/.codex/config.toml,Windows 下是%USERPROFILE%\.codex\config.toml。文件不存在就自己建一个,内容按下面这样写:
model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"几个位置值得单独说明。base_url就是前面反复强调的接口地址,末尾不要加/v1,也不要顺手把它换成官网首页地址——填进工具的是接口根地址,官网是给人看的,两个东西用途不同。env_key声明 Codex 去哪读 Key,你需要在 shell 里设置同名变量:
export TAOTOKEN_API_KEY=YOUR_API_KEYmodel填模型广场上抄下来的 ID。wire_api按模型广场里标注的接口类型来选,标注为 chat 就用chat,标注为 responses 就改成responses;拿不准时以模型广场当时的说明为准,不要自己猜。
2.2 别把 Claude Code 的变量套到 Codex 上
网上不少教程把ANTHROPIC_BASE_URL、ANTHROPIC_AUTH_TOKEN这几个变量贴到 Codex 的配置里,那是 Claude Code 的写法,Codex 不读这些名字。照抄的结果通常是:文件看起来改过了,Codex 还是按原来的默认通道走,或者干脆报鉴权失败。两个工具各用各的配置文件,混着写只会让排查更难。
改完config.toml之后,关掉当前的 Codex 会话重新开一次,让它重新读取配置和环境变量。改文件不重启,是仅次于写错地址的第二大坑。同一台机器上如果同时用两个工具,建议把 Key 分成两把来管理,出问题时一眼就能看出是哪条通道在报错。
3. 配通之后,先做一轮 401 与 404 的对照验证
3.1 用一句提示词确认 Codex 真的连上了
配置落地后别急着丢一大段组件代码进去。先发一句简单的话,比如「你好,请用一句话说明你当前使用的模型名称」。能正常收到回复,说明 Key、Base URL、模型 ID 三样都对上了。这一步的意义在于把「通道问题」和「代码问题」彻底分开,后面再出现报错,就可以放心往 React 那边找。
确认通道可用之后,再把组件源码贴过去,问题描述得具体一点,比如「这个页面反复进出五分钟后明显变卡,帮我检查所有 useEffect 的资源创建与清理」。Codex 拿到完整上下文,输出的清单才有针对性。
3.2 401、404 与模型不存在分别怎么认
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| 401 Unauthorized | Key 没配、拼错,或环境变量没在当前会话生效 | 重新在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 复制 Key,确认export后再启动 Codex |
| 404 Not Found | base_url末尾多写了/v1 | 改成https://taotoken.net/api |
| 模型不存在的报错 | model填了列表里没有的 ID | 以模型广场当时列表为准重新填写 |
看表的时候注意一点:401 和 404 经常被混着描述成「接口报错」,但它们的修法完全不同。401 是身份问题,404 是路径问题,模型不存在是参数问题。把这三类分开记录,下一次排查能省掉大量试错。
4. 让 Codex 真正去看 Effect:五类资源必须成对出现
4.1 事件监听:注册和移除必须是同一个函数引用
最典型的写法是只加不减:
useEffect(() => { const onResize = () => setWidth(window.innerWidth); window.addEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize); }, []);真正容易写错的是下面这种「看起来清理了」的版本:注册时传的是匿名箭头函数,清理时又写了一个新的箭头函数。两个函数对象不同,removeEventListener找不到对应项,监听器就留在window上。正确做法是把回调抽成一个具名变量,注册和移除共用同一个引用。判断标准很简单:进出页面五次之后点一下,如果处理函数执行了五次,就是没清干净。
4.2 Timer:句柄有没有存下来,决定了会不会叠加
useEffect(() => { const timer = setInterval(refreshStatus, 5000); return () => clearInterval(timer); }, [refreshStatus]);setInterval的返回值必须存进变量,清理函数里再clearInterval。不存句柄,计时器就没有任何办法被停掉,每进一次页面多一个,接口请求也跟着成倍上涨。setTimeout、requestAnimationFrame、轮询任务同理。
上面这段依赖数组里带了refreshStatus,这是有意的:如果这个函数每次渲染都是新引用,Effect 就会不断重建计时器,旧的那个虽然被清了,但请求频率会被打乱。稳妥做法是用useCallback固定它,或者把要读的最新值放进 ref,让 Effect 只建一次。
4.3 请求:AbortController 让卸载后的返回变成无效结果
useEffect(() => { const controller = new AbortController(); fetch("/api/orders", { signal: controller.signal }) .then((res) => res.json()) .then(setOrders) .catch((err) => { if (err.name !== "AbortError") console.error(err); }); return () => controller.abort(); }, []);页面关掉了,请求还在网上跑,几秒后返回再setState,这条链路不会直接让内存爆掉,但会白白占着网络连接和服务端资源,也可能用旧数据覆盖新页面的状态。用isMounted标志位只是「忽略结果」,请求本身照样在跑,能取消就取消。
4.4 订阅、Observer 与第三方实例:off、disconnect、destroy
useEffect(() => { const onOrderUpdated = (payload) => mergeOrder(payload); socket.on("order_updated", onOrderUpdated); return () => socket.off("order_updated", onOrderUpdated); }, [socket]);订阅和解绑要成对写,并且用同一个回调引用,否则同一条消息会被处理多次,界面上表现为 Toast 弹好几遍。ResizeObserver、MutationObserver、IntersectionObserver这类对象要在卸载时disconnect(),否则回调会一直持有 DOM 引用。
第三方库是另一类高发区。图表、地图、编辑器内部往往会自己创建 Canvas、Timer、全局监听,React 卸载组件时并不保证它会跟着清理。写集成代码时直接问 Codex 一句:「这个实例在卸载时需要调用哪个销毁方法?」常见答案就是destroy()、dispose()、remove()中的一个,拿到之后写进 cleanup。
4.5 依赖变化与全局实例:清理不只发生在卸载时
useEffect(() => { socket.subscribe(roomId); return () => socket.unsubscribe(roomId); }, [roomId, socket]);roomId从room-1变成room-2时,React 会先跑上一轮的清理,再执行新一轮。所以清理函数不是只在卸载时被调用,依赖变化同样会触发它。这也是为什么把 Effect 写成「依赖里的每个东西都能被正确释放」比只考虑卸载更安全。
反过来,WebSocket 这类应用级资源不要塞进频繁重渲染的组件里。用户状态一更新就新建连接,旧连接如果没清干净,就会同时存在多条。更合适的位置是独立的连接管理模块或 Context,页面组件只负责订阅和退订。
5. 把审查提示词和清理规则固化进项目
5.1 先别改代码,让 Codex 输出资源清单
排查内存问题时,直接说「帮我优化页面内存」得到的结果通常很泛。换成下面这种写法,Codex 会按条列清单,你自己对照着改:
先不要修改代码。逐个检查当前组件里的 useEffect,输出:创建了什么资源;是否添加事件监听;是否创建 Timer;是否发起网络请求;是否建立订阅或 WebSocket 监听;是否创建 Observer 或第三方实例;对应的 cleanup 写在哪里;依赖变化时旧资源会不会被释放;哪些 Effect 可能造成资源累积。
这份清单里最值得盯的是最后两条。很多组件每个 Effect 都有 cleanup,但某个资源在两个 Effect 里各建了一次,或者被放到了错误的依赖上,照样会累积。
另外要记住分工:Codex 负责生成和解释代码、对照组件找可疑位置;页面跑起来、点进去切出来、打开 DevTools 记录内存,这些动作必须由你在本地浏览器里完成,再把现象或截图贴回对话。不要指望它替你跑页面。
5.2 AGENTS.md 里写清 Effect 清理规则
项目根目录放一份规则文件,Codex 后续生成组件时会主动参照,省掉每次重复提醒:
# React Effect 清理规则 - addEventListener 必须对应 removeEventListener,且使用同一函数引用 - setInterval / setTimeout 必须保存句柄并在 cleanup 中清除 - fetch 必须评估 AbortController,卸载后不再 setState - subscribe 必须对应 unsubscribe,socket.on 必须对应 socket.off - Observer 必须在 cleanup 中 disconnect - 第三方实例必须检查 destroy / dispose / remove API - 全局连接不要由高频重渲染的组件创建 - 依赖变化的 Effect 必须确认上一轮资源先释放 - 内存问题用重复挂载测试或 Heap Snapshot 验证规则写具体一点,Codex 的输出质量差别很明显。「注意清理副作用」这种句子它读不出你要什么,「addEventListener 必须对应 removeEventListener」它就能直接照着写。
5.3 怎么确认这次是真的修好了
验证方式不用很复杂:打开 DevTools 的 Memory 面板,进页面记一次快照,离开页面,手动触发 GC,再进来,重复五到十次。如果内存从 100MB 一路涨到 200MB 以上、离开后也回不去,就说明还有东西没释放。
快照对比里重点看 Detached DOM nodes、Listener、Closure、Timer 这几类条目。组件已经离开路由,相关 DOM 却还被一个 window 上的监听器引用着,这条链路就是问题所在。把快照里的关键字贴给 Codex,让它解释「为什么这个 Closure 还持有已卸载组件的引用」,通常比只描述「页面有点卡」有效得多。
6. 这次调用记没记账,回控制台看一眼
6.1 用同一把 Key 在模型对话里发一条测试消息
配置保存、重启会话之后,可以先用同一把 Key 在 TaoToken 模型对话 里发一条消息,确认模型 ID 和 Base URL 都没填错。这一步和 Codex 里发的那句测试提示词作用一样,但更直观:模型选错、Key 失效,在对话页面上会立刻暴露出来,不用去翻日志。
6.2 长期写代码就看一眼套餐够不够
如果你打算每天让 Codex 读组件、查 Effect、对比快照,调用量会稳定增长。Coding Plan 页面能看到适配套餐,新 Key 在 控制台 API Keys 里创建,调用记录和用量也在同一处核对。刚配完通道就顺手对一下这次测试调用有没有被记上账,能省掉以后「明明发出去了却查不到」的困惑。
最后提醒一句:修好 Base URL 只是让 Codex 能说话,真正决定页面会不会越切越卡的,还是那些addEventListener、setInterval、fetch、socket.on、new Observer、createChart有没有对应的收尾动作。让 Codex 把每个副作用的「创建」和「销毁」两栏并排列出来,哪一栏空着,一眼就能看出来。