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

环形滚动动画卡顿问题排查求助

环形滚动动画卡顿问题排查求助

各位前端大佬好,我是刚入门的前端新手,最近在做个人作品集的技能展示模块,想实现一个技能标签无限循环滚动的动画效果。我用Claude 3.7生成了相关的CSS和React代码,但实际运行起来动画特别卡顿,掉帧很明显,试了好多次调试都没解决,实在没办法来求助大家了😣

先简单说下场景:这个技能模块有滚动和网格两种视图模式,切换到滚动视图时,技能标签应该匀速无限循环滚动,但现在动画很不流畅,肉眼就能看出卡顿。

我已经尝试过的优化手段:

  • 用translate3d代替普通translate,触发GPU加速
  • 添加will-change: transform提示浏览器提前优化
  • 采用linear动画时间函数保证匀速滚动
  • 确保动画只修改transform属性,避免触发重排重绘

但这些都没解决卡顿问题,下面是我用到的相关代码,麻烦大家帮我看看哪里出了问题:

自定义滚动动画CSS

/* Enhanced scroll animation styles with forced continuous animation */
.scroll-wrapper {
  overflow: hidden;
  white-space: nowrap;
  width: 100%;
  position: relative;
}

.scroll-content, .scroll-content-reverse {
  display: inline-flex;
  white-space: nowrap;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  animation-play-state: running;
  will-change: transform;
}

.scroll-content {
  animation-name: continuous-scroll;
}

.scroll-content-reverse {
  animation-name: continuous-scroll;
  animation-direction: reverse;
}

@keyframes continuous-scroll {
  0% {
    transform: translate3d(0%, 0, 0);
  }
  100% {
    transform: translate3d(-50%, 0, 0);
  }
}

React技能组件代码

'use client';
import { Skill as SkillType } from '@/types/skill';
import { skillTypeOrder } from '@/types/skill';
import { fetchSkills } from '@/services/skillService';
import { useTheme } from '@/context/ThemeContext';
import { Icon } from '@iconify/react';
import { useEffect, useState, useRef } from 'react';
import { motion } from 'framer-motion';

const Skill = () => {
  const { isDarkMode } = useTheme();
  const [isVisible, setIsVisible] = useState(false);
  const [skillsData, setSkillsData] = useState<SkillType[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [viewMode, setViewMode] = useState<'scroll' | 'grid'>('scroll');
  const [isScrolling, setIsScrolling] = useState(false);

  // Fetch skills data from the database
  useEffect(() => {
    const getSkillsData = async () => {
      try {
        const data = await fetchSkills();
        setSkillsData(data);
        setIsLoading(false);
      } catch (err) {
        console.error('Failed to fetch skills data:', err);
        setError('Failed to load skills data');
        setIsLoading(false);
      }
    };
    getSkillsData();
    setIsVisible(true);
  }, []);

  const groupedSkills = skillsData.reduce((acc, skill) => {
    if (!acc[skill.type]) {
      acc[skill.type] = [];
    }
    acc[skill.type].push(skill);
    return acc;
  }, {} as Record<string, SkillType[]>);

  // Animation variants for the grid items
  const containerVariants = {
    hidden: { opacity: 0 },
    visible: { opacity: 1, transition: { staggerChildren: 0.1 } }
  };

  const itemVariants = {
    hidden: { opacity: 0, y: 20 },
    visible: { opacity: 1, y: 0, transition: { duration: 0.5 } }
  };

  // Toggle view mode
  const toggleViewMode = () => {
    setViewMode(prev => prev === 'scroll' ? 'grid' : 'scroll');
  };

  return (
    <section id="skills" className="py-10 overflow-hidden mt-20">
      <motion.h2 
        className="text-2xl md:text-3xl font-bold mb-6 text-center drop-shadow-xl"
        initial={{ opacity: 0, y: 20 }}
        whileInView={{ opacity: 1, y: 0 }}
        viewport={{ once: true, amount: 0.5 }}
        transition={{ duration: 0.5 }}
      >
        Skills
      </motion.h2>

      {/* View mode toggle */}
      <motion.div 
        className="flex justify-center mb-8"
        initial={{ opacity: 0, y: 20 }}
        whileInView={{ opacity: 1, y: 0 }}
        viewport={{ once: true, amount: 0.5 }}
        transition={{ duration: 0.5, delay: 0.3 }}
      >
        <div className="inline-flex items-center bg-[var(--card-bg)] rounded-full p-1 border border-[var(--border-color)] shadow-md">
          <button 
            onClick={() => setViewMode('scroll')}
            className={`px-4 py-2 rounded-full text-sm font-medium transition-all duration-300 ${
              viewMode === 'scroll' 
                ? 'bg-[var(--button-bg)] text-white shadow-sm' 
                : 'text-[var(--card-text)] hover:bg-[var(--hover-bg)]'
            }`}
          >
            <div className="flex items-center space-x-1">
              <Icon icon="mdi:animation-outline" className="w-4 h-4" />
              <span>Scroll</span>
            </div>
          </button>
          <button 
            onClick={() => setViewMode('grid')}
            className={`px-4 py-2 rounded-full text-sm font-medium transition-all duration-300 ${
              viewMode === 'grid' 
                ? 'bg-[var(--button-bg)] text-white shadow-sm' 
                : 'text-[var(--card-text)] hover:bg-[var(--hover-bg)]'
            }`}
          >
            <div className="flex items-center space-x-1">
              <Icon icon="mdi:grid" className="w-4 h-4" />
              <span>Grid</span>
            </div>
          </button>
        </div>
      </motion.div>

      {isLoading ? (
        <div className="flex justify-center items-center py-20">
          <Icon icon="mdi:loading" className="animate-spin text-4xl text-blue-500" />
        </div>
      ) : error ? (
        <div className="text-center py-10 text-red-500">
          <Icon icon="mdi:alert-circle" className="text-4xl mx-auto mb-2" />
          <p>{error}</p>
        </div>
      ) : skillsData.length === 0 ? (
        <div className="text-center py-10 text-[var(--text-secondary)]">
          <p>No skills found</p>
        </div>
      ) : (
        // 滚动视图核心渲染逻辑
        <div className="scroll-wrapper">
          <div className="scroll-content">
            {skillsData.map(skill => (
              <div key={skill.id} className="mx-4 flex items-center justify-center w-16 h-16 rounded-full bg-[var(--card-bg)] shadow-md p-2">
                <Icon icon={skill.icon} className="text-2xl" />
              </div>
            ))}
            {/* 复制一份实现无缝循环 */}
            {skillsData.map(skill => (
              <div key={`${skill.id}-clone`} className="mx-4 flex items-center justify-center w-16 h-16 rounded-full bg-[var(--card-bg)] shadow-md p-2">
                <Icon icon={skill.icon} className="text-2xl" />
              </div>
            ))}
          </div>
        </div>
      )}
    </section>
  );
};

补充几个我怀疑的点:

  1. 组件里同时使用了framer-motion的入场动画和自定义CSS滚动动画,会不会两者有性能冲突?
  2. 技能项使用了Iconify的Icon组件,会不会是SVG图标的渲染开销导致动画掉帧?
  3. 我用复制技能项的方式实现无缝循环,这种方式会不会带来额外的性能负担?

希望各位大佬能帮我分析下问题所在,或者给一些优化建议,谢谢大家了🙏

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 11:34:35