当然可以。思路是:assetPool 里优先存 ImageBitmap(用于预览渲染更快),同时保留原始 Blob(用于导出/再次像素处理)。
然后 store 里仍然只存 key。
下面给你一套“能直接用”的示范(纯内存,不用 IndexedDB)。
1) 内存资产池:同时存 Blob + ImageBitmap
// assetPool.ts
export type AssetRecord = {
blob: Blob;
bitmap: ImageBitmap; // 用于快速渲染
width: number;
height: number;
};
const g = globalThis as any;
const pool: Map<string, AssetRecord> =
g.__imgAssetPool ?? (g.__imgAssetPool = new Map());
export function putAsset(blob: Blob): Promise<string> {
return new Promise(function (resolve, reject) {
(async function () {
try {
const bitmap = await createImageBitmap(blob);
const key = crypto.randomUUID();
pool.set(key, {
blob: blob,
bitmap: bitmap,
width: bitmap.width,
height: bitmap.height,
});
resolve(key);
} catch (err) {
reject(err);
}
})();
});
}
export function getAsset(key: string): AssetRecord | undefined {
return pool.get(key);
}
export function deleteAsset(key: string): void {
const rec = pool.get(key);
if (rec) {
// ImageBitmap 需要主动释放
rec.bitmap.close();
}
pool.delete(key);
}
export function keys(): string[] {
return Array.from(pool.keys());
}
✅ 重点:
createImageBitmap(blob):异步解码,通常比<img>解码更可控bitmap.close():释放显存/内存(很重要,不然会涨)
2) React 中:用 key 拿到 ImageBitmap(用于画布/预览)
// useAssetBitmap.ts
import { useMemo } from "react";
import { getAsset } from "./assetPool";
export function useAssetBitmap(assetKey: string | null): ImageBitmap | null {
return useMemo(function () {
if (!assetKey) return null;
const rec = getAsset(assetKey);
if (!rec) return null;
return rec.bitmap;
}, [assetKey]);
}
这里不用 state / effect,是因为 bitmap 已经在 pool 里准备好了。
3) 在 Canvas2D 里画 ImageBitmap(最直接)
import React, { useEffect, useRef } from "react";
import { useAssetBitmap } from "./useAssetBitmap";
export function BitmapCanvas(props: { assetKey: string | null }) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const bitmap = useAssetBitmap(props.assetKey);
useEffect(function () {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!bitmap) return;
// 示例:把图按 contain 画进 800x600 画布
const cw = canvas.width;
const ch = canvas.height;
const scale = Math.min(cw / bitmap.width, ch / bitmap.height);
const dw = bitmap.width * scale;
const dh = bitmap.height * scale;
const dx = (cw - dw) / 2;
const dy = (ch - dh) / 2;
ctx.drawImage(bitmap, dx, dy, dw, dh);
}, [bitmap]);
return <canvas ref={canvasRef} width={800} height={600} />;
}
✅ 这就是 ImageBitmap 的爽点:ctx.drawImage(bitmap, ...) 直接画,解码成本已经付过了。
4) 在 Konva 里用 ImageBitmap(你会用得到)
Konva 的 Image 节点的 image 属性可以接收 CanvasImageSource,而 ImageBitmap 就是其中一种。你可以直接传它(前提是版本/浏览器支持没问题)。
示例(思路级):
import { Image as KonvaImage } from "react-konva";
import { useAssetBitmap } from "./useAssetBitmap";
function RasterLayer(props: { assetKey: string | null; x: number; y: number; w: number; h: number }) {
const bitmap = useAssetBitmap(props.assetKey);
if (!bitmap) return null;
return (
<KonvaImage
image={bitmap as any}
x={props.x}
y={props.y}
width={props.w}
height={props.h}
listening={false}
/>
);
}
如果你遇到某些浏览器/konva 版本对 ImageBitmap 支持不稳定,就退回到
HTMLImageElement + objectURL的方式,但大多数现代环境是 OK 的。
5) 你做像素处理时:继续用 Blob(更通用)
虽然预览用 bitmap,但处理一般建议从 blob 来,因为:
- 你可能要 worker / offscreen
- 你可能要重新编码输出
- blob 更易传输与存储
拿 blob:
import { getAsset } from "./assetPool";
export function getBlob(assetKey: string): Blob | null {
const rec = getAsset(assetKey);
return rec ? rec.blob : null;
}
6) 内存回收(必须做)
当你撤销/新提交时,删掉不用的 key:
import { deleteAsset, keys } from "./assetPool";
export function gcAssets(keepKeys: Set<string>): void {
const all = keys();
for (const k of all) {
if (!keepKeys.has(k)) {
deleteAsset(k); // 会 close bitmap
}
}
}
你可以怎么用在你的 store 流程里
putAsset(blob) -> key(异步)- store 里
currentKey = key - 预览组件用
useAssetBitmap(currentKey)快速显示 - commit 产出新 blob,再
putAsset存进去,history 只存 key
如果你愿意,我还能顺手补一段:commit 时如何把 canvas 结果编码成 webp blob(带质量参数),以及“避免 UI 卡顿”的 worker/offscreen 版本。