@react-three/eslint-plugin 使用指南:用 ESLint 规则杜绝 React Three Fiber 帧循环中的性能陷阱
【免费下载链接】react-three-fiber🇨🇭 A React renderer for Three.js项目地址: https://gitcode.com/GitHub_Trending/re/react-three-fiber
导读
本文围绕 react-three-fiber 仓库中 packages/eslint-plugin 这个官方 ESLint 插件展开,讲解如何在 React Three Fiber 项目中通过静态检查,自动发现并拦截useFrame帧循环中"克隆向量""实例化新对象"两类典型的性能隐患。读完本文,你将掌握该插件的安装方式、recommended与all两套共享配置的用法、两条核心规则的触发条件与修复范式,并理解规则背后基于 AST 的实现原理与测试用例。
为什么需要"帧循环专用"的 ESLint 规则
React Three Fiber 的场景逻辑大量运行在useFrame回调中——这个回调在每个渲染帧(通常每秒 60 次以上)都会被调用。Three.js 中的Vector3、Quaternion、Matrix4等数学对象都是可变的 CPU 容器,其构造与销毁成本远高于普通 JavaScript 对象:
- 每帧
new一个对象,一秒钟就会产生 60+ 次分配,长时间运行会持续造成内存压力; - GPU 资源(如
BufferGeometry、Texture)没有可靠的垃圾回收机制,反复创建容易造成显存泄漏; - 触发垃圾回收(GC)时会造成主线程卡顿,直接表现为掉帧。
这些是 React Three Fiber 场景中非常常见的性能反模式。通用 ESLint 规则无法理解useFrame的语义,因此官方在 packages/eslint-plugin 中提供了专门面向该场景的插件。该包版本为 0.1.2,在 package.json 中声明依赖eslint ^8.12.0,作为开发依赖安装即可。
安装与基础配置
安装插件
在项目根目录执行:
npm install @react-three/eslint-plugin --save-dev安装完成后,在 ESLint 配置中完成注册与规则启用。
方式一:使用共享配置(推荐)
在.eslintrc或eslint.config对应的extends字段中加入:
{ "extends": [ "plugin:@react-three/recommended" ] }recommended配置会同时完成两件事:注册@react-three插件,并把两条规则都设置为error级别。该配置的真实内容位于 src/configs/recommended.ts:
export default { plugins: ['@react-three'], rules: { '@react-three/no-clone-in-loop': 'error', '@react-three/no-new-in-loop': 'error', }, }方式二:手动注册插件与规则
如果不想使用共享配置,也可以手动声明插件并逐条启用规则:
{ "plugins": ["@react-three"], "rules": { "@react-three/no-clone-in-loop": "error" } }注意:不通过共享配置时,必须自行把@react-three加入plugins,否则规则无法生效。从 src/index.ts 可以看出,插件导出rules与configs(内含all和recommended)两个入口,这也是 ESLint 解析plugin:@react-three/recommended时的底层依据。
规则总览
| 规则 | 描述 | recommended | 可自动修复 | 编辑器建议 |
|---|---|---|---|---|
| no-clone-in-loop | 禁止在帧循环中克隆向量,避免性能问题 | ✅ | ||
| no-new-in-loop | 禁止在帧循环中实例化新对象,避免性能问题 | ✅ |
两条规则当前均不支持--fix自动修复与编辑器建议(💡)——因为"如何消除分配"依赖开发者对场景逻辑的判断,无法安全地自动改写。规则列表由 scripts/codegen.ts 从源码生成,README 中标注了yarn codegen:eslint命令。
规则一:no-clone-in-loop —— 禁止在帧循环中克隆向量
问题场景
Vector3等 Three.js 数学类的.clone()会返回一个全新的实例。如果把它放进useFrame,就会每帧分配一个新对象:
function Direction({ targetPosition }) { const ref = useRef() useFrame(() => { const direction = ref.current.position.clone().sub(targetPosition).normalize() }) return <mesh ref={ref} /> }这段代码每秒会创建 60 个以上的临时Vector3,持续分配大量内存,且每次帧回调结束后这些临时对象都要等待 GC 回收。
正确写法一:模块级共享引用
在组件外部创建一次Vector3,在帧循环中通过copy复用:
const tempVec = new THREE.Vector3() function Direction({ targetPosition }) { const ref = useRef() useFrame(() => { const direction = tempVec.copy(ref.current.position).sub(targetPosition).normalize() }) return <mesh ref={ref} /> }正确写法二:useMemo 惰性创建
如果希望实例跟随组件生命周期,可以用useMemo只创建一次:
function Direction({ targetPosition }) { const ref = useRef() const tempVec = useMemo(() => new THREE.Vector3()) useFrame(() => { const direction = tempVec.copy(ref.current.position).sub(targetPosition).normalize() }) return <mesh ref={ref} /> }两种写法的共同点是:只在帧循环外分配一次对象,帧循环内只做原地写入(copy/set等)。
源码实现原理
规则本体位于 src/rules/no-clone-in-loop.ts,其核心是一个基于 AST 选择器的监听器:
['CallExpression[callee.name=useFrame] CallExpression MemberExpression Identifier[name=clone]'](node) { ctx.report({ messageId: 'noClone', node }) }该选择器含义是:在useFrame(...)调用内部,找到任意MemberExpression中名为clone的标识符调用并上报。也就是说,只要clone出现在useFrame回调里,无论被调用者是position、quaternion还是其他对象,都会被标记为noClone错误。规则自带recommended: true标记,这也是共享配置默认开启它的来源。
测试用例验证
no-clone-in-loop.test.ts 用 ESLint 官方RuleTester验证了规则行为:
- 合法(valid):模块级
const vec = new THREE.Vector3()后在useFrame中copy(vec);useFrame内调用普通函数clone()(非成员方法);局部变量名为clone的赋值等; - 非法(invalid):
useFrame(() => { ref.current.position.clone() })会命中noClone消息。
这说明规则只针对useFrame上下文中的成员方法.clone(),不会误伤函数名或变量名恰好叫clone的情况。
规则二:no-new-in-loop —— 禁止在帧循环中实例化新对象
问题场景
在useFrame中直接new THREE.Vector3(...)同样每帧产生新对象。对 Three.js 这类大体积 CPU 容器以及 GPU 资源(如几何体、纹理)而言尤其糟糕,因为后者没有可靠的垃圾回收机制:
function MoveTowards({ x, y, z }) { const ref = useRef() useFrame(() => { ref.current.position.lerp(new THREE.Vector3(x, y, z), 0.1) }) return <mesh ref={ref} /> }正确写法一:模块级共享引用
用tempVec.set(x, y, z)原地写入替代new:
const tempVec = new THREE.Vector3() function MoveTowards({ x, y, z }) { const ref = useRef() useFrame(() => { ref.current.position.lerp(tempVec.set(x, y, z), 0.1) }) return <mesh ref={ref} /> }正确写法二:useMemo 惰性创建
function MoveTowards({ x, y, z }) { const ref = useRef() const tempVec = useMemo(() => new THREE.Vector3()) useFrame(() => { ref.current.position.lerp(tempVec.set(x, y, z), 0.1) }) return <mesh ref={ref} /> }源码实现原理
src/rules/no-new-in-loop.ts 的选择器更加直接:
['CallExpression[callee.name=useFrame] NewExpression'](node) { ctx.report({ messageId: 'noNew', node }) }只要useFrame回调内部存在任何NewExpression(new Xxx(...)表达式),就上报noNew。这意味着该规则覆盖的不只是Vector3,任何在帧循环里new出来的对象(Quaternion、Matrix4、Group、BufferGeometry等)都会被拦截。
测试用例验证
no-new-in-loop.test.ts 中:
- 合法(valid):在
useFrame外new THREE.Vector3()后在帧内copy(vec)或lerp(vec.set(x, y, z), 0.1);使用未加THREE.前缀的new Vector3()同样合法(因为new不在帧循环内); - 非法(invalid):
useFrame内new THREE.Vector3(x, y, z)与new Vector3(x, y, z)两种写法都会命中noNew。
可见规则对命名空间前缀(THREE.与否)不敏感,判断依据纯粹是"new表达式是否出现在useFrame内部"。
共享配置详解
recommended(推荐)
适合所有使用 React Three Fiber 的开发者,包含当前全部两条规则且均为error级别:
{ "extends": ["plugin:@react-three/recommended"] }all(全部规则)
all配置包含所有可用规则,当前与recommended内容一致(因为现有规则全部被标记为推荐):
{ "extends": ["plugin:@react-three/all"] }从 src/configs/all.ts 可以看到,all同样注册了@react-three插件并启用两条规则。将来插件新增未进入recommended的规则时,all会成为"无条件启用全部规则"的选项。
典型工作流与排查建议
在实际项目中,建议按以下步骤接入:
- 安装插件:
npm install @react-three/eslint-plugin --save-dev; - 在 ESLint 配置中加入
plugin:@react-three/recommended(或手动注册插件 + 启用单条规则); - 运行
npx eslint src检查代码,重点观察useFrame相关文件的报错; - 参照本文的"正确写法"把
clone()改为copy()、new Xxx()改为模块级或useMemo中的共享实例 + 原地set()写入; - 将 ESLint 接入编辑器与 CI,让帧循环性能问题在提交前暴露。
规则当前不支持--fix自动修复,因此推荐在编辑器保存时实时看到提示(💡编辑器建议也暂未提供),人工按上文模式重构。
小结
@react-three/eslint-plugin通过两条 AST 级规则,把"帧循环中避免对象分配"这一性能最佳实践固化成了可自动执行的静态检查:
no-clone-in-loop:拦截useFrame中的.clone()成员调用;no-new-in-loop:拦截useFrame中的任何new表达式;recommended/all两套共享配置开箱即用,规则与配置的映射由 src/rules/index.ts 与 src/configs 统一导出。
配合本文给出的"模块级共享引用"与"useMemo一次性创建"两种修复范式,即可在代码层面显著降低 React Three Fiber 场景的内存分配与 GC 压力,从源头规避掉帧与显存泄漏风险。
【免费下载链接】react-three-fiber🇨🇭 A React renderer for Three.js项目地址: https://gitcode.com/GitHub_Trending/re/react-three-fiber
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考