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

React Hooks+Styled-components视差动画性能优化求助

优化React视差卡片动画性能的方案

你遇到的问题确实很典型——频繁重渲染和事件监听器反复绑定/解绑是导致卡顿的核心原因。下面是几个针对性的优化方案,一步步帮你实现流畅的视差动画:

1. 稳定事件处理函数引用,避免监听器反复绑定/解绑

你的handleMouseOver和handleMouseOut每次组件渲染都会重新创建,导致useEffect的依赖项变化,进而反复执行移除和添加监听器的操作。用useCallback包裹这两个函数,让它们的引用在组件生命周期内保持稳定:

const handleMouseOver = useCallback(e => {
  const { y, x, width, height } = e.target.getBoundingClientRect();
  const { pageX, pageY } = e;
  const halfW = width / 2;
  const halfH = height / 2;
  const coorX = halfW - (pageX - x);
  const coorY = halfH - (pageY - (y + window.scrollY));
  const degX = `${(coorY / halfH) * 10}deg`;
  const degY = `${(coorX / halfW) * -10}deg`;
  setrotationDegX(degX);
  setrotationDegY(degY);
}, []);

const handleMouseOut = useCallback(e => {
  setrotationDegX('0deg');
  setrotationDegY('0deg');
}, []);

更新useEffect的依赖后,现在监听器只会在组件挂载时绑定一次,卸载时解绑:

useEffect(() => {
  const currentDom = domRef.current;
  currentDom.addEventListener("mousemove", handleMouseOver);
  currentDom.addEventListener("mouseout", handleMouseOut);

  return () => {
    currentDom.removeEventListener("mousemove", handleMouseOver);
    currentDom.removeEventListener("mouseout", handleMouseOut);
  };
}, [handleMouseOver, handleMouseOut]);

2. 减少不必要的重渲染

每次setrotationDegX和setrotationDegY都会触发组件重渲染,我们可以用React.memo包裹组件,避免父组件重渲染时这个卡片组件无意义的刷新:

const AppleTvParalaxCard = React.memo(function AppleTvParalaxCard({children, className}) {
  // 组件内部逻辑保持不变
});

另外,你可以把角度的存储格式从字符串改成数字,在传给styled组件时再拼接单位,这样state更新的类型更稳定,也能减少一些不必要的计算开销:

// 初始化state为数字
const [rotationDegX, setrotationDegX] = useState(0);
const [rotationDegY, setrotationDegY] = useState(0);

// 事件处理里计算数字值
const degX = (coorY / halfH) * 10;
const degY = (coorX / halfW) * -10;
setrotationDegX(degX);
setrotationDegY(degY);

// styled组件里拼接单位
const Card = styled.div`
  // 其他样式不变
  &:hover {
    transform: scale(1.1) perspective(600px) rotateX(${props => `${props.degX}deg`}) rotateY(${props => `${props.degY}deg`});
    // 其他hover样式不变
  }
`;

3. 优化mousemove事件的触发频率

鼠标移动时会触发大量mousemove事件,频繁的setState会导致组件频繁重渲染。可以用节流(throttle)来限制事件触发的频率,比如每16ms(约60帧)更新一次角度:

先实现一个简单的节流函数(无需引入额外依赖):

const throttle = (func, delay) => {
  let lastCall = 0;
  return (...args) => {
    const now = Date.now();
    if (now - lastCall >= delay) {
      lastCall = now;
      func(...args);
    }
  };
};

然后用节流包裹handleMouseOver:

const handleMouseOver = useCallback(throttle(e => {
  // 原有的计算逻辑
}, 16), []);

这样既能保证动画的流畅性(60帧),又能减少不必要的setState调用和组件重渲染。

4. 避免重复获取DOM布局信息

每次mousemove调用getBoundingClientRect()会触发浏览器的重排(reflow),频繁调用会影响性能。可以在mouseenter时获取一次DOM的位置和尺寸,存在ref里,后续mousemove直接使用这些值:

const rectRef = useRef(null);

const handleMouseEnter = useCallback(e => {
  rectRef.current = e.target.getBoundingClientRect();
}, []);

const handleMouseOver = useCallback(throttle(e => {
  const { y, x, width, height } = rectRef.current;
  // 后续计算逻辑不变
}, 16), []);

// 记得在useEffect里添加mouseenter监听器
useEffect(() => {
  const currentDom = domRef.current;
  currentDom.addEventListener("mouseenter", handleMouseEnter);
  currentDom.addEventListener("mousemove", handleMouseOver);
  currentDom.addEventListener("mouseout", handleMouseOut);

  return () => {
    currentDom.removeEventListener("mouseenter", handleMouseEnter);
    currentDom.removeEventListener("mousemove", handleMouseOver);
    currentDom.removeEventListener("mouseout", handleMouseOut);
  };
}, [handleMouseEnter, handleMouseOver, handleMouseOut]);

5. 开启硬件加速优化动画

虽然你已经用了transform(浏览器会自动尝试硬件加速),但可以显式添加will-change属性告诉浏览器提前准备动画优化:

const Card = styled.div`
  box-sizing: border-box;
  box-shadow:0 2px 10px rgba( 0,0,0,.3 );
  border-radius: 8px;
  margin-bottom: 20px;
  transition: transform .5s cubic-bezier(.215, .61, .355, 1), box-shadow .5s cubic-bezier(.215, .61, .355, 1);
  will-change: transform, box-shadow; // 添加这一行
  &:hover {
    transform: scale(1.1) perspective(600px) rotateX(${props => `${props.degX}deg`}) rotateY(${props => `${props.degY}deg`});
    box-shadow: 0 15px 30px rgba(0, 0, 0, 0.3);
  }
`;

最终优化后的完整组件

把上面的优化点整合起来,最终的组件代码如下:

import React, { useState, useRef, useEffect, useCallback } from 'react';
import styled from 'styled-components';

const throttle = (func, delay) => {
  let lastCall = 0;
  return (...args) => {
    const now = Date.now();
    if (now - lastCall >= delay) {
      lastCall = now;
      func(...args);
    }
  };
};

const Card = styled.div`
  box-sizing: border-box;
  box-shadow:0 2px 10px rgba( 0,0,0,.3 );
  border-radius: 8px;
  margin-bottom: 20px;
  transition: transform .5s cubic-bezier(.215, .61, .355, 1), box-shadow .5s cubic-bezier(.215, .61, .355, 1);
  will-change: transform, box-shadow;
  &:hover {
    transform: scale(1.1) perspective(600px) rotateX(${props => `${props.degX}deg`}) rotateY(${props => `${props.degY}deg`});
    box-shadow: 0 15px 30px rgba(0, 0, 0, 0.3);
  }
`;

const AppleTvParalaxCard = React.memo(function AppleTvParalaxCard({children, className}) {
  const domRef = useRef();
  const rectRef = useRef(null);
  const [rotationDegX, setrotationDegX] = useState(0);
  const [rotationDegY, setrotationDegY] = useState(0);

  const handleMouseEnter = useCallback(e => {
    rectRef.current = e.target.getBoundingClientRect();
  }, []);

  const handleMouseOver = useCallback(throttle(e => {
    const { y, x, width, height } = rectRef.current;
    const { pageX, pageY } = e;
    const halfW = width / 2;
    const halfH = height / 2;
    const coorX = halfW - (pageX - x);
    const coorY = halfH - (pageY - (y + window.scrollY));
    const degX = (coorY / halfH) * 10;
    const degY = (coorX / halfW) * -10;
    setrotationDegX(degX);
    setrotationDegY(degY);
  }, 16), []);

  const handleMouseOut = useCallback(e => {
    setrotationDegX(0);
    setrotationDegY(0);
  }, []);

  useEffect(() => {
    const currentDom = domRef.current;
    currentDom.addEventListener("mouseenter", handleMouseEnter);
    currentDom.addEventListener("mousemove", handleMouseOver);
    currentDom.addEventListener("mouseout", handleMouseOut);

    return () => {
      currentDom.removeEventListener("mouseenter", handleMouseEnter);
      currentDom.removeEventListener("mousemove", handleMouseOver);
      currentDom.removeEventListener("mouseout", handleMouseOut);
    };
  }, [handleMouseEnter, handleMouseOver, handleMouseOut]);

  return (
    <Card ref={domRef} className={className} degX={rotationDegX} degY={rotationDegY}>
      {children}
    </Card>
  );
});

export default AppleTvParalaxCard;

这些优化应该能显著提升动画的流畅度,解决你遇到的卡顿问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 10:27:45