El efecto de partículas flotando por el fondo de un hero está sobreusado, pero cuando encaja con la marca queda bien. El problema es que el 80% de las implementaciones que ves por ahí frenan la web entera. Aquí van las tres opciones, ordenadas de más control a más rapidez, cada una con una demo en vivo para que veas exactamente cómo se comporta antes de meterla en tu sitio.

1. La opción «sin librería»: canvas vanilla — recomendada

~100 líneas de JavaScript puro, cero dependencias, cero peso añadido al bundle, control absoluto sobre cada partícula. Es lo que hay debajo de cada demo de este artículo. La versión más popular es el efecto «network» o «constellation»: puntos que se mueven y se conectan con líneas cuando están cerca.

Network · 60 partículas

Lo que tienes que sustituir si quieres algo más calmado o más intenso:

  • count — número de partículas. Más de 200 empieza a notar en móviles modestos.
  • color — color de partículas. Usa el de marca para que se integre.
  • El umbral de conexión (distancia a la que se dibuja la línea) — más alto = más malla densa.

El código base se reduce a esto:

const canvas = document.getElementById('hero-bg');
const ctx = canvas.getContext('2d');
let particles = Array.from({ length: 60 }, () => ({
  x: Math.random() * canvas.width,
  y: Math.random() * canvas.height,
  vx: (Math.random() - 0.5) * 0.5,
  vy: (Math.random() - 0.5) * 0.5,
  r: 1 + Math.random() * 2,
}));

function frame() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  particles.forEach(p => {
    p.x += p.vx; p.y += p.vy;
    if (p.x < 0 || p.x > canvas.width)  p.vx *= -1;
    if (p.y < 0 || p.y > canvas.height) p.vy *= -1;
    ctx.fillStyle = 'rgba(234,88,12,0.9)';
    ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill();
  });
  for (let i = 0; i < particles.length; i++) {
    for (let j = i + 1; j < particles.length; j++) {
      const d = Math.hypot(particles.x - particles.x, particles.y - particles.y);
      if (d < 120) {
        ctx.strokeStyle = `rgba(234,88,12,${(1 - d / 120) * 0.35})`;
        ctx.beginPath();
        ctx.moveTo(particles.x, particles.y);
        ctx.lineTo(particles.x, particles.y);
        ctx.stroke();
      }
    }
  }
  requestAnimationFrame(frame);
}
frame();

Variante: nieve

Cambia el comportamiento — las partículas caen lentamente y se «teletransportan» al techo cuando salen por abajo. Útil para landings de productos invernales o decoración estacional:

Snow · 80 partículas

Variante: burbujas

Suben desde el fondo y se desvanecen al llegar arriba. Encaja con marcas de fitness, bebidas o salud:

Bubbles · 35 partículas

Las tres variantes corren a 60 fps incluso en móviles de gama media porque:

  • Se pausan cuando el canvas no es visible (IntersectionObserver).
  • El requestAnimationFrame respeta la frecuencia del display y se ralentiza cuando la pestaña no está activa.
  • No hay garbage collection en cada frame — el array de partículas se reutiliza.

2. La opción «con librería»: tsParticles

El sucesor mantenido de la mítica particles.js de Vincent Garreau. Se llama tsParticles (GitHub) y conserva la API original — mismo JSON de config, mejor rendimiento, compatibilidad con React, Vue, Angular, Svelte.

Cuándo elegirla en lugar del canvas vanilla:

  • Necesitas modos interactivos avanzados (las partículas reaccionan al cursor, repulsión, atracción).
  • Quieres que un no-dev pueda tocar la configuración vía JSON.
  • Vas a usar varios efectos distintos en distintas páginas y no quieres reinventar la rueda para cada uno.

Coste: ~30 KB gzipped si solo cargas el core y los componentes que usas. ~100 KB si cargas el bundle completo. Verifica con tu setup de bundler — el tree-shaking funciona si haces los imports correctos.

3. La opción «pura CSS»: para fondos decorativos sin JS

Cuando solo necesitas «movimiento sutil de fondo» y no interactividad real, una animación CSS pura es más barata que cualquier canvas. Cero JavaScript ejecutándose, cero impacto en CPU una vez compilada la animación.

@keyframes vbe-ca-float-dadf {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-18px) rotate(6deg); }
}

.box {
  animation: vbe-ca-float-dadf 2.4s ease-in-out infinite;
}

Funciona perfecto para:

  • Iconos que «respiran» (pulse).
  • CTAs que destacan en hover (glow).
  • Cards que entran al hacer scroll (slide-in).

Aquí tienes algunos ejemplos más para que veas el repertorio:

Pulse — para llamadas a la acción

@keyframes vbe-ca-pulse-bc22 {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.18); }
}

.box {
  animation: vbe-ca-pulse-bc22 1.6s ease-in-out infinite;
}

Bounce — para indicadores «hay algo aquí»

@keyframes vbe-ca-bounce-e4ab {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-26px); }
}

.box {
  animation: vbe-ca-bounce-e4ab 1s ease-in-out infinite;
}

Glow — para destacar en oscuro

@keyframes vbe-ca-glow-e5df {
  0%, 100% { box-shadow: 0 0 0   rgba(234,88,12,0.0); }
  50%      { box-shadow: 0 0 32px rgba(234,88,12,0.9); }
}

.box {
  animation: vbe-ca-glow-e5df 1.8s ease-in-out infinite;
}

Slide-in — para entrar elementos al hacer scroll

@keyframes vbe-ca-slide-in-12e0 {
  0%   { transform: translateX(-160%); opacity: 0; }
  100% { transform: translateX(0);      opacity: 1; }
}

.box {
  animation: vbe-ca-slide-in-12e0 0.7s cubic-bezier(.2,.7,.3,1.2) 1;
}

4. Qué mide Lighthouse y cómo no aparecer en rojo

Los tres puntos donde te penaliza una mala implementación de fondo animado:

  1. Total Blocking Time (TBT). Si tu JS de partículas tarda más de 50 ms en inicializarse al cargar la página, suma a la métrica. Solución: usa setTimeout(..., 50) o un IntersectionObserver para no arrancar hasta que el canvas esté visible.
  2. Cumulative Layout Shift (CLS). Si reservas alto al canvas con CSS pero el JS lo redimensiona después, mueves todo. Solución: fija height y aspect-ratio desde CSS, el JS solo pinta dentro.
  3. CPU usage durante el scroll. Si el canvas sigue corriendo cuando ya no se ve, gastas batería. Solución: el IntersectionObserver mencionado arriba.

5. Bonus: cómo decidir entre los tres

NecesidadMejor opción
Hero único con efecto wowCanvas vanilla (opción 1)
Efectos consistentes en muchas páginastsParticles (opción 2)
Decoración sutil sin interactividadCSS puro (opción 3)
El cliente quiere «que las partículas sigan al ratón»tsParticles, ya viene resuelto
Bundle ya pesado y Lighthouse en rojoCanvas vanilla — el JS lo escribes una vez y olvidas

Sea cual sea la opción, recuerda la regla de oro: el efecto está al servicio del contenido, no al revés. Si las partículas distraen del CTA o del titular, baja la opacidad o el conteo hasta que dejen de hacerlo. Si después de bajarlas todavía sobran, quítalas.