perf(ui): optimize animations with hardware acceleration

- Replace transition-all with transition-transform for better performance
- Add will-change and transform properties to enable GPU acceleration
- Simplify marquee animation logic and remove unused hover state
- Remove unused skills and optimize animation variants
This commit is contained in:
joyzhao
2025-06-20 09:11:14 +08:00
parent c698d1ae45
commit 45299c1fa6
3 changed files with 32 additions and 57 deletions

View File

@@ -1,5 +1,4 @@
import { motion, useAnimation } from "framer-motion"; import { motion, type Variants } from "framer-motion";
import { useState, useEffect } from "react";
import { useTranslations } from "@/i18n/utils"; import { useTranslations } from "@/i18n/utils";
import { type SkillItem } from "@/types"; import { type SkillItem } from "@/types";
@@ -13,8 +12,6 @@ const allSkills: SkillItem[] = [
{ name: "JavaScript", icon: "javascript" }, { name: "JavaScript", icon: "javascript" },
{ name: "React", icon: "react" }, { name: "React", icon: "react" },
{ name: "Vue", icon: "vue" }, { name: "Vue", icon: "vue" },
{ name: "微信小程序", icon: "wechat" },
{ name: "UniApp", icon: "vue" }, // Using vue icon as fallback
// Frontend Development // Frontend Development
{ name: "Next.js", icon: "nextjs" }, { name: "Next.js", icon: "nextjs" },
@@ -29,7 +26,7 @@ const allSkills: SkillItem[] = [
{ name: "Node.js", icon: "nodejs" }, { name: "Node.js", icon: "nodejs" },
{ name: "Express.js", icon: "express" }, { name: "Express.js", icon: "express" },
{ name: "Nest.js", icon: "nestjs" }, { name: "Nest.js", icon: "nestjs" },
{ name: "Fastify.js", icon: "fastify" }, // { name: "Fastify.js", icon: "fastify" },
{ name: "Hono.js", icon: "nodejs" }, // Using nodejs icon as fallback { name: "Hono.js", icon: "nodejs" }, // Using nodejs icon as fallback
// Database & Storage // Database & Storage
@@ -75,6 +72,7 @@ function SkillItem({ skill }: { skill: SkillItem }) {
/** /**
* Marquee row component that scrolls skills horizontally * Marquee row component that scrolls skills horizontally
* Optimized for performance with constant animation
*/ */
function MarqueeRow({ skills, direction = "left", speed = 50 }: { function MarqueeRow({ skills, direction = "left", speed = 50 }: {
skills: SkillItem[]; skills: SkillItem[];
@@ -83,57 +81,34 @@ function MarqueeRow({ skills, direction = "left", speed = 50 }: {
}) { }) {
// Duplicate skills array to create seamless infinite scroll // Duplicate skills array to create seamless infinite scroll
const duplicatedSkills = [...skills, ...skills]; const duplicatedSkills = [...skills, ...skills];
// State to control animation play/pause
const [isHovered, setIsHovered] = useState(false);
const controls = useAnimation();
// Start animation on component mount // Define animation variants to ensure type safety with hardware acceleration
useEffect(() => { const variants: Variants = {
const startAnimation = () => { animate: {
controls.start({ transform: direction === "left" ? ["translateX(0%)", "translateX(-50%)"] : ["translateX(-50%)", "translateX(0%)"],
x: direction === "left" ? ["-50%", "0%"] : ["0%", "-50%"], transition: {
transition: { transform: {
x: { repeat: Infinity,
repeat: Infinity, repeatType: "loop" as const,
repeatType: "loop", duration: speed,
duration: speed, ease: [0, 0, 1, 1] // 使用数组形式的 linear easing
ease: "linear"
}
} }
}); }
};
startAnimation();
}, [controls, direction, speed]);
// Handle hover state changes
useEffect(() => {
if (isHovered) {
controls.stop();
} else {
controls.start({
x: direction === "left" ? ["-50%", "0%"] : ["0%", "-50%"],
transition: {
x: {
repeat: Infinity,
repeatType: "loop",
duration: speed,
ease: "linear"
}
}
});
} }
}, [isHovered, controls, direction, speed]); };
return ( return (
<div <div className="overflow-hidden">
className="overflow-hidden"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<motion.div <motion.div
className="flex gap-4 w-fit" className="flex gap-4 w-fit"
animate={controls} variants={variants}
animate="animate"
style={{
willChange: "transform",
backfaceVisibility: "hidden",
WebkitBackfaceVisibility: "hidden",
WebkitFontSmoothing: "subpixel-antialiased"
}}
> >
{duplicatedSkills.map((skill, index) => ( {duplicatedSkills.map((skill, index) => (
<SkillItem key={`${skill.name}-${index}`} skill={skill} /> <SkillItem key={`${skill.name}-${index}`} skill={skill} />

View File

@@ -177,7 +177,7 @@ const pageTitle = t('site.title');
<div class="grid md:grid-cols-2 gap-8"> <div class="grid md:grid-cols-2 gap-8">
{services.en.map((service) => ( {services.en.map((service) => (
<div class="bg-white/70 dark:bg-slate-800/70 backdrop-blur-sm rounded-2xl p-8 border border-white/20 shadow-xl hover:shadow-2xl transition-all duration-300 hover:scale-105"> <div class="bg-white/70 dark:bg-slate-800/70 backdrop-blur-sm rounded-2xl p-8 border border-white/20 shadow-xl hover:shadow-2xl transition-transform duration-300 hover:scale-105" style="will-change: transform; transform: translateZ(0);">
<div class="flex items-center mb-6"> <div class="flex items-center mb-6">
<div class={`w-12 h-12 bg-gradient-to-r ${service.icon.gradient} rounded-lg flex items-center justify-center mr-4`}> <div class={`w-12 h-12 bg-gradient-to-r ${service.icon.gradient} rounded-lg flex items-center justify-center mr-4`}>
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -352,7 +352,7 @@ const pageTitle = t('site.title');
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 items-stretch"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 items-stretch">
<!-- Taskify App Project --> <!-- Taskify App Project -->
<div class="group overflow-hidden border border-purple-500/20 hover:border-purple-500/40 h-full flex flex-col transition-all duration-500 hover:scale-105 bg-white/10 dark:bg-gray-800/50 backdrop-blur-sm rounded-xl relative"> <div class="group overflow-hidden border border-purple-500/20 hover:border-purple-500/40 h-full flex flex-col transition-transform duration-500 hover:scale-105 bg-white/10 dark:bg-gray-800/50 backdrop-blur-sm rounded-xl relative" style="will-change: transform; transform: translateZ(0);">
<!-- Project tag - positioned at top right --> <!-- Project tag - positioned at top right -->
<div class="absolute top-4 right-4 z-10"> <div class="absolute top-4 right-4 z-10">
<span class="px-3 py-1 bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200 text-xs rounded-full font-medium border border-blue-200 dark:border-blue-700">{t('project.tag.business')}</span> <span class="px-3 py-1 bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200 text-xs rounded-full font-medium border border-blue-200 dark:border-blue-700">{t('project.tag.business')}</span>
@@ -360,7 +360,7 @@ const pageTitle = t('site.title');
<!-- Project image placeholder --> <!-- Project image placeholder -->
<div class="h-48 bg-gradient-to-br from-purple-500/20 to-purple-600/20 relative overflow-hidden"> <div class="h-48 bg-gradient-to-br from-purple-500/20 to-purple-600/20 relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-br from-purple-500/10 to-purple-600/10 group-hover:from-purple-500/20 group-hover:to-purple-600/20 transition-all duration-500"></div> <div class="absolute inset-0 bg-gradient-to-br from-purple-500/10 to-purple-600/10 group-hover:from-purple-500/20 group-hover:to-purple-600/20 transition-colors duration-500"></div>
<div class="absolute bottom-4 left-4 right-4"> <div class="absolute bottom-4 left-4 right-4">
<div class="bg-black/50 backdrop-blur-sm rounded px-3 py-1 text-xs font-mono text-purple-400"> <div class="bg-black/50 backdrop-blur-sm rounded px-3 py-1 text-xs font-mono text-purple-400">
Taskify App Taskify App
@@ -419,7 +419,7 @@ const pageTitle = t('site.title');
</div> </div>
<!-- E-Shop Platform Project --> <!-- E-Shop Platform Project -->
<div class="group overflow-hidden border border-purple-500/20 hover:border-purple-500/40 h-full flex flex-col transition-all duration-500 hover:scale-105 bg-white/10 dark:bg-gray-800/50 backdrop-blur-sm rounded-xl relative"> <div class="group overflow-hidden border border-purple-500/20 hover:border-purple-500/40 h-full flex flex-col transition-transform duration-500 hover:scale-105 bg-white/10 dark:bg-gray-800/50 backdrop-blur-sm rounded-xl relative" style="will-change: transform; transform: translateZ(0);">
<!-- Project tag - positioned at top right --> <!-- Project tag - positioned at top right -->
<div class="absolute top-4 right-4 z-10"> <div class="absolute top-4 right-4 z-10">
<span class="px-3 py-1 bg-green-100 dark:bg-green-800 text-green-800 dark:text-green-200 text-xs rounded-full font-medium border border-green-200 dark:border-green-700">{t('project.tag.opensource')}</span> <span class="px-3 py-1 bg-green-100 dark:bg-green-800 text-green-800 dark:text-green-200 text-xs rounded-full font-medium border border-green-200 dark:border-green-700">{t('project.tag.opensource')}</span>
@@ -427,7 +427,7 @@ const pageTitle = t('site.title');
<!-- Project image placeholder --> <!-- Project image placeholder -->
<div class="h-48 bg-gradient-to-br from-purple-500/20 to-purple-600/20 relative overflow-hidden"> <div class="h-48 bg-gradient-to-br from-purple-500/20 to-purple-600/20 relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-br from-purple-500/10 to-purple-600/10 group-hover:from-purple-500/20 group-hover:to-purple-600/20 transition-all duration-500"></div> <div class="absolute inset-0 bg-gradient-to-br from-purple-500/10 to-purple-600/10 group-hover:from-purple-500/20 group-hover:to-purple-600/20 transition-colors duration-500"></div>
<div class="absolute bottom-4 left-4 right-4"> <div class="absolute bottom-4 left-4 right-4">
<div class="bg-black/50 backdrop-blur-sm rounded px-3 py-1 text-xs font-mono text-purple-400"> <div class="bg-black/50 backdrop-blur-sm rounded px-3 py-1 text-xs font-mono text-purple-400">
E-Shop Platform E-Shop Platform
@@ -486,7 +486,7 @@ const pageTitle = t('site.title');
</div> </div>
<!-- Portfolio Site Project --> <!-- Portfolio Site Project -->
<div class="group overflow-hidden border border-purple-500/20 hover:border-purple-500/40 h-full flex flex-col transition-all duration-500 hover:scale-105 bg-white/10 dark:bg-gray-800/50 backdrop-blur-sm rounded-xl relative"> <div class="group overflow-hidden border border-purple-500/20 hover:border-purple-500/40 h-full flex flex-col transition-transform duration-500 hover:scale-105 bg-white/10 dark:bg-gray-800/50 backdrop-blur-sm rounded-xl relative" style="will-change: transform; transform: translateZ(0);">
<!-- Project tag - positioned at top right --> <!-- Project tag - positioned at top right -->
<div class="absolute top-4 right-4 z-10"> <div class="absolute top-4 right-4 z-10">
<span class="px-3 py-1 bg-amber-100 dark:bg-amber-800 text-amber-800 dark:text-amber-200 text-xs rounded-full font-medium border border-amber-200 dark:border-amber-700">{t('project.tag.personal')}</span> <span class="px-3 py-1 bg-amber-100 dark:bg-amber-800 text-amber-800 dark:text-amber-200 text-xs rounded-full font-medium border border-amber-200 dark:border-amber-700">{t('project.tag.personal')}</span>
@@ -494,7 +494,7 @@ const pageTitle = t('site.title');
<!-- Project image placeholder --> <!-- Project image placeholder -->
<div class="h-48 bg-gradient-to-br from-purple-500/20 to-purple-600/20 relative overflow-hidden"> <div class="h-48 bg-gradient-to-br from-purple-500/20 to-purple-600/20 relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-br from-purple-500/10 to-purple-600/10 group-hover:from-purple-500/20 group-hover:to-purple-600/20 transition-all duration-500"></div> <div class="absolute inset-0 bg-gradient-to-br from-purple-500/10 to-purple-600/10 group-hover:from-purple-500/20 group-hover:to-purple-600/20 transition-colors duration-500"></div>
<div class="absolute bottom-4 left-4 right-4"> <div class="absolute bottom-4 left-4 right-4">
<div class="bg-black/50 backdrop-blur-sm rounded px-3 py-1 text-xs font-mono text-purple-400"> <div class="bg-black/50 backdrop-blur-sm rounded px-3 py-1 text-xs font-mono text-purple-400">
Portfolio Site Portfolio Site

View File

@@ -177,7 +177,7 @@ const pageTitle = t('site.title');
<div class="grid md:grid-cols-2 gap-8"> <div class="grid md:grid-cols-2 gap-8">
{services.zh.map((service) => ( {services.zh.map((service) => (
<div class="bg-white/70 dark:bg-slate-800/70 backdrop-blur-sm rounded-2xl p-8 border border-white/20 shadow-xl hover:shadow-2xl transition-all duration-300 hover:scale-105"> <div class="bg-white/70 dark:bg-slate-800/70 backdrop-blur-sm rounded-2xl p-8 border border-white/20 shadow-xl hover:shadow-2xl transition-transform duration-300 hover:scale-105" style="will-change: transform; transform: translateZ(0);">
<div class="flex items-center mb-6"> <div class="flex items-center mb-6">
<div class={`w-12 h-12 bg-gradient-to-r ${service.icon.gradient} rounded-lg flex items-center justify-center mr-4`}> <div class={`w-12 h-12 bg-gradient-to-r ${service.icon.gradient} rounded-lg flex items-center justify-center mr-4`}>
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">