Performance pitfalls
Performance pitfalls
three.js 中最重要的问题是,创建对象可能很昂贵,在你安装/卸载东西之前要三思而后行 你放到场景中的每一个材质或灯光都要进行编译,你创建的每一个几何体都要进行处理。如果可以的话,可以在全局范围内或本地共享材质和几何体。
const geom = useMemo(() => new BoxGeometry(), [])
const mat = useMemo(() => new MeshBasicMaterial(), [])
return items.map(i => <mesh geometry={geom} material={mat} ...
如下面这样尽可能复用:
import * as THREE from "three";
import React, { useRef, useMemo, useState, useEffect } from "react";
import { Canvas, extend, useThree, useFrame } from "@react-three/fiber";
import niceColors from "nice-color-palettes";
import { Effects } from "@react-three/drei";
import { SSAOPass, UnrealBloomPass } from "three-stdlib";
extend({ SSAOPass, UnrealBloomPass });
const tempObject = new THREE.Object3D();
const tempColor = new THREE.Color();
const data = Array.from({ length: 1000 }, () => ({
color: niceColors[17][Math.floor(Math.random() * 5)],
scale: 1,
}));
export function App() {
return (
<Canvas
gl={{ antialias: false }}
camera={{ position: [0, 0, 15], near: 5, far: 20 }}
>
<color attach="background" args={["#f0f0f0"]} />
<Boxes />
<Post />
</Canvas>
);
}
function Boxes() {
const [hovered, set] = useState();
const colorArray = useMemo(
() =>
Float32Array.from(
new Array(1000)
.fill()
.flatMap((_, i) => tempColor.set(data[i].color).toArray())
),
[]
);
const meshRef = useRef();
const prevRef = useRef();
useEffect(() => void (prevRef.current = hovered), [hovered]);
useFrame((state) => {
const time = state.clock.getElapsedTime();
meshRef.current.rotation.x = Math.sin(time / 4);
meshRef.current.rotation.y = Math.sin(time / 2);
let i = 0;
for (let x = 0; x < 10; x++)
for (let y = 0; y < 10; y++)
for (let z = 0; z < 10; z++) {
const id = i++;
tempObject.position.set(5 - x, 5 - y, 5 - z);
tempObject.rotation.y =
Math.sin(x / 4 + time) +
Math.sin(y / 4 + time) +
Math.sin(z / 4 + time);
tempObject.rotation.z = tempObject.rotation.y * 2;
if (hovered !== prevRef.Current) {
(id === hovered
? tempColor.setRGB(10, 10, 10)
: tempColor.set(data[id].color)
).toArray(colorArray, id * 3);
meshRef.current.geometry.attributes.color.needsUpdate = true;
}
const scale = (data[id].scale = THREE.MathUtils.lerp(
data[id].scale,
id === hovered ? 2.5 : 1,
0.1
));
tempObject.scale.setScalar(scale);
tempObject.updateMatrix();
meshRef.current.setMatrixAt(id, tempObject.matrix);
}
meshRef.current.instanceMatrix.needsUpdate = true;
});
return (
<instancedMesh
ref={meshRef}
args={[null, null, 1000]}
onPointerMove={(e) => (e.stopPropagation(), set(e.instanceId))}
onPointerOut={(e) => set(undefined)}
>
<boxGeometry args={[0.6, 0.6, 0.6]}>
<instancedBufferAttribute
attach="attributes-color"
args={[colorArray, 3]}
/>
</boxGeometry>
<meshBasicMaterial toneMapped={false} vertexColors />
</instancedMesh>
);
}
function Post() {
const { scene, camera } = useThree();
return (
<Effects disableGamma>
<sSAOPass args={[scene, camera]} kernelRadius={0.5} maxDistance={0.1} />
<unrealBloomPass threshold={0.9} strength={0.75} radius={0.5} />
</Effects>
);
}
Avoid setState in loops
即使 React 可以处理它,你也不会想每秒钟调用 60 或 120 次。抛开性能风险不谈,仅仅是连续设置数值是不够的,你需要有管理的帧延迟,否则你的项目将根据最终用户的系统以不同的速度运行。另外,threejs 中的许多更新都需要与更新标志(.needsUpdate = true)或强制函数(.updateProjectionMatrix())相匹配。最好习惯于这样的想法:threejs 本身是循环驱动的,而框架是反应式的。你需要两者,状态和道具的反应性,动画的循环。让 React 处理前者,而 Fiber 为后者提供一个出口:useFrame。这个钩子在一个组合的 framelop 中运行,包括 deltas 和更多。
❌ setState in loops is bad
const [x, setX] = useState(0)
useFrame(() => setX((x) => x + 0.1))
return <mesh position-x={x} />
❌ setState in fast intervals is bad
useEffect(() => {
const interval = setInterval(() => setX((x) => x + 0.1), 1)
return () => clearInterval(interval)
}, [])
❌ setState in fast events is bad
<mesh onPointerMove={(e) => setX((x) => e.point.x)} />
一般来说,你应该倾向于使用 Frame。只要组件是唯一会变动的实体,就可以考虑安全地变动 props。使用 deltas 而不是固定值,这样你的应用程序就可以不受刷新率的影响,在任何地方都能以同样的速度运行。
const ref = useRef();
useFrame((state, delta) => (ref.current.position.x += delta));
return <mesh ref={ref} />;
// Same goes for events, use references.
<mesh onPointerMove={(e) => (ref.current.position.x = e.point.x)} />;
// If you must use intervals, use references as well, but keep in mind that this is not refresh-rate independent.
useEffect(() => {
const interval = setInterval(() => ref.current.position.x += 0.1), 1)
return () => clearInterval(interval)
}, [])
Handle animations in loops
帧循环是你应该放置你的动画的地方。例如使用 lerp,或 damp。
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} />
或者,使用动画库。React-spring 有自己的框架-循环,并在 React 之外进行动画。Framer-motion 是另一个流行的选择。
import { a, useSpring } from '@react-spring/three'
function Signal({ active }) {
const { x } = useSpring({ x: active ? 100 : 0 })
return <a.mesh position-x={x} />
Do not bind to fast state reactively
使用状态管理程序和选择性状态是可以的,但对于快速发生的更新来说,就不是这样了,原因和上面一样。
❌ Don't bind reactive fast-state
import { useSelector } from 'react-redux'
// Assuming that x gets animated inside the store 60fps
const x = useSelector((state) => state.x)
return <mesh position-x={x} />
而应该定期主动获取:
useFrame(() => (ref.current.position.x = api.getState().x));
return <mesh ref={ref} />;
Don’t mount indiscriminately
在 threejs 中,完全不重新挂载是很常见的,见 discover-three 中的 “disposing of things” 部分。这是因为缓冲区和材料会被重新初始化/编译,这可能很昂贵。
❌ Avoid mounting runtime
{
stage === 1 && <Stage1 />
}
{
stage === 2 && <Stage2 />
}
{
stage === 3 && <Stage3 />
}
✅ Consider using visibility instead
<Stage1 visible={stage === 1} />
<Stage2 visible={stage === 2} />
<Stage3 visible={stage === 3} />
function Stage1(props) {
return (
<group {...props}>
...
React 18 引入了 startTransition 和 useTransition APIs 来推迟和安排工作和状态更新。使用这些来降低昂贵操作的优先级。自 Fiber canvases 的第 8 版以来,默认使用并发模式,这意味着 React 将安排和推迟昂贵的操作。你不需要做任何事情,但你可以玩玩实验性的调度器,看看用较低的优先级来标记操作是否会有变化。
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>
Don’t re-create objects in loops
尽量避免给垃圾收集器带来太多麻烦,在可以的情况下对对象进行重新分类。
❌ Bad news for the GC
useFrame(() => {
ref.current.position.lerp(new THREE.Vector3(x, y, z), 0.1)
})
✅ Better re-use object
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
Threejs 加载器为你提供了加载异步资产(模型、纹理等)的能力,但如果你不重复使用资产,它很快就会出现问题。
❌ 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;
}
✅ Cache and re-use objects
function Component() {
const texture = useLoader(TextureLoader, url)
return (
<mesh>
<sphereGeometry />
<meshBasicMaterial map={texture} />
</mesh>
)
}