perfprmance 1x1

Performance pitfalls 性能陷阱

three.js中最重要的注意点是创建对象可能会很耗费资源,因此在挂载/卸载物体之前请三思!每个放入场景中的材质或光源都需要编译,每个创建的几何体都需要处理。如果可以的话,请共享材质和几何体,可以在全局范围或局部范围内实现。

Tips and Tricks 技巧和窍门

这里有一篇很好的总结性的文章: https://discoverthreejs.com/tips-and-tricks/

const geom = useMemo(() => new BoxGeometry(), [])
const mat = useMemo(() => new MeshBasicMaterial(), [])
return items.map(i => <mesh geometry={geom} material={mat} ...

在条件允许的情况下,尽可能地使用实例去实现元素的展示。

Avoid setState in loops 不要在循环中进行setState

简而言之,不要在useFrame里面使用setState相关操作。

Threejs有一个渲染循环,它的工作方式与DOM不同。通过更新数据在useFrame中进行快速渲染。useFrame是每个组件的渲染循环。

仅仅连续设置值是不够的,需要帧时间差。例如,不要考虑position.x += 0.1,而应该考虑position.x += delta,否则你的项目将在不同速度下运行,具体取决于最终用户的系统。在threejs中,许多更新需要与更新标志(.needsUpdate = true)或命令式函数(.updateProjectionMatrix())配对使用。

你可能会尝试在useFrame内部使用setState,但没有理由这样做。你这样做只会导致原本简单的数据更新变成了触发复杂的React调度机制和组件渲染机制。

❌setState in loops is bad 在循环中使用setState不好

✅Instead, just mutate, use deltas 要进行改变设置的时候用deltas

useEffect(() => {
  const interval = setInterval(() => setX((x) => x + 0.1), 1)
  return () => clearInterval(interval)
}, [])

❌setState in useFrame is bad 在useFrame中使用setState不好

const [x, setX] = useState(0)
useFrame(() => setX((x) => x + 0.1))
return <mesh position-x={x} />

❌setState in fast events is bad 在设置fast events时使用setState不好

<mesh onPointerMove={(e) => setX((x) => e.point.x)} />

所谓fast events就是react-three-fiber自己封装的,针对三维场景或者元素进行操作的一些事件,这些事件并不是DOM事件

通常情况下,您应该优先考虑使用useFrame。只要组件是唯一进行改变的实体,那么修改props是安全的。使用useFrame时,使用增量delta而不是固定值,以使您的应用程序独立于刷新率并在任何地方以相同的速度运行!

const ref = useRef()
useFrame((state, delta) => (ref.current.position.x += delta))
return <mesh ref={ref} />

使用fast events也是类似的

<mesh onPointerMove={(e) => (ref.current.position.x = e.point.x)} />

如果必须使用定时器,请同时使用Ref,但请记住这不能保证独立于设备显示刷新率。

useEffect(() => {
  const interval = setInterval(() => ref.current.position.x += 0.1), 1)
  return () => clearInterval(interval)
}, [])

Handle animations in loops 在循环中处理动画

帧循环(frame loop)是您应该放置动画的地方。例如,可以使用 lerp 或 damp。

✅ Use lerp + useFrame 在useFrame中使用lerp函数

function Signal({ active }) {
  const ref = useRef()
  useFrame((state, delta) => {
    ref.current.position.x = THREE.MathUtils.lerp(ref.current.position.x, active ? 100 : 0, 0.1)
  })
  return <mesh ref={ref} />

✅ or react-spring

import { a, useSpring } from '@react-spring/three'

function Signal({ active }) {
  const { x } = useSpring({ x: active ? 100 : 0 })
  return <a.mesh position-x={x} />

或者,使用动画库。React-spring 有自己的帧循环,并在 React 外部进行动画处理。Framer-motion 是另一个受欢迎的替代方案。

Do not bind to fast state reactively

不要和快速更新状态的行为关联

使用状态管理器和选择性状态是可以的,但对于出现快速更新的情况,由于与上述原因相同,不应使用。

❌ Don't bind reactive fast-state

import { useSelector } from 'react-redux'

// 假设x的值在以每秒60帧的速度变化
const x = useSelector((state) => state.x)
return <mesh position-x={x} />

✅ Fetch state directly 直接获取并通过Ref方式来设置

useFrame(() => (ref.current.position.x = api.getState().x))
return <mesh ref={ref} />

这里使用的是Zustand这个库(和Redux类似作用)

Don't mount indiscriminately 不要随意挂载组件

在 threejs 中,通常根本不需要重新挂载组件,请参见 discover-three 中的“disposing of things”部分。这是因为缓冲区和材质会被重新初始化/编译,这可能是昂贵的。

❌ Avoid mounting runtime 会导致重新挂载

{
  stage === 1 && <Stage1 />
}
{
  stage === 2 && <Stage2 />
}
{
  stage === 3 && <Stage3 />
}

✅ use startTransition for expensive ops

对于一些耗费性能的操作可以使用startTransition来处理

import { useTransition } from 'react'
import { Points } from '@react-three/drei'

const [isPending, startTransition] = useTransition()
const [radius, setRadius] = useState(1)
const positions = calculatePositions(radius)
const colors = calculateColors(radius)
const sizes = calculateSizes(radius)

<Points
  positions={positions}
  colors={colors}
  sizes={sizes}
  onPointerOut={() => {
    startTransition(() => {
      setRadius(prev => prev + 1)
    })
  }}
>
  <meshBasicMaterial vertexColors />
</Points>

React 18 引入了 startTransition 和 useTransition API 来调整和安排工作和状态更新。使用它们来降低耗费性能的操作的优先级。

自 Fiber 的第 8 版起,默认情况下 Canvas 使用了并发模式,这意味着 React 将安排和推迟耗费性能的操作。你不需要做任何事情,但你可以尝试一下实验性的调度器,并查看将操作标记为较低优先级是否有所不同。

Don't re-create objects in loops

不要在循环中重复创建三维相关数据

尽量避免给垃圾收集器创建太多的负担,可以时尽量重复使用对象!

❌ Bad news for the GC 对于垃圾收集器不好的消息

✅ Better re-use object 更好的重用方式

每秒钟创建一个新的向量,这将分配内存并迫使垃圾收集器最终介入。

useFrame(() => {
  ref.current.position.lerp(new THREE.Vector3(x, y, z), 0.1)
})

在全局或本地环境中设置可重用对象,现在垃圾收集器将不会介入。

function Foo(props)
  const vec = new THREE.Vector()
  useFrame(() => {
    ref.current.position.lerp(vec.set(x, y, z), 0.1)
  })

useLoader instead of plain loaders

使用useLoader代替一般加载

Three.js 加载器使您能够异步加载资源(模型、纹理等),但如果您不重用资源,它可能会很快变得有问题。

❌ No re-use is bad for perf 不重用对象会影响性能

这会为每个组件实例重新获取和解析。

function Component() {
  const [texture, set] = useState()
  useEffect(() => void new TextureLoader().load(url, set), [])
  return texture ? (
    <mesh>
      <sphereGeometry />
      <meshBasicMaterial map={texture} />
    </mesh>
  ) : null
}

相反,使用 useLoader,它可以缓存资源并在整个场景中使它们可用。

✅ Cache and re-use objects 缓存并重用加载的数据

关于 GLTF,尽可能使用 GLTFJSX,它可以创建不可变的 JSX 模型数据,甚至允许您重用完整的模型。

function Component() {
  const texture = useLoader(TextureLoader, url)
  return (
    <mesh>
      <sphereGeometry />
      <meshBasicMaterial map={texture} />
    </mesh>
  )
}