- name
- performance-optimization
- description
- Enforces 60fps rendering for Three.js scenes, bundle size optimization (<500KB initial,
<2MB total), Lighthouse performance score >90, memory leak prevention, efficient Three.js
patterns (instancing, LOD, culling), and performance budget monitoring for Black Trigram.
- license
- MIT
# Performance Optimization Skill
## Purpose
This skill ensures that Black Trigram maintains exceptional performance across all platforms, achieving consistent 60fps rendering, optimal bundle sizes, and efficient resource utilization while providing an immersive Korean martial arts experience.
## When to Apply
**Automatically trigger this skill when:**
- Creating or modifying Three.js scenes and components
- Adding new 3D models, textures, or visual effects
- Implementing animation systems or game loops
- Working with WebGL shaders or materials
- Adding third-party dependencies
- Optimizing rendering performance
- Implementing lazy loading or code splitting
- Adding performance-critical game logic
- Reviewing pull requests with performance impact
## Core Principles
### 1. 60fps Rendering Target (Essential for Combat)
**ALWAYS maintain 60fps (16.67ms per frame):**
✅ **Frame Budget Breakdown**
```typescript
// Frame time budget for 60fps
const FRAME_BUDGET_MS = {
TOTAL: 16.67, // Total frame time
GAME_LOGIC: 3.0, // Combat calculations, AI
PHYSICS: 2.0, // Collision detection, animations
RENDERING: 8.0, // Three.js draw calls
INPUT: 1.0, // Input processing
AUDIO: 1.0, // Audio updates
OVERHEAD: 1.67, // Browser overhead, GC
} as const;
// ALWAYS measure frame time
import { useFrame } from '@react-three/fiber';
export const PerformanceMonitor: React.FC = () => {
const frameTimesRef = useRef<number[]>([]);
const lastTimeRef = useRef(performance.now());
useFrame(() => {
const now = performance.now();
const deltaTime = now - lastTimeRef.current;
lastTimeRef.current = now;
// Track frame times (rolling window)
frameTimesRef.current.push(deltaTime);
if (frameTimesRef.current.length > 120) {
frameTimesRef.current.shift();
}
// Alert if frame time exceeds budget
if (deltaTime > FRAME_BUDGET_MS.TOTAL) {
console.warn(`Frame time exceeded: ${deltaTime.toFixed(2)}ms`);
}
// Calculate average FPS
const avgFrameTime =
frameTimesRef.current.reduce((a, b) => a + b, 0) /
frameTimesRef.current.length;
const fps = 1000 / avgFrameTime;
if (fps < 55) {
console.error(`Low FPS detected: ${fps.toFixed(1)}`);
}
});
return null;
};
```
✅ **Performance Profiling in Development**
```typescript
// ALWAYS profile performance-critical sections
import { useFrame } from '@react-three/fiber';
export const ProfiledCombatSystem: React.FC = () => {
useFrame(() => {
// Profile game logic
performance.mark('combat-logic-start');
updateCombatLogic();
performance.mark('combat-logic-end');
performance.measure(
'Combat Logic',
'combat-logic-start',
'combat-logic-end'
);
// Check measurement
const measure = performance.getEntriesByName('Combat Logic')[0];
if (measure.duration > FRAME_BUDGET_MS.GAME_LOGIC) {
console.warn(`Combat logic too slow: ${measure.duration.toFixed(2)}ms`);
}
performance.clearMarks();
performance.clearMeasures();
});
};
```
### 2. Bundle Size Optimization
**ALWAYS enforce bundle size limits:**
✅ **Bundle Size Targets**
```typescript
// vite.config.ts - Bundle size configuration
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Vendor chunk for React + Three.js
vendor: ['react', 'react-dom'],
three: ['three', '@react-three/fiber', '@react-three/drei'],
// Separate chunk for Korean assets
korean: ['./src/types/constants/korean'],
// Audio system separate chunk
audio: ['./src/audio/AudioProvider'],
},
},
},
// Minification settings
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // Remove console.log in production
drop_debugger: true,
pure_funcs: ['console.info', 'console.debug'],
},
},
// Bundle size warning thresholds
chunkSizeWarningLimit: 500, // 500KB warning
},
});
```
✅ **Bundle Size Monitoring**
```json
// package.json - Add bundle size analysis
{
"scripts": {
"build": "tsc && vite build",
"build:analyze": "vite-bundle-visualizer",
"build:check-size": "npm run build && bundlesize"
},
"bundlesize": [
{
"path": "./dist/assets/index-*.js",
"maxSize": "500 KB"
},
{
"path": "./dist/assets/vendor-*.js",
"maxSize": "300 KB"
},
{
"path": "./dist/assets/three-*.js",
"maxSize": "400 KB"
}
]
}
```
✅ **Lazy Loading for Non-Critical Code**
```typescript
// ALWAYS lazy load non-critical screens
import { lazy, Suspense } from 'react';
import { LoadingScreen } from '@/components/ui/LoadingScreen';
// Lazy load heavy screens
const CombatScreen = lazy(() => import('@/screens/CombatScreen'));
const SettingsScreen = lazy(() => import('@/screens/SettingsScreen'));
const TrainingScreen = lazy(() => import('@/screens/TrainingScreen'));
export const App: React.FC = () => {
return (
<Suspense fallback={<LoadingScreen korean="로딩 중" english="Loading" />}>
<Router>
<Routes>
<Route path="/" element={<IntroScreen />} />
<Route path="/combat" element={<CombatScreen />} />
<Route path="/settings" element={<SettingsScreen />} />
<Route path="/training" element={<TrainingScreen />} />
</Routes>
</Router>
</Suspense>
);
};
```
### 3. Three.js Performance Optimization
**ALWAYS use efficient Three.js patterns:**
✅ **Instancing for Repeated Geometry**
```typescript
// GOOD: Use instancing for particles, bullets, effects
import { Instances, Instance } from '@react-three/drei';
import { useMemo } from 'react';
import { KOREAN_COLORS } from '@/types/constants';
export const OptimizedParticles: React.FC<{ count: number }> = ({ count }) => {
// Pre-calculate positions once
const positions = useMemo(
() => Array.from({ length: count }, () => [
Math.random() * 20 - 10,
Math.random() * 10,
Math.random() * 20 - 10,
] as [number, number, number]),
[count]
);
return (
<Instances limit={count}>
<sphereGeometry args={[0.1, 8, 8]} />
<meshBasicMaterial color={KOREAN_COLORS.PRIMARY_CYAN} />
{positions.map((pos, i) => (
<Instance key={i} position={pos} />
))}
</Instances>
);
};
// BAD: Creating individual meshes (100x slower!)
export const UnoptimizedParticles: React.FC = () => {
return (
<>
{Array.from({ length: 1000 }).map((_, i) => (
<mesh key={i} position={[Math.random() * 20 - 10, 0, 0]}>
<sphereGeometry args={[0.1, 8, 8]} />
<meshBasicMaterial color={0x00ffff} />
</mesh>
))}
</>
);
};
```
✅ **Level of Detail (LOD) for Distance-Based Quality**
```typescript
// GOOD: Use LOD for characters at varying distances
import { Detailed } from '@react-three/drei';
export const OptimizedCharacter: React.FC<{ position: Vector3 }> = ({ position }) => {
return (
<Detailed distances={[0, 10, 20, 40]} position={position}>
{/* High detail (0-10 units) - 5000 triangles */}
<HighDetailCharacterMesh />
{/* Medium detail (10-20 units) - 1500 triangles */}
<MediumDetailCharacterMesh />
{/* Low detail (20-40 units) - 500 triangles */}
<LowDetailCharacterMesh />
{/* Billboard (40+ units) - 2 triangles */}
<CharacterBillboard />
</Detailed>
);
};
```
✅ **Frustum Culling and Object Culling**
```typescript
// GOOD: Enable frustum culling (default in Three.js)
// Manually cull objects outside view when needed
import { useFrame, useThree } from '@react-three/fiber';
import { useRef, useState } from 'react';
import * as THREE from 'three';
export const CulledObject: React.FC<{ position: Vector3 }> = ({ position }) => {
const meshRef = useRef<THREE.Mesh>(null);
const { camera } = useThree();
const [isVisible, setIsVisible] = useState(true);
useFrame(() => {
if (!meshRef.current) return;
// Check if object is in camera frustum
const frustum = new THREE.Frustum();
const projectionMatrix = new THREE.Matrix4().multiplyMatrices(
camera.projectionMatrix,
camera.matrixWorldInverse
);
frustum.setFromProjectionMatrix(projectionMatrix);
const visible = frustum.intersectsObject(meshRef.current);
if (visible !== isVisible) {
setIsVisible(visible);
}
});
// Don't render if not visible
if (!isVisible) return null;
return (
<mesh ref={meshRef} position={position}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial />
</mesh>
);
};
```
✅ **Memoize Geometries and Materials**
```typescript
// GOOD: Reuse geometries and materials
import { useMemo } from 'react';
import * as THREE from 'three';
import { KOREAN_COLORS } from '@/types/constants';
export const OptimizedMeshes: React.FC = () => {
// Create geometry once, reuse for all instances
const sharedGeometry = useMemo(
() => new THREE.BoxGeometry(1, 1, 1),
[]
);
// Create material once, reuse for all instances
const sharedMaterial = useMemo(
() => new THREE.MeshStandardMaterial({
color: KOREAN_COLORS.PRIMARY_CYAN,
metalness: 0.5,
roughness: 0.5,
}),
[]
);
// Cleanup on unmount
useEffect(() => {
return () => {
sharedGeometry.dispose();
sharedMaterial.dispose();
};
}, [sharedGeometry, sharedMaterial]);
return (
<>
<mesh geometry={sharedGeometry} material={sharedMaterial} position={[-2, 0, 0]} />
<mesh geometry={sharedGeometry} material={sharedMaterial} position={[0, 0, 0]} />
<mesh geometry={sharedGeometry} material={sharedMaterial} position={[2, 0, 0]} />
</>
);
};
// BAD: Creating new geometry/material for each mesh
export const UnoptimizedMeshes: React.FC = () => {
return (
<>
{Array.from({ length: 100 }).map((_, i) => (
<mesh key={i} position={[i, 0, 0]}>
<boxGeometry args={[1, 1, 1]} /> {/* New geometry each time! */}
<meshStandardMaterial color={0x00ffff} /> {/* New material each time! */}
</mesh>
))}
</>
);
};
```
✅ **Efficient useFrame Usage**
```typescript
// GOOD: Batch operations in useFrame
import { useFrame } from '@react-three/fiber';
export const OptimizedAnimations: React.FC = () => {
const objectsRef = useRef<THREE.Group>(null);
useFrame((state, delta) => {
if (!objectsRef.current) return;
Ver no GitHub