Three.js 퍼포먼스 최적화 가이드
이 문서는 Three.js 프로젝트에서 퍼포먼스를 극대화하기 위해 사용할 수 있는 다양한 기법들을 구간별로 정리한 문서입니다. 특히 고사양이 아닌 기기에서도 60fps 이상을 유지하기 위한 최적화 포인트를 다루며, 로딩 속도와 GPU/CPU 자원 사용도 함께 고려합니다.
✅ 목표
- 최소 60fps 이상 유지
- 저사양 기기 및 모바일 대응 (디양한 디바이스)
- GPU, CPU 부하 최소화
- 로딩 속도 개선 (텍스처/모델 최적화)
1. Monitoring (측정 도구 세팅)
1.1 FPS 측정
npm install stats.js

import Stats from 'stats.js';
const stats = new Stats();
stats.showPanel(0); // 0: fps, 1: ms, 2: memory
document.body.appendChild(stats.dom);
const tick = () => {
stats.begin();
// ... 업데이트 및 렌더
stats.end();
requestAnimationFrame(tick);
};
1.2 Chrome 프레임 제한 해제 (고성능 장비에서 진짜 성능 확인)
- 프레임 제한 해제 방법
- power preference를 'high-performance'로 설정하면 GPU 우선 사용
const renderer = new THREE.WebGLRenderer({
powerPreference: 'high-performance'
});
1.3 Draw Call 수 확인 (Spector.js)

- Chrome 확장 설치
- Draw Call(파란색) 수를 줄이는 것이 중요


- 1. Spector로 한 프레임 캡처
- 2. drawCall 수가 많은가? → instancing, batching 고려
- 3. state 변경이 많은가? → 재질, 텍스처 정렬
- 4. shader 복잡도가 높은가? → 불필요한 연산 줄이기
- 5. 텍스처 크기, 포맷이 과도한가? → 압축 및 크기 최적화
- 6. WebGL 설정이 비효율적인가? → clear / depth / blending 등 확인
더보기
1. Draw Calls (드로우 호출 횟수)
- 의미: GPU에게 "그려라"는 명령. 너무 많으면 CPU와 GPU 간 병목 발생
- Spector에서 확인 위치:
- Capture 창 → Commands 탭 → drawElements, drawArrays 등
- 최적화 방향:
- Mesh batching: 재질(material)이 같은 오브젝트들을 병합
- Instancing 사용: 같은 geometry를 여러 개 렌더링할 때 instanced mesh 사용
- Frustum Culling 활성화: 카메라 밖 오브젝트는 그리지 않음
- LOD (Level of Detail): 멀리 있는 오브젝트에 낮은 품질 geometry 사용
2. State Changes (상태 전환 비용)
- 의미: GPU 내부의 pipeline 상태가 바뀔 때 비용 발생
- 예: useProgram, bindTexture, blendFunc 등의 전환
- Spector에서 확인 위치:
- Command 리스트의 State Changes 아이콘 (붉은 사각형/초록 체크)
- 최적화 방향:
- 재질(material)을 가능한 한 공유하도록 정렬 (예: 같은 shader를 연속해서 사용)
- 여러 texture를 자주 바꾸는 경우, texture atlas를 고려
3. Frame Time / Command Count
- 의미: 한 프레임을 그리는데 걸리는 시간
- Spector에서 확인 위치:
- Capture 상단 바에서 Frame time, Command count 확인
- 최적화 방향:
- GPU 명령이 너무 많다면 draw call과 상태 전환을 줄이는 방향으로
4. Shaders 탭 (Shader 복잡도)
- 의미: 실행 중인 vertex/fragment shader 소스 확인 가능
- Spector에서 확인 위치:
- Shader 탭 → 현재 바인딩된 프로그램 확인 가능
- 최적화 방향:
- 불필요한 if/else, 반복, 연산 줄이기
- 실제로 쓰지 않는 uniform, attribute, varying 제거
- discard 사용 최소화 (비싼 명령임)
5. Textures 탭 (텍스처 분석)
- 의미: GPU 메모리를 많이 잡아먹는 자원
- Spector에서 확인 위치:
- 각 draw call에서 사용 중인 texture 목록, 크기 및 포맷 확인 가능
- 최적화 방향:
- 너무 큰 텍스처는 다운샘플링 (예: 4K → 2K)
- 사용하지 않는 mipmap은 비활성화 (generateMipmaps: false)
- 압축 포맷 (KTX, Basis 등) 사용 고려
6. Framebuffer 설정 확인
- clearColor, depthTest, blendFunc, stencilOp 등의 설정을 추적 가능
- 최적화 측면에서는 depth 테스트나 blending이 비효율적으로 설정되어 있는지 확인
1.4 Renderer info 로그로 출력

console.log(renderer.info);
2. General (일반적인 최적화)
2.1 JavaScript 최적화
- tick() 내 연산 최소화
- 루프, 배열, 조건문 최적화
2.2 불필요한 리소스 제거 (Dispose)
scene.remove(mesh);
mesh.geometry.dispose();
mesh.material.dispose();
3. Lights (조명)
3.1 조명 최소화 또는 제거
- 가능하면 빛 대신 베이크된 그림자 사용
- 최소한의 Light 사용 (AmbientLight, DirectionalLight 등 성능이 좋은 Light 사용)
3.2 조명 추가/제거 최소화
- 조명 추가/제거 시에는 재질 컴파일 발생으로 렌더 지연 가능성 있음.
4. Shadows (그림자)
4.1 그림자 비활성화 또는 베이크
renderer.shadowMap.enabled = false;
4.2 Shadow 영역 최소화 (카메라 제한)
directionalLight.shadow.camera.top = 3;
directionalLight.shadow.camera.bottom = -3;
directionalLight.shadow.camera.left = -6;
directionalLight.shadow.camera.right = 6;
directionalLight.shadow.camera.far = 10;
4.3 castShadow, receiveShadow 최소화
castShadow 그림자를 만들고, receiveShadow 그림자를 받는다. 오브젝트 주변에 오브젝트가 없어서 그림자를 받을 필요가 없다면 reciveShadow는 필요없으므로 꺼준다. 반대로 지형? 바닥 오브젝트는 그림자를 생성할 필요가 없으므로 castShadow가 필요가 없다.
mesh.castShadow = true;
mesh.receiveShadow = false;
4.4 Shadow 업데이트 수동 설정
autoUpdate를 끄고, needUpdate를 하면 최초 렌더링 이후에는 shadow 자동 업데이트를 하지 않는다.
renderer.shadowMap.autoUpdate = false;
renderer.shadowMap.needsUpdate = true;
5. Textures (텍스처)
5.1 해상도 최소화 (파일 크기 아님!)
- GPU 메모리 부담 줄이기 위해 필요한 최소 해상도만 유지
5.2 2의 제곱 해상도 유지
- ex) 516 * 516, 1024 * 1024
- Mipmap 사용을 위한 필수 조건
5.3 적절한 파일 형식
- tinyPng, Basis 사용
- .jpg / .png 또는 압축된 Basis 포맷 고려
- Basis 소개
6. Geometries (기하 구조)
6.1 BufferGeometry 사용
- 최신 Three.js는 자동 적용됨. 최신 버전이 아니라면 BufferGeometry를 사용.
- ex) SphereBufferGeometry(최신 버전에는 없음.)와 SphereGeomety
6.2 최대한 정점 변경 피하기
- 애니메이션은 vertex shader로 처리
6.3 Geometry 재사용하기
const geometry = new THREE.BoxGeometry();
const mesh1 = new THREE.Mesh(geometry, material);
const mesh2 = new THREE.Mesh(geometry, material);
6.4 Geometry 병합 (BufferGeometryUtils)
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
const merged = BufferGeometryUtils.mergeGeometries([...]);

// 반복해서 그리므로 좋은 코드가 아니다.
for (let i = 0; i < 50; i++) {
const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const material = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh(geometry, material);
mesh.position.x = (Math.random() - 0.5) * 10;
mesh.position.y = (Math.random() - 0.5) * 10;
mesh.position.z = (Math.random() - 0.5) * 10;
mesh.rotation.x = (Math.random() - 0.5) * Math.PI * 2;
mesh.rotation.y = (Math.random() - 0.5) * Math.PI * 2;
scene.add(mesh);
}

const geometries = [];
for (let i = 0; i < 50; i++) {
const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
geometry.translate(
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
);
geometries.push(geometry);
}
const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries);
const material = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh(mergedGeometry, material);
scene.add(mesh);
한 번에 그리므로 Draw Call이 적게 발생한다.
7. Materials (재질)
7.1 재질 재사용
const material = new THREE.MeshNormalMaterial();
new THREE.Mesh(geometry, material);
const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const material = new THREE.MeshNormalMaterial();
for (let i = 0; i < 50; i++) {
// 반복문 내부에서 반복적으로 생성되므로 성능 저하
// const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
// const material = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh(geometry, material);
mesh.position.x = (Math.random() - 0.5) * 10;
mesh.position.y = (Math.random() - 0.5) * 10;
mesh.position.z = (Math.random() - 0.5) * 10;
mesh.rotation.x = (Math.random() - 0.5) * Math.PI * 2;
mesh.rotation.y = (Math.random() - 0.5) * Math.PI * 2;
scene.add(mesh);
}
7.2 비용 낮은 재질 사용
- MeshBasicMaterial, MeshLambertMaterial, MeshPhongMaterial 우선 고려
- MeshStandardMaterial, MeshPhysicalMaterial은 비용이 높음
8. Meshes (메시)
8.1 InstancedMesh 사용
- 동일한 지오메트리/재질을 가진 수많은 메시를 하나의 인스턴스로 렌더링
const mesh = new THREE.InstancedMesh(geometry, material, count);
mesh.setMatrixAt(index, matrix);
const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const material = new THREE.MeshNormalMaterial();
for (let i = 0; i < 50; i++) {
const mesh = new THREE.Mesh(geometry, material);
mesh.position.x = (Math.random() - 0.5) * 10;
mesh.position.y = (Math.random() - 0.5) * 10;
mesh.position.z = (Math.random() - 0.5) * 10;
mesh.rotation.x = (Math.random() - 0.5) * Math.PI * 2;
mesh.rotation.y = (Math.random() - 0.5) * Math.PI * 2;
scene.add(mesh);
}
위 코드 대신 InstanceMesh 사용 (아래 방식이 상자를 그릴 때 Draw Call이 1 / 50로 적음.)
// 0.5 × 0.5 × 0.5 크기의 박스 지오메트리를 생성
const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
// 표면 법선(normal)에 따라 색상이 달라지는 머티리얼 (디버깅용 또는 시각 테스트용)
const material = new THREE.MeshNormalMaterial();
// InstancedMesh 생성: 동일한 geometry + material을 공유하는 50개의 인스턴스를 GPU에 한 번에 업로드
// 성능 측면에서 일반 Mesh 50개를 각각 만드는 것보다 훨씬 효율적
const mesh = new THREE.InstancedMesh(geometry, material, 50);
scene.add(mesh); // 씬에 추가
// 50개의 인스턴스를 각기 다른 위치와 회전으로 배치
for (let i = 0; i < 50; i++) {
// 위치 벡터를 -5 ~ 5 범위 내에서 랜덤하게 생성
const position = new THREE.Vector3(
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
);
// 회전값을 오일러 → 쿼터니언으로 변환 (회전 방향이 랜덤이 되도록)
const quaternion = new THREE.Quaternion();
quaternion.setFromEuler(
new THREE.Euler(
(Math.random() - 0.5) * Math.PI * 2, // x축 회전: -180° ~ 180°
(Math.random() - 0.5) * Math.PI * 2, // y축 회전: -180° ~ 180°
0, // z축 회전은 고정 (회전 방향을 2D 평면처럼 단순화함)
),
);
// 4x4 변환 행렬 생성
const matrix = new THREE.Matrix4();
// 회전을 먼저 적용한 뒤
matrix.makeRotationFromQuaternion(quaternion);
// 위치를 설정하여 최종 변환 행렬로 구성
matrix.setPosition(position);
// i번째 인스턴스에 이 행렬을 적용 (위치 + 회전)
mesh.setMatrixAt(i, matrix);
}
9. Models (3D 모델)
9.1 Low Poly 모델 사용
- 디테일은 normalMap으로 대체
9.2 Draco 압축
- 고해상도 모델을 경량화
9.3 Gzip, Brotli 서버 압축 (모든 프로젝트에서 필수!)
- Gzip, Brotli: 서버가 텍스트 기반 파일을 압축해서 클라이언트에 빠르게 전달하는 기술
- .glb, .gltf 파일도 압축 가능하게 설정
10. Camera (카메라)
10.1 FOV 축소
- 시야각을 줄이면 렌더링 대상이 줄어듬
10.2 near / far 거리 축소
- 너무 멀거나 가까운 오브젝트는 렌더 제외
11. Renderer (렌더러)
11.1 Pixel Ratio 제한
- renderer.setPixelRatio(window.devicePixelRatio) 사용하지 않기
- 항상 아래와 같이 최대값을 지정해서 사용
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
11.2 Power Preference 설정
- GPU가 여러 개 있을 때(내장 그래픽 vs 외장 그래픽), 성능이 더 좋은 GPU를 사용하도록 요청하는 옵션
- 프레임 드랍이 발생하거나 VR, AR, 리얼타임 시뮬레이션, 고성능 그래픽이 필요한 경우에만 사용
- 일반적으로 사용하지 않아도 됨.
설정 값
- 'default': 브라우저/OS가 알아서 선택
- 'low-power': 전력 소모 적은 GPU (ex. 인텔 내장 그래픽)
- 'high-performance': 외장 GPU (ex. Nvidia, AMD) 사용 유도
주의사항:
- 브라우저가 반드시 고성능 GPU를 쓴다는 보장은 없음 → 단지 “요청”하는 것뿐
- 모바일 기기는 GPU가 1개뿐이라 효과 없음
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
powerPreference: 'high-performance',
antialias: true
})
11.3 Antialias
- 필요할 때만 antialias: true 설정
12. Post-processing (후처리)
12.1 Pass 수 제한
- 후처리 패스는 렌더링 해상도(픽셀 비율 포함)만큼의 픽셀을 사용하여 렌더링 (1920 x 1080에 2 픽셀을 사용 시에는 4백만이 넘는 연산이 발생)
- 지나치게 많은 pass는 수백만 픽셀 연산 발생
12.2 Pass 병합
- custom pass들은 한 셰이더로 통합 가능
13. Shaders (셰이더)
13.1 Precision 설정
// js
const shaderMaterial = new THREE.ShaderMaterial({
precision: 'lowp', // 이것과
...
...
fragmentShader: `
precision lowp float; // 이것이 동일
// fragmentShader.glsl
precision lowp float;
13.2 조건문 최소화
- if 대신 clamp, mix 함수 사용
// glsl에서
if(elevation < 0.25)
{
elevation = 0.25;
}
// if 대신 max나 clamp 함수 사용
elevation = max(elevation, 0.25);
// 이런 코드 대신
finalColor.r += depthColor.r + (surfaceColor.r - depthColor.r) * elevation;
finalColor.g += depthColor.g + (surfaceColor.g - depthColor.g) * elevation;
finalColor.b += depthColor.b + (surfaceColor.b - depthColor.b) * elevation;
// mix 사용
vec3 finalColor = mix(depthColor, surfaceColor, elevation);
13.3 텍스처 기반 노이즈 사용
- perlin noise 대신 texture2D(noiseTex, uv)
- Perlin noise를 직접 계산하는 대신, 이미 만들어진 노이즈 텍스처를 샘플링해서 사용하는 방식
// 미리 이미지로 만들어서 텍스처로 업로드한 뒤,쉐이더에서 uv로 색상을 샘플링해서 노이즈처럼 사용
uniform sampler2D noiseTex; // 노이즈 이미지 텍스처
varying vec2 vUv; // 표면에 대한 UV 좌표
void main() {
vec3 noise = texture2D(noiseTex, vUv).rgb;
// 이 noise 값을 불, 연기, 디졸브 등 다양하게 활용
gl_FragColor = vec4(noise, 1.0);
}
13.4 Defines 사용
- uniform으로 전달하는 것과 성능 차이는 크지 않지만, 상수로 고정되는 값이나 on/off 등의 값은 difine으로 전달하거나 적용하는 게 최적화에 좋음.
new THREE.ShaderMaterial({
defines: { uDisplacementStrength: 1.5 },
});
// shader 내부
#define uDisplacementStrength 1.5;
13.5 vertexShader 내 계산 처리
- vertexShader에서 계산 후 varying으로 fragmentShader로 전달하는 게 성능에 좋음.
- vertexShader는 정점을 기준으로 fragmentShader는 픽셀을 기준으로 계산하기 때문에, 같은 계산을 fragmentShader에서 하면 수백~수천 배 더 많이 호출
아래의 경우 fragment에서 계산이 필요함:
- 픽셀 단위 정밀도 필수(노이즈, 텍스처 샘플링, 라이트 셰이딩 등)
- gl_FragCoord 기반 계산(스크린 공간 기준 좌표라 vertex에서 접근 불가)
- 복잡한 머티리얼 표현(반사, 투명도, 다중 텍스처 블렌딩 등)
// Vertex Shader
varying vec3 vColor;
vColor = mix(colorA, colorB, weight);
// Fragment Shader
vec4 finalColor = vec4(vColor, 1.0);
fragmentShader의 다음 코드를:
fragmentShader: `
uniform sampler2D uDisplacementTexture;
varying vec2 vUv;
void main()
{
float elevation = texture2D(uDisplacementTexture, vUv).r;
elevation = max(elevation, 0.25);
vec3 depthColor = vec3(1.0, 0.1, 0.1);
vec3 surfaceColor = vec3(0.1, 0.0, 0.5)
vec3 finalColor = mix(depthColor, surfaceColor, elevation);
gl_FragColor = vec4(finalColor, 1.0);
}
`,
아래처럼 계산 후에 넘기는게 좋음.
vertexShader: `
uniform sampler2D uDisplacementTexture;
varying vec3 vColor;
void main()
{
// Position
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
float elevation = texture2D(uDisplacementTexture, uv).r;
modelPosition.y += max(elevation, 0.5) * DISPLACMENT_STRENGH;
gl_Position = projectionMatrix * viewMatrix * modelPosition;
// Color
float colorElevation = max(elevation, 0.25);
vec3 color = mix(vec3(1.0, 0.1, 0.1), vec3(0.1, 0.0, 0.5), colorElevation);
// Varying
vColor = color;
}
`,
fragmentShader: `
varying vec3 vColor;
void main()
{
gl_FragColor = vec4(vColor, 1.0);
}
`
✅ 결론 및 요약
- 측정부터 시작하자: stats.js, Spector.js, renderer.info
- 복잡한 모델보다 저용량 최적화
- Instancing, 병합을 적극 활용
- Post-processing은 필수만 최소화하여 사용
- Shader 연산은 vertex에서 끝내라!
🛠 추가 자료: Discover Three.js Tips and Tricks
'Graphic > ThreeJS' 카테고리의 다른 글
| 48 3D 위치에 HTML 요소 고정하기 (HTML과 WebGL 혼합) (3) | 2025.08.07 |
|---|---|
| 47 로딩 스크린과 인트로(Intro and loading progress) (2) | 2025.08.06 |
| 45 후처리(Post-Processing) (5) | 2025.08.06 |
| 31 MeshStandardMaterial 셰이더 확장(Modified materials) (2) | 2025.08.05 |
| 30 Animated galaxy (5) | 2025.08.05 |