Back

如何为您的网站添加简单的飘雪效果

如何为您的网站添加简单的飘雪效果

节日主题的网站动画可以取悦访客,但大多数教程忽略了生产环境中真正重要的内容:性能、可访问性和用户体验。您不会希望一个装饰性的背景动画拖累您的核心网页指标(Core Web Vitals)或惹恼那些偏好减少动效的用户。

本指南将向您展示如何构建一个轻量级的 canvas 飘雪效果,它尊重用户偏好设置,在不可见时暂停,并且不会妨碍用户操作。您还将了解何时使用更简单的 CSS 飘雪效果更合理。

核心要点

  • 一旦粒子数量超过少量,Canvas 的扩展性比基于 DOM 的方案更可靠
  • 始终尊重 prefers-reduced-motion 以遵守用户的可访问性偏好
  • 使用 Page Visibility API 在后台标签页中暂停动画并节省资源
  • 纯 CSS 飘雪适用于最小化实现(5-10 片雪花),但扩展性不佳

为什么使用 Canvas 实现 JavaScript 飘雪动画

基于 DOM 的方案为每片雪花创建单独的元素。这对于少量粒子可行,但扩展到几十或几百个意味着持续的 DOM 操作、布局重新计算,以及元素创建和删除带来的内存压力。

Canvas 飘雪效果将所有内容绘制到单个元素上。您可以控制渲染循环,在普通数组中管理粒子状态,并完全避免 DOM 开销。一旦您需要扩展到大量粒子或想要更流畅的运动效果,canvas 就成为更可预测的默认选择。

需要理解的权衡:

  • Canvas 需要 JavaScript——没有 JS 就没有雪花
  • Canvas 内的文本对屏幕阅读器不可访问(对于纯装饰效果来说没问题)
  • CSS 动画并非”免费”——它们仍然消耗 CPU/GPU 资源

设置 Canvas 元素

使用 CSS 将 canvas 定位在内容后面:

#snowfall {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: -1;
}

pointer-events: none 规则确保 canvas 永远不会阻止滚动、点击或任何用户交互。

<canvas id="snowfall" aria-hidden="true"></canvas>

添加 aria-hidden="true" 告诉辅助技术忽略这个纯装饰性元素。

构建动画循环

以下是一个带有适当防护措施的最小实现:

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);

这段代码通过将 canvas 缓冲区缩放以匹配 devicePixelRatio 来正确处理高 DPI 屏幕。ctx.setTransform(1, 0, 0, 1, 0, 0) 调用在应用新缩放之前重置变换矩阵,防止窗口调整大小时的累积缩放。

必要的性能和可访问性防护措施

尊重用户偏好

设置了 prefers-reduced-motion 的用户明确要求减少动画。请尊重这一点:

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();
}

此实现监听用户动效偏好的变化,允许动画在页面打开时如果设置发生变化能够动态响应。

隐藏时暂停

在后台标签页中运行动画会浪费电池和 CPU。Page Visibility API 解决了这个问题:

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    cancelAnimationFrame(animationId);
  } else if (!prefersReduced) {
    update();
  }
});

何时使用 CSS 飘雪效果更合理

对于非常小的实现——也许在首屏区域放置 5-10 片雪花——纯 CSS 方案完全避免了 JavaScript:

.snowflake {
  position: absolute;
  color: white;
  animation: fall 8s linear infinite;
}

@keyframes fall {
  to { transform: translateY(100vh); }
}

这适用于最小化的装饰用例,但不具备扩展性。每个元素仍然会触发合成,而且您失去了对密度和行为的编程控制。

自定义选项

调整这些值以匹配您网站的美学风格:

  • 密度:更改 80 上限和 0.95 生成阈值
  • 速度范围:修改 Math.random() * 1 + 0.5 计算
  • 大小:调整半径计算
  • 范围:针对特定容器而不是 window.innerWidth/Height

结论

节日网站动画应该增强体验而不是损害体验。使用 canvas 获得可扩展的性能,尊重 prefers-reduced-motion,在页面不可见时暂停,并保持效果非交互性。从保守的粒子数量开始,根据实际设备测试进行调整——而不是假设浏览器能处理什么。

常见问题

实现良好的 canvas 动画对核心网页指标的影响很小。由于 canvas 渲染到单个元素并使用 requestAnimationFrame,它不会导致布局偏移或阻塞主线程。保持合理的粒子数量(100 个以下)并在标签页隐藏时暂停动画以维持良好的性能评分。

可以。为每个雪花对象添加一个漂移属性,并在动画循环中与 y 位置一起更新 x 位置。使用带有基于时间偏移的 Math.sin 来实现自然的振荡效果,或应用恒定的水平值来实现稳定的风。为每片雪花随机化漂移值以增加变化。

用目标容器的尺寸替换 window.innerWidth 和 window.innerHeight。使用 getBoundingClientRect 获取容器的大小和位置。将 canvas CSS 从 position fixed 改为 position absolute,并将其放置在目标容器元素内部。

这是因为 canvas 缓冲区大小与显示器的像素密度不匹配。代码示例中的 devicePixelRatio 缩放通过创建更大的 canvas 缓冲区并缩放绘图上下文来解决这个问题。确保在 resize 函数中应用此缩放,并在每次缩放操作前重置变换矩阵。

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.

OpenReplay