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;
解决方案
核心问题分析
- URL.createObjectURL特性:每次调用该方法返回的URL字符串是唯一的,即使指向同一个File对象,这会让
useLoader误以为依赖变化,重复加载纹理触发重渲染。 - Part组件未缓存:父组件
Specs渲染时,若files引用变化(比如重新选择相同文件),会重新创建所有Part实例,即使image属性未实际改变。
具体修复步骤
- 缓存File对应的URL和纹理:用
useRef维护缓存对象,确保同一个File只创建一次URL、加载一次纹理。 - 给Part组件加React.memo:避免父组件渲染时,未改变props的Part组件重复渲染。
- 优化清理逻辑: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
相关产品推荐
相关产品推荐

