AgentSkillsCN

ta-r3f-performance

R3F 与 Three.js 的性能优化技巧。无论是优化帧率、减少绘制调用,还是提升移动端性能,此技能都能为您提供专业指导。

SKILL.md
--- frontmatter
name: ta-r3f-performance
description: Performance optimization techniques for R3F and Three.js. Use when optimizing frame rate, reducing draw calls, mobile performance.
category: performance

R3F Performance Skill

"Optimize for mobile, scale up for desktop – 60 FPS is the goal."

When to Use This Skill

Use when:

  • FPS drops below 60
  • Targeting mobile devices
  • Rendering many objects
  • Implementing LOD systems
  • Debugging performance issues

Quick Start

tsx
// Performance-optimized Canvas
<Canvas
  dpr={[1, 2]} // Limit pixel ratio
  performance={{ min: 0.5 }} // Auto-reduce quality
  gl={{ antialias: false }} // Disable for mobile
>
  <Suspense fallback={null}>
    <Scene />
  </Suspense>
</Canvas>

The 16ms Budget (60 FPS)

SystemBudgetNotes
Input~1msEvent handling
Physics~3msRapier/Cannon updates
Game Logic~4msState, AI, animations
Render~5msThree.js draw calls
Buffer~3msSafety margin
Total16.67ms60 FPS target

Performance Monitoring Setup

Always establish performance monitoring before optimizing. Use Stats.js from drei:

tsx
import { Stats } from '@react-three/drei';

function Scene() {
  const showStats = import.meta.env.DEV;

  return (
    <>
      {showStats && <Stats />}
      {/* Your scene content */}
    </>
  );
}

// In App.tsx
<Canvas>
  <Stats className="stats-position" />
  <Scene />
</Canvas>

CSS for Stats Positioning

css
.stats-position {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1000;
}

Performance Benchmarks to Establish

MetricTargetHow to Measure
FPS60+Stats.js
Frame Time<16.67msStats.js MS
Draw Calls<100renderer.info.render.calls
Triangles<100krenderer.info.render.triangles
Textures<50MBrenderer.info.memory.textures
Geometries<20MBrenderer.info.memory.geometries

Reading Renderer Info (Development)

tsx
import { useThree } from '@react-three/fiber';

function PerfMonitor() {
  const { gl } = useThree();

  useEffect(() => {
    if (!import.meta.env.DEV) return;

    const info = gl.info;
    console.log('Render Info:', {
      calls: info.render.calls,
      triangles: info.render.triangles,
      points: info.render.points,
      lines: info.render.lines,
      textures: info.memory.textures,
      geometries: info.memory.geometries,
    });
  }, [gl]);

  return null;
}

Decision Framework

SymptomLikely CauseSolution
Low FPS everywhereToo many draw callsInstancing, merging
FPS drops on zoomLOD not implementedAdd LOD system
Mobile slowDPR too highLimit to 1.5
Memory growsDispose missingAdd cleanup
StutteringGC pressureObject pooling

Progressive Guide

Level 1: Basic Optimizations

tsx
// Limit device pixel ratio
<Canvas dpr={Math.min(window.devicePixelRatio, 2)}>

// Disable expensive features on mobile
const isMobile = /iPhone|iPad|Android/i.test(navigator.userAgent);

<Canvas
  shadows={!isMobile}
  gl={{
    antialias: !isMobile,
    powerPreference: 'high-performance',
  }}
>

Level 2: Instanced Rendering

tsx
import { Instances, Instance } from '@react-three/drei';

// Instead of 1000 separate meshes
function OptimizedTrees({ positions }) {
  return (
    <Instances limit={positions.length}>
      <cylinderGeometry args={[0.1, 0.3, 2]} />
      <meshStandardMaterial color="brown" />
      {positions.map((pos, i) => (
        <Instance key={i} position={pos} />
      ))}
    </Instances>
  );
}

Level 3: Level of Detail (LOD)

Using Drei's <Detailed> Component

tsx
import { Detailed, useGLTF } from '@react-three/drei';

function LODTree({ position }: { position: [number, number, number] }) {
  // Load all LOD levels at once
  const [low, mid, high] = useGLTF([
    '/assets/models/tree-low.glb',
    '/assets/models/tree-mid.glb',
    '/assets/models/tree-high.glb',
  ]);

  return (
    <Detailed
      distances={[0, 10, 30]}  // Distance thresholds for each LOD
      position={position}
    >
      {/* 0-10 units: High detail */}
      <primitive object={high.scene} />
      {/* 10-30 units: Medium detail */}
      <primitive object={mid.scene} />
      {/* 30+ units: Low detail */}
      <primitive object={low.scene} />
    </Detailed>
  );
}

Using Native Three.js LOD

tsx
import { useRef, useEffect } from 'react';
import { useGLTF } from '@react-three/drei';
import * as THREE from 'three';

function NativeLODTree({ position }: { position: [number, number, number] }) {
  const lodRef = useRef<THREE.LOD>(null);
  const [low, mid, high] = useGLTF([
    '/assets/models/tree-low.glb',
    '/assets/models/tree-mid.glb',
    '/assets/models/tree-high.glb',
  ]);

  useEffect(() => {
    if (!lodRef.current) return;

    // Add LOD levels with distance thresholds
    lodRef.current.addLevel(high.scene.clone(), 0);    // 0-10 units
    lodRef.current.addLevel(mid.scene.clone(), 10);   // 10-30 units
    lodRef.current.addLevel(low.scene.clone(), 30);   // 30+ units

    // Optional: Auto-update LOD based on camera
    lodRef.current.update(camera);
  }, [high, mid, low]);

  return <lod ref={lodRef} position={position} />;
}

Billboard LOD for Distance Objects

tsx
import { useRef, useEffect } from 'react';
import { useFrame, useThree } from '@react-three/fiber';
import { TextureLoader } from 'three';

function BillboardLOD({
  textureUrl,
  position,
}: {
  textureUrl: string;
  position: [number, number, number];
}) {
  const meshRef = useRef<THREE.Mesh>(null);
  const texture = new TextureLoader().load(textureUrl);
  const { camera } = useThree();

  // Make billboard always face camera
  useFrame(() => {
    if (meshRef.current && camera) {
      meshRef.current.lookAt(camera.position);
    }
  });

  return (
    <mesh ref={meshRef} position={position}>
      <planeGeometry args={[2, 4]} />
      <meshBasicMaterial
        map={texture}
        transparent
        side={THREE.DoubleSide}
      />
    </mesh>
  );
}

LOD Distance Guidelines

Object TypeNear (High)MidFar (Low)Billboard
Trees0-15m15-40m40-100m100m+
Buildings0-20m20-60m60-150m150m+
Characters0-10m10-25m25-50m-
Props0-10m10-30m30m+-

Polygon Budget per LOD Level

AssetLOD0 (Near)LOD1 (Mid)LOD2 (Far)Billboard
Character15K tris5K tris1K trisN/A
Vehicle20K tris8K tris2K trisN/A
Tree5K tris1K tris500 tris2 tris
Building10K tris3K tris1K tris2 tris

Learned from bugfix-005 retrospective (2026-01-22):

  • LOD systems reduce GPU workload for distant objects
  • Use <Detailed> from Drei for simpler React integration
  • Consider billboards for very distant objects (trees, props)

Level 4: Frustum Culling & BVH

tsx
import { useBVH } from '@react-three/drei';

function OptimizedMesh() {
  const meshRef = useRef();

  // Enable BVH for faster raycasting
  useBVH(meshRef);

  return (
    <mesh ref={meshRef} frustumCulled>
      <complexGeometry />
      <meshStandardMaterial />
    </mesh>
  );
}

Level 5: Object Pooling

tsx
// Pool for frequently created/destroyed objects
const bulletPool = useMemo(() => {
  const pool = [];
  for (let i = 0; i < 100; i++) {
    pool.push({
      active: false,
      position: new THREE.Vector3(),
      velocity: new THREE.Vector3(),
    });
  }
  return pool;
}, []);

function getBullet() {
  return bulletPool.find((b) => !b.active);
}

function releaseBullet(bullet) {
  bullet.active = false;
}

Mobile Optimization

FeatureDesktopMobile
Pixel Ratio2.01.0-1.5
ShadowsOnOff
Anti-aliasingMSAAOff
Post-processingFullMinimal
Draw calls< 200< 50
Polygons< 1M< 100K
tsx
// Mobile detection and config
const config = useMemo(() => {
  const isMobile = /iPhone|iPad|Android/i.test(navigator.userAgent);
  return {
    dpr: isMobile ? 1 : Math.min(window.devicePixelRatio, 2),
    shadows: !isMobile,
    antialias: !isMobile,
    maxDrawCalls: isMobile ? 50 : 200,
  };
}, []);

Memory Management

tsx
// CRITICAL: Dispose of Three.js objects
useEffect(() => {
  const geometry = new THREE.BoxGeometry();
  const material = new THREE.MeshStandardMaterial();

  return () => {
    geometry.dispose();
    material.dispose();
    // Also dispose textures
    if (material.map) material.map.dispose();
  };
}, []);

Anti-Patterns

DON'T:

  • Create objects inside useFrame
  • Use high polygon models without LOD
  • Skip dispose() calls
  • Use shadows on mobile without testing
  • Render invisible objects
  • Use uncompressed textures

DO:

  • Reuse Vector3, Quaternion instances
  • Implement LOD for complex scenes
  • Always dispose geometries and materials
  • Profile before and after optimizations
  • Use Instances for repeated objects
  • Compress textures (WebP, Basis)

Performance Monitoring

tsx
import { useFrame } from '@react-three/fiber';
import { useRef } from 'react';

function PerformanceMonitor() {
  const frameCount = useRef(0);
  const lastTime = useRef(performance.now());

  useFrame(() => {
    frameCount.current++;

    const now = performance.now();
    if (now - lastTime.current >= 1000) {
      console.log(`FPS: ${frameCount.current}`);
      frameCount.current = 0;
      lastTime.current = now;
    }
  });

  return null;
}

Checklist

Performance review:

  • DPR limited appropriately
  • Instancing used for repeated objects
  • LOD implemented for complex models
  • Dispose called on cleanup
  • No object creation in useFrame
  • Shadows disabled on mobile
  • Textures compressed
  • Draw calls under budget
  • FPS stable at 60

Common Performance Killers

  1. Too many draw calls → Use Instances
  2. High polygon count → Use LOD
  3. Unoptimized textures → Compress, resize
  4. No frustum culling → Enable frustumCulled
  5. Memory leaks → Call dispose()
  6. GC pressure → Object pooling
  7. Expensive shaders → Simplify, use mobile variants
  8. Post-processing → Limit on mobile

Related Skills

For R3F fundamentals: Skill("ta-r3f-fundamentals") For material optimization: Skill("ta-r3f-materials")

External References