Производительность анимаций на мобильных устройствах определяется не только сложностью эффектов, но и частотой кадров, нагрузкой на главный поток и количеством одновременно активных трансформаций. В условиях ограниченных ресурсов мобильных браузеров особенно важно управлять тем, когда анимации должны выполняться, а когда их следует полностью отключать.
Motion One предоставляет минималистичный и высокопроизводительный API, но архитектурно он не включает автоматического управления средой выполнения. Это означает, что контроль за мобильной оптимизацией ложится на разработчика.
Первый шаг — корректное определение устройства. Самый базовый способ основан на ширине viewport:
const isMobile = window.matchMedia("(max-width: 768px)").matches;
Такой подход используется в большинстве интерфейсных систем, где мобильная версия определяется по брейкпоинту. Однако он не учитывает устройства с высокой плотностью пикселей или планшеты в альбомной ориентации.
Более точная проверка включает user-agent и pointer-тип:
const isMobile =
window.matchMedia("(max-width: 768px)").matches &&
window.matchMedia("(pointer: coarse)").matches;
pointer: coarse позволяет отсеять десктопы с изменённым
размером окна, что делает проверку более устойчивой.
Наиболее радикальный и надёжный способ оптимизации — полное отключение Motion One на мобильных устройствах.
import { animate } from "motion";
const isMobile =
window.matchMedia("(max-width: 768px)").matches &&
window.matchMedia("(pointer: coarse)").matches;
if (!isMobile) {
animate(".box", { opacity: [0, 1], y: [20, 0] }, { duration: 0.6 });
}
Такой подход гарантирует отсутствие лишних вычислений, но приводит к потере визуальной динамики интерфейса.
В большинстве случаев предпочтительнее не отключать анимации полностью, а упрощать их. Это позволяет сохранить визуальную целостность интерфейса при сниженной нагрузке.
const prefersReduced = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
const isMobile =
window.matchMedia("(max-width: 768px)").matches &&
window.matchMedia("(pointer: coarse)").matches;
const shouldReduceMotion = prefersReduced || isMobile;
animate(
".card",
shouldReduceMotion
? { opacity: [0, 1] }
: { opacity: [0, 1], y: [30, 0], scale: [0.95, 1] },
{ duration: shouldReduceMotion ? 0.2 : 0.6 }
);
Здесь мобильные устройства получают упрощённую версию анимации без трансформаций, но с сохранением базового fade-in эффекта.
При построении крупных приложений удобнее централизовать логику определения среды:
export const motionConfig = {
reducedMotion: false,
mobile: false,
};
export function initMotionConfig() {
const mqReduced = window.matchMedia("(prefers-reduced-motion: reduce)");
const mqMobile = window.matchMedia(
"(max-width: 768px) and (pointer: coarse)"
);
motionConfig.reducedMotion = mqReduced.matches;
motionConfig.mobile = mqMobile.matches;
}
Далее этот флаг используется во всех анимациях:
import { animate } from "motion";
import { motionConfig } from "./motionConfig";
const shouldSimplify =
motionConfig.reducedMotion || motionConfig.mobile;
animate(
".item",
shouldSimplify
? { opacity: [0, 1] }
: { opacity: [0, 1], y: [20, 0] }
);
Такой подход обеспечивает консистентное поведение анимаций во всём приложении.
На мобильных устройствах особенно дорогими являются анимации,
вызывающие reflow: изменения height, width,
top, left. Motion One работает преимущественно
через transform, но при неосторожной разработке можно легко
вызвать перерасчёт layout.
Правильная стратегия — заменить любые layout-анимации на transform-аналог:
Плохо:
animate(".panel", { height: [0, 300] });
Оптимально:
animate(".panel", { scaleY: [0, 1] }, { transformOrigin: "top" });
На мобильных устройствах это критично, так как transform не вызывает перерасчёт потока документа.
Для тяжёлых интерфейсов можно откладывать запуск анимаций до момента простоя:
const isMobile =
window.matchMedia("(max-width: 768px)").matches;
if (isMobile && "requestIdleCallback" in window) {
requestIdleCallback(() => {
animate(".item", { opacity: [0, 1] });
});
} else {
animate(".item", { opacity: [0, 1], y: [20, 0] });
}
Такой подход снижает вероятность просадки FPS при первичной загрузке страницы.
На мобильных устройствах важно контролировать параллелизм анимаций. Даже лёгкие эффекты при массовом запуске могут перегрузить основной поток.
const isMobile =
window.matchMedia("(max-width: 768px)").matches;
const maxConcurrent = isMobile ? 3 : 10;
let activeAnimations = 0;
function safeAnimate(selector, keyframes) {
if (activeAnimations >= maxConcurrent) return;
activeAnimations++;
const controls = animate(selector, keyframes, {
onComplete: () => {
activeAnimations--;
}
});
return controls;
}
Такой механизм предотвращает деградацию производительности при массовых рендерах списков и карточек.
Более продвинутый подход — динамическое отключение анимаций при падении производительности. На мобильных устройствах это особенно эффективно.
let lastFrame = performance.now();
let lowPerformance = false;
function monitorFPS() {
const now = performance.now();
const delta = now - lastFrame;
if (delta > 50) {
lowPerformance = true;
}
lastFrame = now;
requestAnimationFrame(monitorFPS);
}
monitorFPS();
Использование:
if (!lowPerformance) {
animate(".box", { opacity: [0, 1], y: [20, 0] });
}
Такой механизм адаптирует интерфейс к реальному состоянию устройства, а не только к его типу.
На мобильных устройствах иногда целесообразно полностью отказаться от Motion One для простых эффектов.
const isMobile =
window.matchMedia("(max-width: 768px)").matches;
if (isMobile) {
document.querySelector(".box").style.transition = "opacity 200ms";
document.querySelector(".box").style.opacity = "1";
} else {
animate(".box", { opacity: [0, 1], y: [20, 0] });
}
CSS transitions выполняются на уровне браузера и могут быть дешевле в обработке.
Эффективная архитектура предполагает разделение анимаций на уровни:
const isMobile =
window.matchMedia("(max-width: 768px)").matches;
const motionLayer = {
critical: true,
decorative: !isMobile,
experimental: false
};
if (motionLayer.decorative) {
animate(".header", { y: [-10, 0], opacity: [0, 1] });
}
Такой подход упрощает масштабирование интерфейса и делает поведение анимаций предсказуемым.
Несмотря на наличие мобильного детекта, основным источником истины считается системная настройка пользователя:
const prefersReduced = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
На мобильных устройствах этот флаг должен иметь приоритет над любыми другими проверками. Это позволяет учитывать реальные ограничения доступности и энергосбережения системы.
const disableMotion = prefersReduced;
if (!disableMotion) {
animate(".item", { opacity: [0, 1], y: [15, 0] });
}
Комбинированный подход включает:
prefers-reduced-motion