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
相关产品推荐
相关产品推荐

