⚡ Performance

Cómo Optimizar Juegos Web para Móviles

📅 8 de Octubre, 2024 ⏱️ 10 minutos 📱 Mobile-First

60 FPS en móviles no es suerte, es ingeniería. Después de optimizar 12 juegos HTML5 para funcionar perfectamente en dispositivos de gama baja, hemos aprendido exactamente qué técnicas funcionan y cuáles son mitos.

Esta guía técnica te mostrará cómo transformar un juego laggy en una experiencia ultra fluida, incluso en móviles con 2GB de RAM.

⚡ Aprenderás:

🎨 Canvas Rendering: El Cuello de Botella #1

El 80% de los problemas de performance en juegos web vienen del rendering ineficiente. Estas técnicas arreglan el 90% de casos:

1. requestAnimationFrame > setInterval

MAL (30 FPS inconsistente):

setInterval(() => {
  gameLoop();
}, 16); // Intenta 60 FPS pero falla

BIEN (60 FPS sólido):

function gameLoop() {
  update();
  render();
  requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);

requestAnimationFrame sincroniza con el refresh rate del display. En móviles modernos (120Hz), obtienes 120 FPS gratis.

2. Clear Solo Lo Necesario

MAL (desperdicia 40% de performance):

ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear todo

BIEN (3x más rápido):

// Solo clear áreas que cambiaron
dirtyRects.forEach(rect => {
  ctx.clearRect(rect.x, rect.y, rect.w, rect.h);
});

3. Offscreen Canvas para Sprites Estáticos

No redibujes UI/backgrounds cada frame. Renderiza una vez en offscreen canvas:

const bgCanvas = document.createElement('canvas');
const bgCtx = bgCanvas.getContext('2d');
// Dibuja background UNA VEZ
drawComplexBackground(bgCtx);

// En gameLoop, solo copia:
ctx.drawImage(bgCanvas, 0, 0);

Ganancia: 60-70% menos CPU usage.

👆 Touch Events: Reduciendo Latencia

Mouse events en móviles tienen 300ms de delay (para detectar double-tap). Touch events son inmediatos:

Implementación Correcta

// Prevenir scroll accidental
canvas.addEventListener('touchstart', (e) => {
  e.preventDefault();
  const touch = e.touches[0];
  handleInput(touch.clientX, touch.clientY);
}, { passive: false });

// Touch move para gestures
canvas.addEventListener('touchmove', (e) => {
  e.preventDefault();
  const touch = e.touches[0];
  handleMove(touch.clientX, touch.clientY);
}, { passive: false });

Pro tip: { passive: false } permite preventDefault() pero reduce performance ligeramente. Solo usa donde necesites bloquear scroll.

📦 Lazy Loading: Carga Inteligente de Assets

Cargar todos los assets al inicio = loading infinito. Carga solo lo necesario:

Sistema de Prioridades

  1. Critical (loading screen): Logo, spinner, UI básico
  2. Gameplay Core: Player sprite, primeros obstáculos
  3. Secondary: Power-ups, efectos visuales
  4. Nice-to-have: Backgrounds complejos, música
async function loadAssets() {
  // Fase 1: Critical
  await loadCriticalAssets();
  startGame(); // Juego jugable
  
  // Fase 2: Background loading
  loadSecondaryAssets();
  loadNiceToHaveAssets();
}

Resultado: tiempo de carga 5s → 1.5s (perceived performance 3x mejor).

🧠 Memory Management

Móviles tienen RAM limitada. Juegos con memory leaks crashean en <5 minutos:

Técnicas Anti-Leak

// Object Pool Example
const particlePool = [];

function getParticle() {
  return particlePool.pop() || new Particle();
}

function releaseParticle(p) {
  p.reset();
  particlePool.push(p);
}

🔊 Audio Optimization

Audio mal implementado causa stuttering. Estas técnicas lo arreglan:

1. Web Audio API > HTML Audio

Web Audio API tiene mejor performance y control:

const audioCtx = new AudioContext();
const buffers = {};

// Load & decode
async function loadSound(name, url) {
  const response = await fetch(url);
  const arrayBuffer = await response.arrayBuffer();
  buffers[name] = await audioCtx.decodeAudioData(arrayBuffer);
}

// Play instantáneo
function playSound(name) {
  const source = audioCtx.createBufferSource();
  source.buffer = buffers[name];
  source.connect(audioCtx.destination);
  source.start(0);
}

2. Sprite Sheets de Audio

Combina múltiples SFX en un solo archivo. Reduce HTTP requests 10x:

// Define offsets
const sfxMap = {
  jump: { start: 0, duration: 0.3 },
  coin: { start: 0.3, duration: 0.2 },
  hit: { start: 0.5, duration: 0.4 }
};

// Play desde offset
source.start(0, sfxMap.jump.start, sfxMap.jump.duration);

📊 Métricas de Performance

🎯 Target FPS

Desktop: 60 FPS mínimo

Mobile High: 60 FPS

Mobile Low: 30 FPS estable

⏱️ Load Time

First Playable: < 2s

Full Assets: < 5s

3G Network: < 8s

💾 Memory Usage

Inicial: < 50MB

Gameplay: < 100MB

Max Peak: < 150MB

📦 Bundle Size

HTML+JS: < 200KB

Images: < 500KB

Audio: < 300KB

🛠️ Herramientas de Profiling

✅ Checklist de Optimización

  1. ✅ requestAnimationFrame implementado
  2. ✅ Clear solo dirty rects
  3. ✅ Offscreen canvas para statics
  4. ✅ Touch events en lugar de mouse
  5. ✅ Lazy loading con prioridades
  6. ✅ Object pooling para partículas
  7. ✅ Event listeners cleanup
  8. ✅ Web Audio API para sonidos
  9. ✅ Assets < 1MB total
  10. ✅ Tested en device gama baja

🔬 Casos de Estudio: LIPA Studios

Stack Tower Neon

Antes: 25 FPS en móviles, stuttering al apilar

Después: 60 FPS estable, 0 frame drops

Cambios clave:

Neon Runner WOW

Antes: Lag en spawning de obstáculos, audio crackling

Después: 60+ FPS incluso a max velocidad

Cambios clave:

La optimización no es opcional en mobile gaming. Es la diferencia entre un juego que los usuarios cierran en 30s y uno que juegan por horas. Invierte el tiempo, vale cada minuto.

📚 Más sobre desarrollo web:
← Volver al Blog