Отключение анимаций на мобильных

Производительность анимаций на мобильных устройствах определяется не только сложностью эффектов, но и частотой кадров, нагрузкой на главный поток и количеством одновременно активных трансформаций. В условиях ограниченных ресурсов мобильных браузеров особенно важно управлять тем, когда анимации должны выполняться, а когда их следует полностью отключать.

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

Такой подход обеспечивает консистентное поведение анимаций во всём приложении.


Отказ от layout-движений на мобильных

На мобильных устройствах особенно дорогими являются анимации, вызывающие reflow: изменения height, width, top, left. Motion One работает преимущественно через transform, но при неосторожной разработке можно легко вызвать перерасчёт layout.

Правильная стратегия — заменить любые layout-анимации на transform-аналог:

Плохо:

animate(".panel", { height: [0, 300] });

Оптимально:

animate(".panel", { scaleY: [0, 1] }, { transformOrigin: "top" });

На мобильных устройствах это критично, так как transform не вызывает перерасчёт потока документа.


Использование requestIdleCallback для мобильной оптимизации

Для тяжёлых интерфейсов можно откладывать запуск анимаций до момента простоя:

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

Такой механизм предотвращает деградацию производительности при массовых рендерах списков и карточек.


Отключение анимаций при плохом FPS

Более продвинутый подход — динамическое отключение анимаций при падении производительности. На мобильных устройствах это особенно эффективно.

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

Такой механизм адаптирует интерфейс к реальному состоянию устройства, а не только к его типу.


Приоритет CSS-переходов вместо JS-анимаций

На мобильных устройствах иногда целесообразно полностью отказаться от 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] });
}

Такой подход упрощает масштабирование интерфейса и делает поведение анимаций предсказуемым.


Контроль через prefers-reduced-motion как приоритетный сигнал

Несмотря на наличие мобильного детекта, основным источником истины считается системная настройка пользователя:

const prefersReduced = window.matchMedia(
  "(prefers-reduced-motion: reduce)"
).matches;

На мобильных устройствах этот флаг должен иметь приоритет над любыми другими проверками. Это позволяет учитывать реальные ограничения доступности и энергосбережения системы.

const disableMotion = prefersReduced;

if (!disableMotion) {
  animate(".item", { opacity: [0, 1], y: [15, 0] });
}

Итоговая стратегия мобильной оптимизации Motion One

Комбинированный подход включает:

  • определение мобильного устройства через media queries
  • приоритет prefers-reduced-motion
  • упрощение keyframes вместо полного отключения
  • отказ от layout-анимаций
  • ограничение параллельных анимаций
  • адаптивное отключение при падении FPS
  • использование CSS transitions для простых эффектов