Как добавить простой эффект снегопада на ваш сайт
Сезонная праздничная анимация на сайте может порадовать посетителей, но большинство руководств игнорируют то, что важно в продакшене: производительность, доступность и пользовательский опыт. Вам не нужна декоративная фоновая анимация, которая ухудшает ваши 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
Расположите canvas за вашим контентом с помощью CSS:
#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);
Это корректно обрабатывает экраны с высоким DPI, масштабируя буфер canvas в соответствии с devicePixelRatio. Вызов 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 снежинок в секции hero — чистый 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-анимация оказывает минимальное влияние на Core Web Vitals. Поскольку canvas рендерится в один элемент и использует requestAnimationFrame, он не вызовет сдвигов макета или блокировки основного потока. Держите количество частиц разумным (до 100) и приостанавливайте анимацию, когда вкладка скрыта, чтобы поддерживать хорошие показатели производительности.
Да. Добавьте свойство дрейфа к каждому объекту снежинки и обновляйте позицию x в вашем цикле анимации вместе с позицией y. Используйте Math.sin со смещением на основе времени для естественно выглядящих колебаний или примените постоянное горизонтальное значение для устойчивого ветра. Рандомизируйте значения дрейфа для каждой снежинки для разнообразия.
Замените window.innerWidth и window.innerHeight на размеры вашего целевого контейнера. Используйте getBoundingClientRect для получения размера и позиции контейнера. Измените CSS canvas с 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.