CPU o GPU: la pregunta que decide todo lo demás
El método para determinar dónde está el cuello de botella, las pruebas concretas que lo distinguen, el árbol de decisión completo, y por qué optimizar el lado equivocado no produce ninguna mejora.
Todo lo que puedes hacer para acelerar una escena 3D pertenece a uno de dos grupos, y los dos grupos son mutuamente inútiles. Reducir draw calls no sirve absolutamente de nada si el problema es el relleno de píxeles. Bajar la resolución no sirve de nada si el problema son diez mil objetos. La CPU y la GPU trabajan en paralelo, y el frame dura lo que dure el más lento de los dos: mejorar el que ya iba sobrado no cambia ni un milisegundo. Determinar cuál es cuál cuesta cinco minutos y es la única decisión que de verdad importa.
- Explicar por qué CPU y GPU forman una tubería y el frame lo marca el más lento.
- Aplicar las pruebas de aislamiento que distinguen un cuello del otro.
- Recorrer el árbol de diagnóstico hasta una causa concreta.
- Confirmar el diagnóstico con una medida de tiempo de GPU cuando esté disponible.
Dos procesadores en tubería
En cada frame la CPU prepara trabajo y la GPU lo ejecuta, y lo hacen solapados: mientras la GPU dibuja el frame n, la CPU ya está preparando el n+1. Si la CPU tarda 8 ms y la GPU 14, el frame dura 14 y la CPU pasa 6 ms esperando. Si la CPU tarda 20 y la GPU 5, el frame dura 20 y la GPU está ociosa tres cuartas partes del tiempo.
De ahí sale la consecuencia que hay que interiorizar: optimizar el lado que no es el cuello no produce ninguna mejora medible. Cero. Puedes reducir tus draw calls a la mitad y ver exactamente el mismo framerate, porque la GPU seguía siendo el freno. Es la causa número uno de tiempo perdido optimizando escenas 3D.
Lo que hace difícil el diagnóstico es que las herramientas cómodas —el perfilador del navegador, Stats— solo ven la CPU. La GPU es una caja negra que devuelve píxeles con retraso. Así que el método no consiste en medir la GPU, sino en provocar cambios y observar qué pasa.
Las pruebas de aislamiento
Son cuatro y cada una aísla una variable. Todas se hacen en menos de un minuto y no requieren ninguna herramienta.
Prueba 1: bajar la resolución. Es la prueba decisiva y la primera que hay que hacer.
// Renderizar a un cuarto de los pixeles.
renderer.setPixelRatio( 0.5 );
// O, de forma mas agresiva:
renderer.setSize( window.innerWidth / 2, window.innerHeight / 2, false );
El número de píxeles procesados cae a la cuarta parte. La CPU hace exactamente el mismo trabajo: los mismos objetos, los mismos draw calls, los mismos uniforms. Por tanto:
- Si el framerate mejora mucho, estás limitado por la GPU, y concretamente por el trabajo por píxel: fragment shaders, overdraw, post-proceso, transparencias.
- Si no cambia nada, la GPU no era el freno en la parte de fragmentos. Sigue investigando.
El tercer argumento false de setSize evita que Three.js toque el estilo CSS del canvas, así que el lienzo sigue ocupando lo mismo en pantalla y solo cambia la resolución interna. Es lo que quieres para que la prueba sea limpia.
Prueba 2: quitar la geometría. Deja un solo objeto en escena o pon scene.visible = false un momento.
- Si el framerate se dispara, el coste está en dibujar. Falta saber si en draw calls o en vértices, y eso lo dice
renderer.info. - Si sigue igual, el coste está fuera del render: tu lógica de JavaScript, la física, el raycasting, la actualización de datos.
Prueba 3: material trivial. Sustituye todos los materiales por uno básico con scene.overrideMaterial.
const trivial = new THREE.MeshBasicMaterial( { color: 0x808080 } );
// Un interruptor para comparar en caliente.
scene.overrideMaterial = pruebaActiva ? trivial : null;
Se dibujan los mismos objetos con los mismos vértices, pero el fragment shader pasa a ser trivial y desaparecen todas las texturas y luces.
- Si mejora mucho, el coste está en el sombreado por píxel: materiales caros, demasiadas luces, mapas pesados.
- Si no cambia, el coste está en el procesado de vértices o en la CPU.
Prueba 4: duplicar la carga. A veces el diagnóstico se confirma mejor añadiendo que quitando. Duplica el número de objetos manteniendo el tamaño en pantalla, y observa si el tiempo crece proporcionalmente. Si duplicar objetos duplica el tiempo pero duplicar píxeles no lo cambia, el diagnóstico es inequívoco.
El árbol de decisión
flowchart TB
A[La escena va lenta] --> B[Bajar la resolucion a la mitad]
B --> C{Mejora mucho el tiempo de frame}
C -- Si --> D[Limitado por GPU en fragmentos]
C -- No --> E[Ocultar toda la escena]
E --> F{Mejora mucho el tiempo de frame}
F -- No --> G[Limitado por CPU fuera del render]
F -- Si --> H[Mirar renderer info]
H --> I{Draw calls por encima de mil}
I -- Si --> J[Limitado por CPU en preparacion de comandos]
I -- No --> K{Triangulos en millones}
K -- Si --> L[Limitado por GPU en vertices]
K -- No --> M[Probar override material trivial]
M --> N{Mejora mucho}
N -- Si --> O[Limitado por GPU en sombreado]
N -- No --> P[Revisar sombras texturas y ancho de banda]
style A fill:#f38ba8,color:#11111b
style D fill:#fab387,color:#11111b
style G fill:#89b4fa,color:#11111b
style J fill:#89b4fa,color:#11111b
style L fill:#fab387,color:#11111b
style O fill:#fab387,color:#11111b
style P fill:#f9e2af,color:#11111bCada hoja lleva a un conjunto de soluciones distinto y no intercambiable:
Limitado por GPU en fragmentos. Bajar el pixelRatio, reducir el overdraw ordenando y recortando transparencias, simplificar el post-proceso, renderizar los efectos caros a media resolución, quitar alphaTest donde se pueda.
Limitado por CPU en preparación de comandos. Fusionar geometría estática, usar InstancedMesh, compartir materiales, reducir el número de objetos del grafo, reducir los pases de sombra.
Limitado por CPU fuera del render. Perfilar con el flame chart: aquí sí es la herramienta correcta. Suele ser física, raycasting por evento, recolección de basura, o actualizaciones de matrices innecesarias.
Limitado por GPU en vértices. LOD, simplificación, culling más agresivo, y revisar si el mapa de sombras está procesando geometría que no proyecta nada.
Limitado por GPU en sombreado. Materiales más simples, menos luces dinámicas, hornear iluminación, mapas de menor resolución.
Una comprobación de diez segundos que sirve de confirmación: cambia el tamaño de la ventana del navegador a una cuarta parte y observa. Es la misma prueba que bajar el pixelRatio pero sin tocar código, y funciona incluso sobre una aplicación de la que no tienes el fuente. Si la escena de otro va lenta y quieres saber por qué, es lo primero que puedes hacer.
Medir el tiempo de GPU de verdad
Las pruebas de aislamiento dan el diagnóstico y no dan el número. Para el número hacen falta consultas de marca temporal, que la GPU emite entre comandos y que se leen de forma asíncrona.
En WebGPURenderer, Three.js lo expone a través del renderer:
// Requiere que el backend tenga habilitadas las marcas temporales.
if ( renderer.hasFeature( 'timestamp-query' ) ) {
renderer.backend.trackTimestamp = true;
}
async function medirGPU() {
await renderer.resolveTimestampsAsync( 'render' );
console.log( 'GPU render:', renderer.info.render.timestamp.toFixed( 2 ), 'ms' );
await renderer.resolveTimestampsAsync( 'compute' );
console.log( 'GPU compute:', renderer.info.compute.timestamp.toFixed( 2 ), 'ms' );
}
resolveTimestampsAsync( tipo ) recoge los resultados pendientes y actualiza info.render.timestamp o info.compute.timestamp. Como la lectura es asíncrona, los valores corresponden a frames anteriores, no al actual; para monitorización continua eso da igual, y para medidas puntuales hay que promediar sobre varios frames.
Ese es exactamente el mecanismo sobre el que se apoya el Inspector de Three.js, que activa trackTimestamp y avisa por consola si el dispositivo no soporta la característica. Lo verás en la lección de inspección de frames.
En WebGL la funcionalidad existe a nivel de extensión —EXT_disjoint_timer_query_webgl2— pero Three.js no la expone en WebGLRenderer, así que hay que usarla a mano contra el contexto o quedarse con las pruebas de aislamiento. En la práctica, para WebGL, las pruebas de aislamiento son suficientes.
La consecuencia de la estructura en tubería que más sorprende en la práctica no es que optimizar el lado equivocado no sirva, sino algo peor: el cuello se desplaza en cuanto arreglas el que tenías, y lo hace de forma que puede hacerte creer que tu optimización no funcionó. Un escenario real: una escena a 30 fps limitada por CPU con 4000 draw calls. Fusionas y bajas a 400. Vuelves a medir y sigue a 30 fps. La conclusión intuitiva —“fusionar no ha servido de nada”— es exactamente la contraria de la verdad. Lo que ha pasado es que la CPU ha bajado de 33 ms a 12, y ahora el freno es la GPU, que siempre había tardado 33 ms y estaba oculta detrás de la CPU. Tu optimización funcionó perfectamente y destapó el segundo problema, que estaba ahí desde el principio. Si en ese momento revierte el cambio porque “no sirvió”, pierdes trabajo bueno y vuelves a un estado donde las dos cosas están mal. De ahí salen tres hábitos que separan la optimización sistemática de la aleatoria. El primero: rediagnostica después de cada cambio significativo, aunque el framerate no se haya movido. Las cinco pruebas de aislamiento cuestan un minuto y te dicen si acabas de cambiar de régimen. El segundo: mide siempre las dos magnitudes, no solo el framerate. Si registras el tiempo de CPU por frame junto al tiempo total, ves que la CPU bajó de 33 a 12 aunque el total siguiera en 33, y eso convierte una optimización aparentemente inútil en una victoria documentada. Y el tercero, el que más rentabilidad da a largo plazo: cuando midas, anota el régimen, no solo el número. “30 fps” no dice nada dentro de un mes; “30 fps, limitado por GPU en fragmentos, 4,2 ms de CPU” te dice exactamente dónde estabas y qué queda por hacer. Un cuaderno de medidas con esa forma es la diferencia entre optimizar una escena y dar palos de ciego durante una semana.
- Coge una escena que vaya lenta y aplica las cuatro pruebas de aislamiento en orden.
- Recorre el árbol hasta una hoja y escribe la conclusión en una frase antes de optimizar nada.
- Aplica una única optimización del grupo correspondiente y vuelve a medir.
- Repite el diagnóstico completo y comprueba si el régimen ha cambiado.
- Registra tiempo total, tiempo de CPU y régimen en una tabla a lo largo de cinco iteraciones.