为几百份已保存的设计生成缩略图,通常要创建 Fabric.js 多个画布,每份设计一个。还没处理完,标签页的内存就耗尽了,因为浏览器归还画布内存很慢,而 Fabric.js 的 dispose 根本不会释放它。renderDocuments 只用几个复用的画布来做 canvas生成图片,每份文档完成后释放所有资源。
正在加载在线示例,代码在下方。
"use client";
import { type FabricDocument, renderDocuments } from "fabricjs-document-engine";
import { CircleStopIcon, ImagesIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react";
import { COLORS, DemoFrame, Status, Toolbar, ToolButton } from "./demo-ui";
import { PAGE } from "./use-fabric-canvas";
const COUNT = 24;
/** A saved design, as a database would return it. */
function savedDesign(index: number): FabricDocument {
const color = (offset: number) => COLORS[(index + offset) % COLORS.length];
return {
schemaVersion: 1,
id: `design-${index + 1}`,
createdAt: "",
updatedAt: "",
canvas: { width: PAGE.width, height: PAGE.height, background: "#ffffff" },
objects: [
{
type: "Rect",
originX: "left",
originY: "top",
left: 40,
top: 40,
width: 400,
height: 220,
rx: 24,
ry: 24,
fill: color(0),
},
{
type: "Circle",
originX: "left",
originY: "top",
left: 70 + (index % 5) * 50,
top: 80,
radius: 60,
fill: color(2),
},
{
type: "Textbox",
originX: "left",
originY: "top",
left: 250,
top: 120,
width: 170,
text: `#${index + 1}`,
fontSize: 64,
fontFamily: "sans-serif",
fill: "#0d0d0d",
},
],
metadata: {},
};
}
export default function BatchRenderDemo() {
const t = useTranslations("demos");
const [thumbnails, setThumbnails] = useState<
Array<{ id: string; url: string }>
>([]);
const [done, setDone] = useState(0);
const [running, setRunning] = useState(false);
const [message, setMessage] = useState("");
const controller = useRef<AbortController | null>(null);
const urls = useRef<string[]>([]);
// Thumbnails are object URLs; free them when the demo goes away.
useEffect(
() => () => urls.current.forEach((url) => URL.revokeObjectURL(url)),
[]
);
async function render() {
urls.current.forEach((url) => URL.revokeObjectURL(url));
urls.current = [];
setThumbnails([]);
setDone(0);
setRunning(true);
const current = new AbortController();
controller.current = current;
const started = performance.now();
const designs = Array.from({ length: COUNT }, (_, index) =>
savedDesign(index)
);
try {
for await (const { documentId, result } of renderDocuments(designs, {
format: "png",
scale: 0.25,
concurrency: 2,
signal: current.signal,
onProgress: ({ done: finished }) => setDone(finished),
})) {
if (!result) continue;
const url = URL.createObjectURL(result.blob);
urls.current.push(url);
setThumbnails((previous) => [
...previous,
{ id: documentId ?? url, url },
]);
}
setMessage(
t("batch.done", {
count: COUNT,
ms: Math.round(performance.now() - started),
})
);
} catch {
setMessage(t("batch.cancelled"));
} finally {
controller.current = null;
setRunning(false);
}
}
return (
<DemoFrame>
<ul
aria-label={t("batch.list")}
className="grid aspect-480/300 grid-cols-6 content-start gap-1.5 overflow-hidden rounded-lg border bg-muted/40 p-2"
>
{thumbnails.map(({ id, url }) => (
<li key={id} className="overflow-hidden rounded-sm border bg-white">
{/* eslint-disable-next-line @next/next/no-img-element -- a rendered PNG from the batch */}
<img src={url} alt={id} className="block aspect-480/300 w-full" />
</li>
))}
</ul>
<Toolbar>
<ToolButton
icon={ImagesIcon}
label={t("batch.render", { count: COUNT })}
primary
disabled={running}
onClick={() => void render()}
/>
<ToolButton
icon={CircleStopIcon}
label={t("batch.cancel")}
disabled={!running}
onClick={() => controller.current?.abort()}
/>
</Toolbar>
<Status tone={running ? "busy" : message ? "ok" : "idle"}>
{running
? t("batch.progress", { done, total: COUNT })
: message || t("batch.hint")}
</Status>
</DemoFrame>
);
}渲染缩略图
import { renderDocuments } from "fabricjs-document-engine";
for await (const { documentId, result, error } of renderDocuments(savedDocuments, {
format: "png",
scale: 0.25,
concurrency: 2,
})) {
if (result) await uploadThumbnail(documentId, result.blob);
else console.warn(documentId, error?.message);
}- 每份文档完成后就会返回结果,可以逐个上传后丢弃,不必全部留在内存里。
- 损坏的文档会报告自己的
error,其余文档继续渲染。 ExportOptions的所有字段都可以用,同一个循环可以做 canvas生成图片(PNG、JPEG),也可以导出 SVG 或 JSON。
来自数据库的文档
documents 可以是任意可迭代对象,也可以是异步可迭代对象,例如数据库查询的分页结果,这样所有文档不会同时留在内存里:
async function* allDesigns() {
for (let page = 0; ; page += 1) {
const rows = await db.designs.findMany({ skip: page * 50, take: 50 });
if (rows.length === 0) return;
yield* rows.map((row) => row.document);
}
}
for await (const rendered of renderDocuments(allDesigns(), { format: "png", scale: 0.5 })) {
// store rendered.result
}内存、取消和进度
| 配置项 | 作用 | 默认值 |
|---|---|---|
concurrency | 同时渲染几份文档,每份用自己的画布 | 2 |
onProgress | 每份文档完成后以 { done, total } 调用 | 无 |
signal | 以 EXPORT_ABORTED 停止整个批次 | 无 |
customObjects, assets, limits | 和 createDocumentEngine 相同 | 无 |
- 不再需要 Fabric.js 多个画布:无论有多少份文档,只存在
concurrency个画布。 - 每份文档完成后,它的对象、对象的缓存画布和导出用的画布都会立即释放。
- 用
break或signal提前停止时,所有画布也会被释放。