如何为您的网站添加简单的飘雪效果
节日主题的网站动画可以取悦访客,但大多数教程忽略了生产环境中真正重要的内容:性能、可访问性和用户体验。您不会希望一个装饰性的背景动画拖累您的核心网页指标(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) 调用在应用新缩放之前重置变换矩阵,防止窗口调整大小时的累积缩放。
Discover how at OpenReplay.com.
必要的性能和可访问性防护措施
尊重用户偏好
设置了 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.