Como Adicionar um Efeito Simples de Neve ao Seu Website
Uma animação sazonal de feriado no website pode encantar os visitantes, mas a maioria dos tutoriais ignora o que realmente importa em produção: desempenho, acessibilidade e experiência do usuário. Você não quer uma animação decorativa de fundo que prejudique seus Core Web Vitals ou incomode usuários que preferem movimento reduzido.
Este guia mostra como construir um efeito leve de neve em canvas que respeita as preferências do usuário, pausa quando invisível e não atrapalha. Você também aprenderá quando um efeito de neve mais simples em CSS faz sentido.
Pontos-Chave
- Canvas escala de forma mais confiável do que abordagens baseadas em DOM quando você vai além de um pequeno punhado de partículas
- Sempre respeite
prefers-reduced-motionpara honrar as preferências de acessibilidade do usuário - Use a Page Visibility API para pausar animações em abas em segundo plano e economizar recursos
- Neve em CSS puro funciona para implementações mínimas (5-10 flocos de neve), mas não escala bem
Por Que Canvas para Animação de Neve em JavaScript
Abordagens baseadas em DOM criam elementos individuais para cada floco de neve. Isso funciona para um punhado de partículas, mas escalar para dezenas ou centenas significa manipulação constante do DOM, recálculos de layout e pressão de memória pela criação e remoção de elementos.
Um efeito de neve em canvas desenha tudo em um único elemento. Você controla o loop de renderização, gerencia o estado das partículas em arrays simples e evita completamente a sobrecarga do DOM. Quando você escala além de um pequeno número de partículas ou deseja movimento mais suave, canvas se torna o padrão mais previsível.
Trade-offs a entender:
- Canvas requer JavaScript—sem JS significa sem neve
- Texto dentro do canvas não é acessível para leitores de tela (adequado para efeitos puramente decorativos)
- Animações CSS não são “gratuitas”—elas ainda consomem recursos de CPU/GPU
Configurando o Elemento Canvas
Posicione o canvas atrás do seu conteúdo com CSS:
#snowfall {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: -1;
}
A regra pointer-events: none garante que o canvas nunca bloqueie rolagem, cliques ou qualquer interação do usuário.
<canvas id="snowfall" aria-hidden="true"></canvas>
Adicionar aria-hidden="true" informa às tecnologias assistivas para ignorar este elemento puramente decorativo.
Construindo o Loop de Animação
Aqui está uma implementação mínima com proteções adequadas:
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);
Isso lida corretamente com telas de alta resolução escalando o buffer do canvas para corresponder ao devicePixelRatio. A chamada ctx.setTransform(1, 0, 0, 1, 0, 0) redefine a matriz de transformação antes de aplicar a nova escala, prevenindo escalamento cumulativo no redimensionamento da janela.
Discover how at OpenReplay.com.
Proteções Essenciais de Desempenho e Acessibilidade
Respeitando as Preferências do Usuário
Usuários que definem prefers-reduced-motion pediram explicitamente por menos animação. Respeite isso:
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 implementação escuta mudanças na preferência de movimento do usuário, permitindo que a animação responda dinamicamente se a configuração mudar enquanto a página estiver aberta.
Pausando Quando Oculto
Executar animações em abas em segundo plano desperdiça bateria e CPU. A Page Visibility API resolve isso:
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
cancelAnimationFrame(animationId);
} else if (!prefersReduced) {
update();
}
});
Quando Efeitos de Neve em CSS Fazem Sentido
Para implementações muito pequenas—talvez 5-10 flocos de neve em uma seção hero—uma abordagem em CSS puro evita JavaScript completamente:
.snowflake {
position: absolute;
color: white;
animation: fall 8s linear infinite;
}
@keyframes fall {
to { transform: translateY(100vh); }
}
Isso funciona para casos de uso decorativos mínimos, mas não escala. Cada elemento ainda aciona composição, e você perde controle programático sobre densidade e comportamento.
Opções de Personalização
Ajuste esses valores para combinar com a estética do seu site:
- Densidade: Altere o limite
80e o limiar de spawn0.95 - Faixa de velocidade: Modifique o cálculo
Math.random() * 1 + 0.5 - Tamanho: Ajuste o cálculo do raio
- Escopo: Direcione um contêiner específico em vez de
window.innerWidth/Height
Conclusão
Uma animação de feriado no website deve adicionar à experiência sem comprometê-la. Use canvas para desempenho escalável, respeite prefers-reduced-motion, pause quando a página não estiver visível e mantenha o efeito não-interativo. Comece com contagens conservadoras de partículas e ajuste com base em testes reais em dispositivos—não em suposições sobre o que os navegadores podem suportar.
Perguntas Frequentes
Uma animação em canvas bem implementada tem impacto mínimo nos Core Web Vitals. Como o canvas renderiza para um único elemento e usa requestAnimationFrame, não causará mudanças de layout nem bloqueará a thread principal. Mantenha contagens de partículas razoáveis (abaixo de 100) e pause a animação quando a aba estiver oculta para manter boas pontuações de desempenho.
Sim. Adicione uma propriedade de deriva a cada objeto de floco e atualize a posição x no seu loop de animação junto com a posição y. Use Math.sin com um deslocamento baseado em tempo para oscilação de aparência natural, ou aplique um valor horizontal constante para vento constante. Randomize valores de deriva por floco para variedade.
Substitua window.innerWidth e window.innerHeight pelas dimensões do seu contêiner alvo. Use getBoundingClientRect para obter o tamanho e posição do contêiner. Mude o CSS do canvas de position fixed para position absolute e coloque-o dentro do elemento contêiner alvo.
Isso acontece quando o tamanho do buffer do canvas não corresponde à densidade de pixels do display. O escalamento de devicePixelRatio no exemplo de código corrige isso criando um buffer de canvas maior e escalando o contexto de desenho. Certifique-se de aplicar esse escalamento na sua função resize e redefinir a matriz de transformação antes de cada operação 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.