You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React应用中Framer Motion动画在iOS Safari上异常的修复咨询

iOS Safari 中 Framer-Motion 计数器组件不渲染问题修复

我用 React 开发了一个带 Framer-Motion 动画的计数器组件,每秒递增,桌面浏览器(Chrome、Firefox)表现正常,但在 iOS Safari 里有时完全不显示。怀疑和 setInterval 更新或 Framer-Motion 在 Safari 的处理逻辑有关,求稳定修复方案。

原代码

React 组件

import { motion, AnimatePresence } from "framer-motion";
import { useEffect, useState } from "react";
import "./Counter.css";

export const Counter = () => {
  const [count, setCount] = useState(0);
  const increment = 0.001;

  useEffect(() => {
    const storedStartTime = localStorage.getItem("startTime");
    if (storedStartTime) {
      const startTime = new Date(storedStartTime).getTime();
      const currentTime = Date.now();
      const elapsedSeconds = (currentTime - startTime) / 1000;

      setCount(elapsedSeconds * increment);
    }
  }, []);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount((prevCoins) => prevCoins + increment);
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  const digitVariants = {
    enter: {
      y: "100%",
      scale: 0.8,
      opacity: 0,
      transition: { duration: 0.2, ease: "easeInOut" },
    },
    center: {
      y: "0%",
      scale: 1,
      opacity: 1,
      transition: { duration: 0.2, ease: "easeInOut" },
    },
    exit: {
      y: "-100%",
      scale: 0.8,
      opacity: 0,
      transition: { duration: 0.2, ease: "easeInOut" },
    },
  };

  const getDigits = (value: number) => value.toFixed(3).split("");
  const digits = getDigits(count);
  const prevDigits = getDigits(count - increment);

  return (
    <div className="counter-container">
      {digits.map((digit, index) => {
        const prevDigit = prevDigits[index];
        const shouldAnimate = digit !== prevDigit;

        return (
          <div key={index} className="counter">
            <AnimatePresence>
              {shouldAnimate && (
                <motion.div
                  key={`prev-${index}-${prevDigit}`}
                  initial="center"
                  animate="exit"
                  exit="exit"
                  variants={digitVariants}
                  className="timer-current-digit"
                >
                  {prevDigit}
                </motion.div>
              )}
              <motion.div
                key={`digit-${index}-${digit}`}
                initial="enter"
                animate="center"
                exit="exit"
                variants={digitVariants}
                className="timer-digit"
              >
                {digit}
              </motion.div>
            </AnimatePresence>
          </div>
        );
      })}
    </div>
  );
};

CSS 样式

.counter-container {
  display: flex;
  height: 16px;
  font-size: 16px;
  font-weight: 700;
  line-height: 16px;
  align-items: flex-start;
}

.counter {
  position: relative;
  height: 16px;
  width: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.timer-digit {
  position: absolute;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.counter-container,
.counter,
.timer-digit {
  transform: translateZ(0);
  -webkit-transform: translateZ(0);
  will-change: transform, opacity;
}

修复方案

1. 替换时间计算逻辑,避免浮点精度误差

原代码依赖 count 累加计算前一位数字,浮点精度误差会导致渲染异常。改成基于起始时间实时计算,同时用 requestAnimationFrame 替代 setInterval,适配浏览器渲染周期:

import { motion, AnimatePresence } from "framer-motion";
import { useEffect, useState, useMemo } from "react";
import "./Counter.css";

export const Counter = () => {
  const increment = 0.001;
  // 初始化起始时间
  const [startTime, setStartTime] = useState<number>(() => {
    const stored = localStorage.getItem("startTime");
    return stored ? new Date(stored).getTime() : Date.now();
  });

  // 实时计算当前数值,避免累加误差
  const count = useMemo(() => {
    const elapsedSeconds = (Date.now() - startTime) / 1000;
    return elapsedSeconds * increment;
  }, [startTime, increment]);

  // 首次加载时存储起始时间
  useEffect(() => {
    if (!localStorage.getItem("startTime")) {
      localStorage.setItem("startTime", new Date(startTime).toISOString());
    }
  }, [startTime]);

  // 用requestAnimationFrame更新,适配浏览器渲染
  useEffect(() => {
    let animationId: number;
    const update = () => {
      animationId = requestAnimationFrame(update);
    };
    animationId = requestAnimationFrame(update);
    return () => cancelAnimationFrame(animationId);
  }, []);

  // 动画变体保留
  const digitVariants = {
    enter: { y: "100%", scale: 0.8, opacity: 0, transition: { duration: 0.2, ease: "easeInOut" } },
    center: { y: "0%", scale: 1, opacity: 1, transition: { duration: 0.2, ease: "easeInOut" } },
    exit: { y: "-100%", scale: 0.8, opacity: 0, transition: { duration: 0.2, ease: "easeInOut" } },
  };

  const getDigits = (value: number) => value.toFixed(3).split("");
  const digits = getDigits(count);

  return (
    <div className="counter-container">
      {digits.map((digit, index) => (
        <div key={index} className="counter">
          <AnimatePresence mode="wait">
            <motion.div
              key={digit}
              initial="enter"
              animate="center"
              exit="exit"
              variants={digitVariants}
              className="timer-digit"
            >
              {digit}
            </motion.div>
          </AnimatePresence>
        </div>
      ))}
    </div>
  );
};

2. 优化 CSS 适配 Safari

调整容器对齐方式,添加 Safari 专属渲染优化:

.counter-container {
  display: flex;
  height: 16px;
  font-size: 16px;
  font-weight: 700;
  line-height: 16px;
  align-items: center; /* 替换flex-start,避免高度计算异常 */
}

.counter {
  position: relative;
  height: 16px;
  width: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  -webkit-overflow-scrolling: touch; /* 修复Safari触摸容器渲染问题 */
}

.timer-digit {
  position: absolute;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* 移除强制硬件加速,避免Safari GPU渲染bug */
.counter-container,
.counter,
.timer-digit {
  will-change: transform, opacity;
}

3. 简化动画逻辑

移除原代码中多余的前一位数字渲染,用 AnimatePresence 的 mode="wait" 确保动画顺序,避免 Safari 渲染冲突。

内容的提问来源于stack exchange,提问作者user27254038

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 15:57:09