Cómo Añadir un Efecto Simple de Nieve a tu Sitio Web
Una animación estacional de vacaciones en un sitio web puede deleitar a los visitantes, pero la mayoría de los tutoriales ignoran lo que importa en producción: rendimiento, accesibilidad y experiencia de usuario. No querrás una animación decorativa de fondo que arruine tus Core Web Vitals o moleste a usuarios que prefieren movimiento reducido.
Esta guía te muestra cómo construir un efecto de nieve ligero basado en canvas que respeta las preferencias del usuario, se pausa cuando está invisible y no interfiere. También aprenderás cuándo tiene sentido un efecto de nieve CSS más simple.
Puntos Clave
- Canvas escala de manera más confiable que los enfoques basados en DOM una vez que superas un pequeño puñado de partículas
- Siempre respeta
prefers-reduced-motionpara honrar las preferencias de accesibilidad del usuario - Usa la API de Visibilidad de Página para pausar animaciones en pestañas en segundo plano y ahorrar recursos
- La nieve CSS pura funciona para implementaciones mínimas (5-10 copos de nieve) pero no escala bien
Por Qué Canvas para Animación de Nieve en JavaScript
Los enfoques basados en DOM crean elementos individuales para cada copo de nieve. Esto funciona para un puñado de partículas, pero escalar a docenas o cientos significa manipulación constante del DOM, recálculos de diseño y presión de memoria por la creación y eliminación de elementos.
Un efecto de nieve en canvas dibuja todo en un solo elemento. Controlas el bucle de renderizado, gestionas el estado de las partículas en arrays simples y evitas completamente la sobrecarga del DOM. Una vez que escalas más allá de un pequeño número de partículas o quieres un movimiento más suave, canvas se convierte en la opción predeterminada más predecible.
Compromisos a entender:
- Canvas requiere JavaScript—sin JS significa sin nieve
- El texto dentro de canvas no es accesible para lectores de pantalla (aceptable para efectos puramente decorativos)
- Las animaciones CSS no son “gratuitas”—todavía consumen recursos de CPU/GPU
Configurando el Elemento Canvas
Posiciona el canvas detrás de tu contenido con CSS:
#snowfall {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: -1;
}
La regla pointer-events: none asegura que el canvas nunca bloquee el desplazamiento, clics o cualquier interacción del usuario.
<canvas id="snowfall" aria-hidden="true"></canvas>
Añadir aria-hidden="true" indica a las tecnologías de asistencia que ignoren este elemento puramente decorativo.
Construyendo el Bucle de Animación
Aquí hay una implementación mínima con las protecciones adecuadas:
const canvas = document.getElementById('snowfall');
const ctx = canvas.getContext('2d');
let flakes = [];
let animationId = null;
function resize() {
const dpr = window.devicePixelRatio || 1;
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
}
function createFlake() {
return {
x: Math.random() * window.innerWidth,
y: -10,
radius: Math.random() * 3 + 1,
speed: Math.random() * 1 + 0.5,
opacity: Math.random() * 0.6 + 0.4
};
}
function update() {
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
if (flakes.length < 80 && Math.random() > 0.95) {
flakes.push(createFlake());
}
flakes = flakes.filter(f => {
f.y += f.speed;
ctx.beginPath();
ctx.arc(f.x, f.y, f.radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 255, 255, ${f.opacity})`;
ctx.fill();
return f.y < window.innerHeight + 10;
});
animationId = requestAnimationFrame(update);
}
resize();
window.addEventListener('resize', resize);
Esto maneja correctamente las pantallas de alta densidad de píxeles escalando el búfer del canvas para coincidir con devicePixelRatio. La llamada ctx.setTransform(1, 0, 0, 1, 0, 0) reinicia la matriz de transformación antes de aplicar la nueva escala, previniendo el escalado acumulativo al redimensionar la ventana.
Discover how at OpenReplay.com.
Protecciones Esenciales de Rendimiento y Accesibilidad
Respetando las Preferencias del Usuario
Los usuarios que configuran prefers-reduced-motion han solicitado explícitamente menos animación. Respeta eso:
const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
let prefersReduced = reducedMotionQuery.matches;
reducedMotionQuery.addEventListener('change', (e) => {
prefersReduced = e.matches;
if (prefersReduced) {
cancelAnimationFrame(animationId);
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
} else if (!document.hidden) {
update();
}
});
if (!prefersReduced) {
update();
}
Esta implementación escucha cambios en la preferencia de movimiento del usuario, permitiendo que la animación responda dinámicamente si la configuración cambia mientras la página está abierta.
Pausando Cuando Está Oculta
Ejecutar animaciones en pestañas en segundo plano desperdicia batería y CPU. La API de Visibilidad de Página soluciona esto:
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
cancelAnimationFrame(animationId);
} else if (!prefersReduced) {
update();
}
});
Cuándo Tienen Sentido los Efectos de Nieve CSS
Para implementaciones muy pequeñas—quizás 5-10 copos de nieve en una sección hero—un enfoque CSS puro evita JavaScript por completo:
.snowflake {
position: absolute;
color: white;
animation: fall 8s linear infinite;
}
@keyframes fall {
to { transform: translateY(100vh); }
}
Esto funciona para casos de uso decorativos mínimos pero no escala. Cada elemento todavía activa la composición, y pierdes el control programático sobre la densidad y el comportamiento.
Opciones de Personalización
Ajusta estos valores para que coincidan con la estética de tu sitio:
- Densidad: Cambia el límite de
80y el umbral de aparición0.95 - Rango de velocidad: Modifica el cálculo
Math.random() * 1 + 0.5 - Tamaño: Ajusta el cálculo del radio
- Alcance: Apunta a un contenedor específico en lugar de
window.innerWidth/Height
Conclusión
Una animación de vacaciones en un sitio web debe añadir a la experiencia sin comprometerla. Usa canvas para rendimiento escalable, respeta prefers-reduced-motion, pausa cuando la página no esté visible y mantén el efecto no interactivo. Comienza con conteos conservadores de partículas y ajusta basándote en pruebas reales de dispositivos—no en suposiciones sobre lo que los navegadores pueden manejar.
Preguntas Frecuentes
Una animación canvas bien implementada tiene un impacto mínimo en los Core Web Vitals. Dado que canvas renderiza a un solo elemento y usa requestAnimationFrame, no causará cambios de diseño ni bloqueará el hilo principal. Mantén los conteos de partículas razonables (menos de 100) y pausa la animación cuando la pestaña esté oculta para mantener buenos puntajes de rendimiento.
Sí. Añade una propiedad de deriva a cada objeto de copo y actualiza la posición x en tu bucle de animación junto con la posición y. Usa Math.sin con un desplazamiento basado en tiempo para una oscilación de aspecto natural, o aplica un valor horizontal constante para viento constante. Aleatoriza los valores de deriva por copo para variedad.
Reemplaza window.innerWidth y window.innerHeight con las dimensiones de tu contenedor objetivo. Usa getBoundingClientRect para obtener el tamaño y posición del contenedor. Cambia el CSS del canvas de position fixed a position absolute y colócalo dentro de tu elemento contenedor objetivo.
Esto sucede cuando el tamaño del búfer del canvas no coincide con la densidad de píxeles de la pantalla. El escalado de devicePixelRatio en el código de ejemplo soluciona esto creando un búfer de canvas más grande y escalando el contexto de dibujo. Asegúrate de aplicar este escalado en tu función resize y reiniciar la matriz de transformación antes de cada operación de escala.
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.