在 Three 项目中使用 IndexedDB 缓存 静态资源,使加载速度飞起来-简易版

在 Three.js 或 Web3D 项目里,模型文件(GLB、FBX、OBJ)、HDR 环境贴图、纹理资源通常都比较大,每次进入页面都要重新从网络加载,导致:

  • 加载速度慢

  • 白屏时间长

  • 移动端耗流量

而浏览器本地存储(localStorage / memory / HTTP cache)又不适合存放二进制大文件,因此"本地持久缓存 + 二进制文件"最合适的方案是:

IndexedDB + ArrayBuffer 的持久缓存方案

本篇文章主要介绍一个实战级别的 IndexedDB 缓存实现,包括:

  • 封装通用存取工具类(支持二进制)

  • 缓存 GLB/GTLF 解析(parse)

  • 缓存 HDR/贴图(Blob + URL)

  • 性能分析与最佳实践

并给出你可以直接使用的完整源码。


📦 一、为什么要用 IndexedDB

IndexedDB 是浏览器内置的 NoSQL 持久化数据库,特点:

特点 说明
支持大容量 大多数浏览器可达到 500MB 以上
可存 ArrayBuffer / Blob 适合二进制资源缓存
异步 不阻塞页面渲染
可控 自己管理缓存策略,如 TTL / LRU

对于 Web3D 项目的大模型、HDR 贴图、纹理文件缓存,IndexedDB 是最佳方案。


📁 二、IndexedDBStore:可直接复用的数据库封装

为了优雅使用 IndexedDB,我们封装了一个轻量级工具类,只关心:

  • 打开数据库

  • 存储(set)

  • 读取(get)

  • 删除(delete)

  • 清空(clear)

通用性强,可用于所有 ArrayBuffer 缓存。

javascript 复制代码
// db.js
import * as THREE from "three";
const DB_NAME = "GLB_CACHE_DB";
const STORE_NAME = "glb_store";
const DB_VERSION = 1;

class IndexedDBStore {
  constructor({ dbName, storeName, version = 1 }) {
    console.log("IndexedDBStore");
    this.dbName = dbName;
    this.storeName = storeName;
    this.version = version;
    this.dbPromise = null;
  }

  open() {
    if (this.dbPromise) return this.dbPromise;
    this.dbPromise = new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);

      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        if (!db.objectStoreNames.contains(this.storeName)) {
          db.createObjectStore(this.storeName);
        }
      };

      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
    return this.dbPromise;
  }

  async set(key, value) {
    const db = await this.open();
    return new Promise((resolve, reject) => {
      const tx = db.transaction([this.storeName], "readwrite");
      tx.objectStore(this.storeName).put(value, key).onsuccess = resolve;
      tx.onerror = () => reject(tx.error);
    });
  }

  async get(key) {
    const db = await this.open();
    return new Promise((resolve, reject) => {
      const tx = db.transaction([this.storeName], "readonly");
      const req = tx.objectStore(this.storeName).get(key);
      req.onsuccess = () => resolve(req.result ?? null);
      req.onerror = () => reject(req.error);
    });
  }

  async delete(key) {
    const db = await this.open();
    return new Promise((resolve, reject) => {
      const tx = db.transaction([this.storeName], "readwrite");
      tx.objectStore(this.storeName).delete(key).onsuccess = resolve;
      tx.onerror = () => reject(tx.error);
    });
  }

  async clear() {
    const db = await this.open();
    return new Promise((resolve, reject) => {
      const tx = db.transaction([this.storeName], "readwrite");
      tx.objectStore(this.storeName).clear().onsuccess = resolve;
      tx.onerror = () => reject(tx.error);
    });
  }
}

// ⭐ 初始化一次单例
const dbStore = new IndexedDBStore({
  dbName: DB_NAME,
  storeName: STORE_NAME,
  version: DB_VERSION,
});

export default dbStore;

🧩 三、缓存不同类型资源的最佳方式

不同资源类型需要不同处理方式:

资源类型 最佳缓存方式 原因
GLB / GLTF ArrayBuffer → loader.parse glTF Loader 直接支持 parse
STL / OBJ / FBX ArrayBuffer → Blob → URL loader.load 需要 URL
HDR(RGBELoader) ArrayBuffer → Blob → URL 环境贴图必须 URL 形式加载
纹理 jpg/png/webp 网络缓存即可 双缓存意义不大
MP3/MP4 根据业务可加可不加 体积大时需斟酌

🔥 四、缓存 GLB / GLTF / STL 的函数(parse 方式)

GLTFLoader / DRACOLoader 支持:

javascript 复制代码
loader.parse(arrayBuffer, path, callback)

因此缓存命中后无需再次请求。

javascript 复制代码
// 缓存 GLB / GLTF / STL 模型
export async function loadParseWithCache(loader, url, key) {
  const cache = await dbStore.get(key);
  if (cache) {
    return new Promise((resolve) => {
      loader.parse(cache, "", (gltf) => resolve(gltf));
    });
  }
  return new Promise((resolve, reject) => {
    const fileLoader = new THREE.FileLoader();
    fileLoader.setResponseType("arraybuffer");
    fileLoader.load(
      url,
      async (buffer) => {
        await dbStore.set(key, buffer);
        loader.parse(buffer, "", (gltf) => resolve(gltf));
      },
      undefined,
      reject
    );
  });
}

使用:

javascript 复制代码
import { loadParseWithCache } from "@/three/wisdormScreen/utils/dbGLB.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";

 const loader = new GLTFLoader();
   
 const dracoLoader = new DRACOLoader();
 dracoLoader.setDecoderPath("draco/gltf/");
 loader.setDRACOLoader(dracoLoader);

 loadParseWithCache(loader , "/model/floor/floor.glb", FLOOR_KEY).then((gltf) => {
    //对返回的模型进行操作
    
 })

🌈 五、缓存 HDR 环境贴图(Blob + URL)

HDR 文件必须通过 RGBELoader.load(url) 加载,因此要:

  1. 缓存 ArrayBuffer

  2. 转成 Blob

  3. 创建 ObjectURL 再 load

javascript 复制代码
export async function loadHDRWithCache(rgbLoader, url, key) {
  // 1. 如果有缓存,直接使用 RGBELoader 加载 Blob URL
  const cacheBuffer = await dbStore.get(key);
  if (cacheBuffer) {
    const blobUrl = URL.createObjectURL(new Blob([cacheBuffer]));
    return new Promise((resolve, reject) => {
      rgbLoader.load(
        blobUrl,
        (tex) => {
          URL.revokeObjectURL(blobUrl);
          resolve(tex);
        },
        undefined,
        reject
      );
    });
  }

  // 2. 第一次从网络加载 → 使用 FileLoader 取 buffer → 再用 RGBELoader 转换为 HDR
  const fileLoader = new THREE.FileLoader();
  fileLoader.setResponseType("arraybuffer");

  return new Promise((resolve, reject) => {
    fileLoader.load(
      url,
      async (buffer) => {
        // 写入缓存
        await dbStore.set(key, buffer);

        // 关键:必须用 RGBELoader,而不是 FileLoader 再 load!
        const blobUrl = URL.createObjectURL(new Blob([buffer]));

        rgbLoader.load(
          blobUrl,
          (tex) => {
            URL.revokeObjectURL(blobUrl);
            resolve(tex);
          },
          undefined,
          reject
        );
      },
      undefined,
      reject
    );
  });
}

🧪 六、实际使用示例

javascript 复制代码
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";


async function addHdr(){

 const loader = new RGBELoader();
 const texture = await loadHDRWithCache(loader,"/hdr/12.hdr", "HDR_BG_12")
 hdrTexture = texture;
 texture.mapping = THREE.EquirectangularReflectionMapping;
 scene.background = texture;

}

首次加载需几秒,第二次刷新页面瞬间完成


🚄 七、性能提升对比

项目 未缓存 IndexedDB 缓存
GLB 模型加载 2--8 秒 < 30ms
HDR 贴图加载 1--5 秒 < 20ms
页面白屏 明显 几乎没有
移动端流量 每次加载 首次加载一次

缓存完全命中后,不再消耗网络时间。


🎯 八、总结:这是 Web3D 的必备基础设施

通过 IndexedDB 缓存大型资源,你能获得:

  • 更快的加载速度

  • 大幅减少网络请求

  • 离线体验

  • 提高用户留存

这是 Three.js/Web3D 项目性能优化的必备方案。

如果你后续需要:

✅ 支持 LRU 最大容量限制

✅ 支持 分片写入 2MB (大模型)

✅ 支持 缓存过期 TTL

✅ 支持 缓存命中统计

我也可以帮你继续扩展。


🎯 九.完整版代码:

javascript 复制代码
// db.js
import * as THREE from "three";
const DB_NAME = "GLB_CACHE_DB";
const STORE_NAME = "glb_store";
const DB_VERSION = 1;

class IndexedDBStore {
  constructor({ dbName, storeName, version = 1 }) {
    console.log("IndexedDBStore");
    this.dbName = dbName;
    this.storeName = storeName;
    this.version = version;
    this.dbPromise = null;
  }

  open() {
    if (this.dbPromise) return this.dbPromise;
    this.dbPromise = new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);

      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        if (!db.objectStoreNames.contains(this.storeName)) {
          db.createObjectStore(this.storeName);
        }
      };

      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
    return this.dbPromise;
  }

  async set(key, value) {
    const db = await this.open();
    return new Promise((resolve, reject) => {
      const tx = db.transaction([this.storeName], "readwrite");
      tx.objectStore(this.storeName).put(value, key).onsuccess = resolve;
      tx.onerror = () => reject(tx.error);
    });
  }

  async get(key) {
    const db = await this.open();
    return new Promise((resolve, reject) => {
      const tx = db.transaction([this.storeName], "readonly");
      const req = tx.objectStore(this.storeName).get(key);
      req.onsuccess = () => resolve(req.result ?? null);
      req.onerror = () => reject(req.error);
    });
  }

  async delete(key) {
    const db = await this.open();
    return new Promise((resolve, reject) => {
      const tx = db.transaction([this.storeName], "readwrite");
      tx.objectStore(this.storeName).delete(key).onsuccess = resolve;
      tx.onerror = () => reject(tx.error);
    });
  }

  async clear() {
    const db = await this.open();
    return new Promise((resolve, reject) => {
      const tx = db.transaction([this.storeName], "readwrite");
      tx.objectStore(this.storeName).clear().onsuccess = resolve;
      tx.onerror = () => reject(tx.error);
    });
  }
}

// ⭐ 直接在模块加载时初始化一次
const dbStore = new IndexedDBStore({
  dbName: DB_NAME,
  storeName: STORE_NAME,
  version: DB_VERSION,
});

export default dbStore;


// 资源文件类型
// 模型:glb/gltf、FBX、OBJ、STL、...
// 纹理贴图:jpg、png、webp
// HDR环境贴图:hdr、exr
// 音频:.mp3, .wav, .ogg
// 视频:mp4/webm

// 缓存 GLB、GLTF、STL模型
export async function loadParseWithCache(loader, url, key) {
  const cache = await dbStore.get(key);
  if (cache) {
    return new Promise((resolve) => {
      loader.parse(cache, "", (gltf) => resolve(gltf));
    });
  }
  return new Promise((resolve, reject) => {
    const fileLoader = new THREE.FileLoader();
    fileLoader.setResponseType("arraybuffer");
    fileLoader.load(
      url,
      async (buffer) => {
        await dbStore.set(key, buffer);
        loader.parse(buffer, "", (gltf) => resolve(gltf));
      },
      undefined,
      reject
    );
  });
}

// 缓存fbx、obj、hdr环境贴图
export async function loadHDRWithCache(rgbLoader, url, key) {
  // 1. 如果有缓存,直接使用 RGBELoader 加载 Blob URL
  const cacheBuffer = await dbStore.get(key);
  if (cacheBuffer) {
    const blobUrl = URL.createObjectURL(new Blob([cacheBuffer]));
    return new Promise((resolve, reject) => {
      rgbLoader.load(
        blobUrl,
        (tex) => {
          URL.revokeObjectURL(blobUrl);
          resolve(tex);
        },
        undefined,
        reject
      );
    });
  }

  // 2. 第一次从网络加载 → 使用 FileLoader 取 buffer → 再用 RGBELoader 转换为 HDR
  const fileLoader = new THREE.FileLoader();
  fileLoader.setResponseType("arraybuffer");

  return new Promise((resolve, reject) => {
    fileLoader.load(
      url,
      async (buffer) => {
        // 写入缓存
        await dbStore.set(key, buffer);

        // 关键:必须用 RGBELoader,而不是 FileLoader 再 load!
        const blobUrl = URL.createObjectURL(new Blob([buffer]));

        rgbLoader.load(
          blobUrl,
          (tex) => {
            URL.revokeObjectURL(blobUrl);
            resolve(tex);
          },
          undefined,
          reject
        );
      },
      undefined,
      reject
    );
  });
}
// 其他不考虑上面方法的资源文件,直接从网络加载
相关推荐
zlpzlpzyd2 小时前
vue.js 3中全局组件和局部组件的区别
前端·javascript·vue.js
浩星2 小时前
css实现类似element官网的磨砂屏幕效果
前端·javascript·css
一只小风华~2 小时前
Vue.js 核心知识点全面解析
前端·javascript·vue.js
2022.11.7始学前端2 小时前
n8n第七节 只提醒重要的待办
前端·javascript·ui·n8n
SakuraOnTheWay2 小时前
React Grab实践 | 记一次与Cursor的有趣对话
前端·cursor
阿星AI工作室2 小时前
gemini3手势互动圣诞树保姆级教程来了!附提示词
前端·人工智能
徐小夕2 小时前
知识库创业复盘:从闭源到开源,这3个教训价值百万
前端·javascript·github
xhxxx2 小时前
函数执行完就销毁?那闭包里的变量凭什么活下来!—— 深入 JS 内存模型
前端·javascript·ecmascript 6
StarkCoder2 小时前
求求你试试 DiffableDataSource!别再手算 indexPath 了(否则迟早崩)
前端
fxshy2 小时前
Cursor 前端Global Cursor Rules
前端·cursor