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

react-three-fiber中useLoader触发画布持续重渲染问题求助

问题描述

通过文件选择器选中4个文件并传入Specs组件,使用react-three-fiber的TextureLoader与useLoader钩子在场景中渲染组件。注释掉meshMaterial的map属性时可正常渲染4个平面,但添加纹理后会触发画布持续重渲染。尝试通过useMemo缓存纹理加载逻辑并传入image文件作为依赖,但未解决问题。推测问题可能是每次创建的imageUrl不同,导致useLoader每次接收新URL触发重渲染。

相关代码
import { useLoader } from "@react-three/fiber";
import React from "react";
import { DoubleSide, TextureLoader } from "three";

/**
 * specs value is in the order frame , stickL ,stickR
 * ,glass because it is being filtered in the handle input section
 *
 */
const specsValues = [
  { position: [0, -0.1, 0] },
  { position: [0.5, -0.1, -0.5], rotation: 0.5 * Math.PI },
  { position: [-0.5, -0.1, -0.5], rotation: -0.5 * Math.PI },
  { position: [0, -0.1, 0] },
];
//converst number into radian 
const Specs = ({ files }) => {
  console.log(files);
  function degToRad(x) {
    return x * (Math.PI / 180);
  }

  return (
    <mesh
      scale={[14, 6.75, 16.5]}
      position={[0, 22.5, 14]}
      rotation-x={degToRad(6)}
    >
      {files.map((e, index) => {
        return <Part image={e} key={index} index={index} />;
      })}
    </mesh>
  );
};

/**
 * specs value is in the order frame , stickL ,stickR
 * ,glass because it is being filtered in the handle input section
 *
 */

const Part = ({ image, index }) => {
  const imageUrl = React.useMemo(() => {
    if (typeof image === "string") {
      return image;
    } else {
      return URL.createObjectURL(image);
    }
  }, [image]);

  const texture = useLoader(TextureLoader, imageUrl); // Load texture only once

  // Clean up URL when component unmounts
  React.useEffect(() => {
    return () => {
      if (typeof image !== "string") {
        URL.revokeObjectURL(imageUrl);
      }
    };
  }, [image, imageUrl]);

  console.log(imageUrl);
  return (
    <mesh
      rotation-y={specsValues[index].rotation}
      position={specsValues[index].position}
    >
      <meshBasicMaterial
        color={"#fff"}
        side={DoubleSide}
        map={texture}
        // transparent={true}
        // map={useLoader(TextureLoader, imageUrl)}
      />
      <planeGeometry args={[1, 1]} />
    </mesh>
  );
};

export default Specs;
解决方案

核心问题分析

  1. URL.createObjectURL特性:每次调用该方法返回的URL字符串是唯一的,即使指向同一个File对象,这会让useLoader误以为依赖变化,重复加载纹理触发重渲染。
  2. Part组件未缓存:父组件Specs渲染时,若files引用变化(比如重新选择相同文件),会重新创建所有Part实例,即使image属性未实际改变。

具体修复步骤

  1. 缓存File对应的URL和纹理:用useRef维护缓存对象,确保同一个File只创建一次URL、加载一次纹理。
  2. 给Part组件加React.memo:避免父组件渲染时,未改变props的Part组件重复渲染。
  3. 优化清理逻辑:File对象变化时,及时清理旧的URL和纹理,避免内存泄漏。

修改后的代码

import { useLoader } from "@react-three/fiber";
import React, { useMemo, useEffect, useRef, memo } from "react";
import { DoubleSide, TextureLoader } from "three";

const specsValues = [
  { position: [0, -0.1, 0] },
  { position: [0.5, -0.1, -0.5], rotation: 0.5 * Math.PI },
  { position: [-0.5, -0.1, -0.5], rotation: -0.5 * Math.PI },
  { position: [0, -0.1, 0] },
];

const Specs = ({ files }) => {
  function degToRad(x) {
    return x * (Math.PI / 180);
  }

  return (
    <mesh
      scale={[14, 6.75, 16.5]}
      position={[0, 22.5, 14]}
      rotation-x={degToRad(6)}
    >
      {files.map((e, index) => {
        // 用File的唯一标识做key,避免index导致的组件复用错误
        const key = typeof e === "string" ? e : `${e.name}-${e.lastModified}`;
        return <Part image={e} key={key} index={index} />;
      })}
    </mesh>
  );
};

// 用React.memo缓存Part组件,减少不必要的重渲染
const Part = memo(({ image, index }) => {
  // 缓存URL和纹理的容器
  const cacheRef = useRef({ urls: new Map(), textures: new Map() });

  // 生成File/URL的唯一缓存key
  const getCacheKey = () => {
    return typeof image === "string" ? image : `${image.name}-${image.lastModified}`;
  };

  // 获取或创建URL
  const imageUrl = useMemo(() => {
    const key = getCacheKey();
    if (cacheRef.current.urls.has(key)) {
      return cacheRef.current.urls.get(key);
    }
    let url;
    if (typeof image === "string") {
      url = image;
    } else {
      url = URL.createObjectURL(image);
    }
    cacheRef.current.urls.set(key, url);
    return url;
  }, [image]);

  // 获取或加载纹理
  const texture = useMemo(() => {
    const key = getCacheKey();
    if (cacheRef.current.textures.has(key)) {
      return cacheRef.current.textures.get(key);
    }
    const loadedTexture = useLoader(TextureLoader, imageUrl);
    cacheRef.current.textures.set(key, loadedTexture);
    return loadedTexture;
  }, [imageUrl]);

  // 清理旧资源
  useEffect(() => {
    return () => {
      const oldKey = getCacheKey();
      if (typeof image !== "string") {
        const oldUrl = cacheRef.current.urls.get(oldKey);
        if (oldUrl) {
          URL.revokeObjectURL(oldUrl);
          cacheRef.current.urls.delete(oldKey);
        }
      }
      // 可选:释放纹理GPU内存
      // const oldTexture = cacheRef.current.textures.get(oldKey);
      // if (oldTexture) oldTexture.dispose();
      // cacheRef.current.textures.delete(oldKey);
    };
  }, [image]);

  return (
    <mesh
      rotation-y={specsValues[index].rotation}
      position={specsValues[index].position}
    >
      <meshBasicMaterial
        color={"#fff"}
        side={DoubleSide}
        map={texture}
      />
      <planeGeometry args={[1, 1]} />
    </mesh>
  );
});

export default Specs;

额外优化点

  • 替换index为File唯一标识作为组件key:避免文件顺序变化时组件错误复用。
  • 可选纹理释放:如果需要严格管理GPU内存,可在清理逻辑中调用texture.dispose()释放纹理资源。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 20:36:07