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.
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:
Variante: burbujas
Suben desde el fondo y se desvanecen al llegar arriba. Encaja con marcas de fitness, bebidas o salud:
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
requestAnimationFramerespeta 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.
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
Bounce — para indicadores «hay algo aquí»
Glow — para destacar en oscuro
Slide-in — para entrar elementos al hacer scroll
4. Qué mide Lighthouse y cómo no aparecer en rojo
Los tres puntos donde te penaliza una mala implementación de fondo animado:
- 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 unIntersectionObserverpara no arrancar hasta que el canvas esté visible. - Cumulative Layout Shift (CLS). Si reservas alto al canvas con CSS pero el JS lo redimensiona después, mueves todo. Solución: fija
heightyaspect-ratiodesde CSS, el JS solo pinta dentro. - CPU usage durante el scroll. Si el canvas sigue corriendo cuando ya no se ve, gastas batería. Solución: el
IntersectionObservermencionado arriba.
5. Bonus: cómo decidir entre los tres
| Necesidad | Mejor opción |
|---|---|
| Hero único con efecto wow | Canvas vanilla (opción 1) |
| Efectos consistentes en muchas páginas | tsParticles (opción 2) |
| Decoración sutil sin interactividad | CSS 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 rojo | Canvas 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.