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

React+Three.js网页高清图片渐进式加载优化求助

React+Three.js网页高清图片渐进式加载优化求助

看起来你已经搭好了分画质加载的雏形,但核心问题是当前逻辑还是在等所有高清图加载完才让用户看到页面,完全没发挥low/mid/high文件夹的渐进式优势。我帮你重构代码,实现真正的「先看低清占位,后台悄悄升级画质」的流畅体验,同时把加载屏的逻辑改合理,让用户不用等10秒才能看到内容~


核心优化思路

渐进式加载的关键是拆分加载优先级:

  1. 「关键资源」(低清图+3D模型)优先加载,加载完成后立刻隐藏加载屏,让用户看到内容
  2. 「更高清资源」(中/高清图)在后台静默加载,加载完成后无缝替换当前显示的低清图
  3. 加载状态管理要清晰,避免多个useEffect的逻辑冲突

逐步优化方案(附代码修改)

1. 重构加载状态:区分「关键加载」和「后台加载」

把原来混乱的isLoading拆成两个状态:

  • isLoadingCritical:控制加载屏,只等低清图+3D模型加载完成
  • 后台加载的中/高清图不阻塞页面显示,只在后台跟踪进度
function VitrineCanvas() {
  // 把主题和图片数绑定,结构更清晰
  const imageThemes = [
    { name: 'noir-blanc', count: 8 },
    { name: 'nerd', count: 18 },
    { name: 'bg', count: 31 }
  ];

  // 核心状态:仅控制加载屏的关键资源加载状态
  const [isLoadingCritical, setIsLoadingCritical] = useState(true);
  const [criticalLoadedCount, setCriticalLoadedCount] = useState(0);
  const totalCriticalImages = imageThemes.reduce((sum, theme) => sum + theme.count, 0);

  // 用@react-three/drei的useProgress自动跟踪3D模型加载进度
  const { progress: modelProgress } = useProgress();
  // ...其他状态
}

2. 实现分阶段加载与无缝替换

封装一个工具函数,用来后台加载更高清的图片并替换当前图片,同时加小延迟避免浏览器请求阻塞:

// 封装渐进式替换函数:从fromQuality加载toQuality,完成后替换
const preloadAndReplace = async (fromQuality, toQuality, themeName, imageCount) => {
  for (let i = 1; i <= imageCount; i++) {
    const imgElement = document.querySelector(`.VitrineImg-${themeName}-${i}`);
    if (!imgElement || imgElement.src.includes(toQuality)) continue;

    await new Promise((resolve) => {
      const newImg = new Image();
      newImg.onload = () => {
        imgElement.src = newImg.src; // 无缝替换,无闪烁
        resolve();
      };
      newImg.onerror = resolve; // 加载失败直接跳过,不影响其他图片
      newImg.src = `/pictures/${toQuality}/${themeName}/${i}.jpg`;
    });
    // 加50ms延迟,避免瞬间请求太多资源
    await new Promise(resolve => setTimeout(resolve, 50));
  }
};

3. 关键资源加载完成后立刻显示页面

优先加载低清图,加载完成后隐藏加载屏,同时后台启动中/高清图的加载:

useEffect(() => {
  let loaded = 0;

  // 1. 优先加载低清图(关键资源)
  const loadLowQuality = () => {
    imageThemes.forEach((theme) => {
      for (let i = 1; i <= theme.count; i++) {
        const img = new Image();
        const onLoadOrError = () => {
          loaded++;
          setCriticalLoadedCount(loaded);
          // 关键资源全部加载完成:所有低清图+3D模型
          if (loaded === totalCriticalImages && modelProgress === 100) {
            setIsLoadingCritical(false); // 隐藏加载屏,显示页面
            loadHigherQualities(); // 后台开始加载中/高清图
          }
        };
        img.onload = onLoadOrError;
        img.onerror = onLoadOrError;
        img.src = `/pictures/low/${theme.name}/${i}.jpg`;
      }
    });
  };

  loadLowQuality();
}, [modelProgress]);

// 2. 后台加载中、高清图
const loadHigherQualities = async () => {
  try {
    // 先加载中清图,替换低清图
    for (const theme of imageThemes) {
      await preloadAndReplace('low', 'mid', theme.name, theme.count);
    }
    // 中清图加载完成后,加载高清图替换中清图
    for (const theme of imageThemes) {
      await preloadAndReplace('mid', 'high', theme.name, theme.count);
    }
    console.log('✅ 所有高清图加载完成!');
  } catch (err) {
    console.error('❌ 高清图加载出错', err);
  }
};

4. 简化3D模型加载逻辑

用useGLTF的预加载功能优化首次加载,同时避免重复加载:

// 在组件顶部预加载3D模型,提升首次加载速度
useGLTF.preload('/3dModels/pictures_shower.glb');

function MyModel() {
  // useGLTF会自动处理加载,进度会被useProgress捕获
  const gltf = useGLTF('/3dModels/pictures_shower.glb');
  return <primitive object={gltf.scene} position={[0, -1.5, 0]} scale={2.5} />;
}

完整优化后代码示例

把所有改动整合到一起,整理成可运行的代码:

import './contact.css'
import './pics.css'
import { MyHeader } from './Welcome'
import { useGLTF, useProgress } from '@react-three/drei'
import React, { useState, useEffect } from 'react'
import { Canvas } from '@react-three/fiber'

// 预加载3D模型,优化首次加载体验
useGLTF.preload('/3dModels/pictures_shower.glb');

function MyModel() {
  const gltf = useGLTF('/3dModels/pictures_shower.glb')
  return <primitive object={gltf.scene} position={[0, -1.5, 0]} scale={2.5} />
}

// 封装渐进式加载替换函数
const preloadAndReplace = async (fromQuality, toQuality, themeName, imageCount) => {
  for (let i = 1; i <= imageCount; i++) {
    const imgElement = document.querySelector(`.VitrineImg-${themeName}-${i}`);
    if (!imgElement || imgElement.src.includes(toQuality)) continue;

    await new Promise((resolve) => {
      const newImg = new Image();
      newImg.onload = () => {
        imgElement.src = newImg.src;
        resolve();
      };
      newImg.onerror = resolve;
      newImg.src = `/pictures/${toQuality}/${themeName}/${i}.jpg`;
    });
    await new Promise(resolve => setTimeout(resolve, 50));
  }
};

export default function Pics() {
  const [model, setModel] = useState('none')
  return (
    <React.StrictMode>
      <MyHeader setModel={setModel} />
      <VitrineCanvas />
    </React.StrictMode>
  )
}

function VitrineCanvas() {
  const imageThemes = [
    { name: 'noir-blanc', count: 8 },
    { name: 'nerd', count: 18 },
    { name: 'bg', count: 31 }
  ];
  const [image, setImage] = useState(0)
  const [isLoadingCritical, setIsLoadingCritical] = useState(true);
  const [criticalLoadedCount, setCriticalLoadedCount] = useState(0);
  const totalCriticalImages = imageThemes.reduce((sum, theme) => sum + theme.count, 0);
  const { progress: modelProgress } = useProgress();
  const [rotationOffset, setRotationOffset] = useState(0)

  // 滚轮控制旋转逻辑(保持原逻辑)
  useEffect(() => {
    const container = document.querySelector('.carousel-3d')
    if (!container) return
    const onWheel = (e) => {
      e.preventDefault()
      const delta = e.deltaY > 0 ? 1 : -1
      const theme = imageThemes[image];
      setRotationOffset((prev) => prev + delta * (360 / theme.count))
    }
    container.addEventListener('wheel', onWheel)
    return () => container.removeEventListener('wheel', onWheel)
  }, [image])

  // 关键资源加载逻辑
  useEffect(() => {
    let loaded = 0;
    const loadLowQuality = () => {
      imageThemes.forEach((theme) => {
        for (let i = 1; i <= theme.count; i++) {
          const img = new Image();
          const onLoadOrError = () => {
            loaded++;
            setCriticalLoadedCount(loaded);
            if (loaded === totalCriticalImages && modelProgress === 100) {
              setIsLoadingCritical(false);
              loadHigherQualities();
            }
          };
          img.onload = onLoadOrError;
          img.onerror = onLoadOrError;
          img.src = `/pictures/low/${theme.name}/${i}.jpg`;
        }
      });
    };
    loadLowQuality();
  }, [modelProgress])

  // 后台加载更高清图片
  const loadHigherQualities = async () => {
    try {
      await imageThemes.forEach(theme => preloadAndReplace('low', 'mid', theme.name, theme.count));
      await imageThemes.forEach(theme => preloadAndReplace('mid', 'high', theme.name, theme.count));
      console.log('✅ 所有高清图加载完成');
    } catch (err) {
      console.error('❌ 高清图加载出错', err);
    }
  }

  return (
    <>
      {/* 加载屏:仅等待关键资源 */}
      {isLoadingCritical && (
        <div className="loading-screen" style={{
          position: 'fixed',
          inset: 0,
          background: '#000',
          color: '#fff',
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'center',
          alignItems: 'center',
          zIndex: 9999
        }}>
          <p>Chargement initial... {criticalLoadedCount}/{totalCriticalImages}</p>
          <p>Modèle 3D: {Math.round(modelProgress)}%</p>
        </div>
      )}
      <Canvas className="carousel-3d">
        <MyModel />
        {/* 在这里渲染你的图片元素,确保每个img的class为`VitrineImg-${theme.name}-${i}` */}
        {/* 例如:<img className="VitrineImg-noir-blanc-1" src="/pictures/low/noir-blanc/1.jpg" alt="..." /> */}
      </Canvas>
    </>
  )
}

额外优化建议

  1. 图片格式升级:把JPG转成WebP格式,体积可缩小50%左右,同时保留JPG作为降级方案
  2. CDN加速:将图片/3D模型托管到CDN,利用边缘节点加快全球用户的加载速度
  3. 可视区域懒加载:结合IntersectionObserver,仅加载当前3D carousel可视区域内的图片
  4. 3D模型压缩:用Blender开启DRACO压缩你的GLB模型,体积可大幅减小

这样改完后,用户打开页面几秒内就能看到低清图+3D场景,同时后台悄悄升级画质,完全解决了加载时间过长的问题~

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.09 03:04:52