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

图片缩放实现存在卡顿问题(附复现链接)

React+TypeScript图片缩放卡顿的优化方案

我用React+TypeScript实现的图片缩放效果存在卡顿,使用无小数的整数坐标能稍微改善,但仍有明显卡顿问题。原实现代码如下:

import * as React from 'react';
import './style.css';

export default function App() {
  const [initZoomData, setInitZoomData] = 
    React.useState<Record<string, number>>();

  const [zoomData, setZoomData] = React.useState<Record<string, number>>({});

  const onImageOver = (e: React.PointerEvent<HTMLImageElement>) => {
    const image = e.currentTarget;
    const wrapper = image.parentElement;
    if (!wrapper) return;

    const top = wrapper.getBoundingClientRect().top;
    const width = wrapper.getBoundingClientRect().width;
    const height = wrapper.getBoundingClientRect().height;
    const left = wrapper.getBoundingClientRect().left;

    const scale = 2;
    const coeff = (width * scale) / ((width * scale - width) / 2);

    const centerX = left + width / 2;
    const centerY = top + height / 2;

    setInitZoomData({
      top,
      width,
      height,
      left,
      centerX,
      centerY,
      coeff,
      scale,
    });
  };

  const onImageMove = (e: React.PointerEvent<HTMLImageElement>) => {
    if (!initZoomData) return;

    const { scale, top, width, height, left, centerX, centerY } = initZoomData;

    let offsetX;
    let offsetY;

    if (e.pageX > centerX) {
      offsetX = ((centerX - e.clientX) / (width / 2)) * 100;
    } else {
      offsetX = 100 - ((e.clientX - left) / (width / 2)) * 100;
    }

    if (e.pageY > centerY) {
      offsetY = ((centerY - e.clientY) / (height / 2)) * 100;
    } else {
      offsetY = 100 - ((e.clientY - top) / (height / 2)) * 100;
    }

    setZoomData({
      scale,
      offsetX,
      offsetY,
    });
  };

  const onImageLeave = () => {
    setInitZoomData(undefined);
  };

  return (
    <div className="imageWrapperInner">
      <img
        style={{
          transform: initZoomData
            ? `scale(${zoomData.scale}) translate(${zoomData.offsetX / 4}%, ${zoomData.offsetY / 4}%)`
            : 'scale(1)',
        }}
        onPointerOver={onImageOver}
        onPointerMove={onImageMove}
        onPointerLeave={onImageLeave}
        src="https://createx.createx.studio/assets/img/ecommerce/shop/09.jpg"
        alt="Product image."
        className="image"
        loading="lazy"
      />
    </div>
  );
}

核心优化方向

  • 避免React状态频繁更新
    原代码中onPointerMove每次鼠标移动都会调用setZoomData,触发组件重渲染,这是卡顿的主要原因。改用useRef存储实时计算的缩放参数,直接操作DOM更新transform,跳过React的重渲染流程,同时用requestAnimationFrame确保动画帧对齐浏览器刷新节奏。

  • 优化DOM读取操作
    getBoundingClientRect会触发浏览器重排,原代码中多次调用该方法,应改为只调用一次并解构复用结果:

    const rect = wrapper.getBoundingClientRect();
    const { top, width, height, left } = rect;
    
  • 开启GPU硬件加速
    给图片元素添加will-change: transform或transform: translateZ(0),让浏览器将图片放到GPU层渲染,减少CPU计算压力:

    .image {
      will-change: transform;
    }
    
  • 简化偏移计算逻辑
    原代码的offset计算有冗余分支,可简化为基于鼠标与中心的差值直接计算,同时改用px单位替代百分比,避免浏览器重复计算布局:

    const deltaX = e.clientX - centerX;
    const deltaY = e.clientY - centerY;
    const offsetX = deltaX * (scale - 1);
    const offsetY = deltaY * (scale - 1);
    
  • 防抖指针事件(可选)
    若鼠标移动时计算量仍过大,可给onPointerMove添加防抖处理,控制计算频率,避免短时间内触发过多计算。

优化后的完整代码

import * as React from 'react';
import './style.css';

export default function App() {
  const imageRef = React.useRef<HTMLImageElement>(null);
  const initZoomDataRef = React.useRef<Record<string, number>>();
  const zoomDataRef = React.useRef<Record<string, number>>({ scale: 1, offsetX: 0, offsetY: 0 });

  const updateImageTransform = () => {
    if (!imageRef.current) return;
    const { scale, offsetX, offsetY } = zoomDataRef.current;
    imageRef.current.style.transform = `scale(${scale}) translate(${offsetX}px, ${offsetY}px)`;
  };

  const onImageOver = (e: React.PointerEvent<HTMLImageElement>) => {
    const image = e.currentTarget;
    const wrapper = image.parentElement;
    if (!wrapper) return;

    const rect = wrapper.getBoundingClientRect();
    const { top, width, height, left } = rect;
    const scale = 2;
    const centerX = left + width / 2;
    const centerY = top + height / 2;

    initZoomDataRef.current = {
      top,
      width,
      height,
      left,
      centerX,
      centerY,
      scale,
    };

    zoomDataRef.current = { scale, offsetX: 0, offsetY: 0 };
    updateImageTransform();
  };

  const onImageMove = (e: React.PointerEvent<HTMLImageElement>) => {
    const initData = initZoomDataRef.current;
    if (!initData) return;

    const { scale, centerX, centerY } = initData;
    const deltaX = e.clientX - centerX;
    const deltaY = e.clientY - centerY;
    const offsetX = deltaX * (scale - 1);
    const offsetY = deltaY * (scale - 1);

    zoomDataRef.current = { scale, offsetX, offsetY };
    requestAnimationFrame(updateImageTransform);
  };

  const onImageLeave = () => {
    initZoomDataRef.current = undefined;
    zoomDataRef.current = { scale: 1, offsetX: 0, offsetY: 0 };
    updateImageTransform();
  };

  return (
    <div className="imageWrapperInner">
      <img
        ref={imageRef}
        onPointerOver={onImageOver}
        onPointerMove={onImageMove}
        onPointerLeave={onImageLeave}
        src="https://createx.createx.studio/assets/img/ecommerce/shop/09.jpg"
        alt="Product image."
        className="image"
        loading="lazy"
      />
    </div>
  );
}

对应的CSS补充:

.image {
  will-change: transform;
  /* 可选:添加平滑过渡提升体验 */
  transition: transform 0.1s ease-out;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 08:30:05