loadFromJSON 一次创建所有对象,所以一张包含几千个对象的户型图或长设计稿,会把标签页卡住直到加载完成。做 fabricjs 性能优化时,大文档的加载最值得关注,因为用户什么都看不到,也没法取消。引擎会分批加载、报告进度,并在需要时停下。
正在加载在线示例,代码在下方。
"use client";
import {
type FabricDocument,
isDocumentEngineError,
type LoadProgress,
type SerializedFabricObject,
} from "fabricjs-document-engine";
import { useDocumentEngine } from "fabricjs-document-engine/react";
import { CircleStopIcon, FolderOpenIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRef, useState } from "react";
import {
COLORS,
DemoCanvas,
DemoFrame,
Status,
Toolbar,
ToolButton,
} from "./demo-ui";
import { PAGE, useFabricCanvas } from "./use-fabric-canvas";
const COUNT = 20_000;
/** A document with thousands of small shapes, like a busy floor plan. */
function bigDocument(): FabricDocument {
const objects: SerializedFabricObject[] = Array.from(
{ length: COUNT },
(_, index) => ({
type: index % 3 === 0 ? "Circle" : "Rect",
originX: "left",
originY: "top",
left: Math.random() * (PAGE.width - 8),
top: Math.random() * (PAGE.height - 8),
width: 6,
height: 6,
radius: 3,
fill: COLORS[index % COLORS.length],
objectCaching: false,
})
);
return {
schemaVersion: 1,
id: `floor-plan-${Date.now()}`,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
canvas: { width: PAGE.width, height: PAGE.height, background: "#ffffff" },
objects,
metadata: {},
};
}
export default function LargeDocumentDemo() {
const t = useTranslations("demos");
const { elementRef, canvas } = useFabricCanvas();
const engine = useDocumentEngine(canvas);
const controller = useRef<AbortController | null>(null);
const [progress, setProgress] = useState<LoadProgress | null>(null);
const [message, setMessage] = useState("");
const loading =
controller.current !== null &&
progress !== null &&
progress.stage !== "done";
async function load() {
if (!engine) return;
const current = new AbortController();
controller.current = current;
const started = performance.now();
setMessage("");
try {
await engine.loadDocument(bigDocument(), {
signal: current.signal,
onProgress: setProgress,
discardUnsavedChanges: true,
});
setMessage(
t("large.done", {
count: COUNT,
ms: Math.round(performance.now() - started),
})
);
} catch (error) {
setMessage(
isDocumentEngineError(error) && error.code === "LOAD_ABORTED"
? t("large.cancelled")
: t("large.failed", { message: String(error) })
);
} finally {
controller.current = null;
setProgress(null);
}
}
const share =
progress && progress.total > 0 ? progress.done / progress.total : 0;
return (
<DemoFrame>
<DemoCanvas elementRef={elementRef} label={t("canvas")} />
<div
role="progressbar"
aria-label={t("large.progress")}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(share * 100)}
className="h-1.5 overflow-hidden rounded-full bg-muted"
>
<div
className="h-full bg-foreground transition-[width]"
style={{ width: `${share * 100}%` }}
/>
</div>
<Toolbar>
<ToolButton
icon={FolderOpenIcon}
label={t("large.load", { count: COUNT.toLocaleString() })}
primary
disabled={!engine || loading}
onClick={() => void load()}
/>
<ToolButton
icon={CircleStopIcon}
label={t("large.cancel")}
disabled={!loading}
onClick={() => controller.current?.abort()}
/>
</Toolbar>
<Status tone={loading ? "busy" : message ? "ok" : "idle"}>
{progress
? t("large.stage", {
stage: t(`large.stages.${progress.stage}`),
done: progress.done,
total: progress.total,
})
: message || t("large.hint")}
</Status>
</DemoFrame>
);
}显示加载进度并取消
const controller = new AbortController();
cancelButton.onclick = () => controller.abort();
await engine.load("floor-plan", {
signal: controller.signal,
onProgress: ({ stage, done, total }) => {
progressBar.value = total > 0 ? done / total : 0;
progressLabel.textContent = stage;
},
});stage依次是prepare(校验和迁移)、images(检查图片)、objects(创建对象)和done。- 对象每次创建 100 个,每批之间把控制权交还给页面,所以点击和滚动不会卡住。这也是常见的 canvas性能优化 思路。
- 取消
AbortSignal会以LOAD_ABORTED拒绝,画布继续显示原来的内容。取消加载和加载失败都一样,因为所有对象都创建完成后才会清空画布。 load:progress事件也带有同样的值,方便调用方以外的代码使用。
signal 和 onProgress 可以用于 load、loadDocument、importFabricJson 和 restoreRecovery。
图片加载失败的原因
有图片缺失时,加载会以 MISSING_ASSETS 停止,现在每一项都会说明原因:
try {
await engine.load("floor-plan");
} catch (error) {
if (isDocumentEngineError(error) && error.code === "MISSING_ASSETS") {
for (const { url, failure } of error.missingAssets) {
console.warn(url, failure?.reason, failure?.status);
}
}
}reason | 含义 |
|---|---|
NOT_FOUND | 服务器返回 404 或 410 |
HTTP_ERROR | 服务器返回了其他错误状态 |
CORS | 其他网站不允许这个页面读取图片 |
NETWORK | 地址无法访问 |
TIMEOUT | 图片加载超过了 assets.imageTimeout |
DECODE | 文件已经下载,但不是浏览器能读取的图片 |
ABORTED | 加载被取消 |
浏览器会有意隐藏一些细节。来自其他网站、没有 CORS 头的图片失败时,如果图片设置了 crossOrigin,原因是 CORS,否则是 NETWORK。
调整图片加载
createDocumentEngine({
canvas,
assets: {
imageTimeout: 15_000, // default 30 seconds
maxConcurrentImages: 4, // default 6
},
});网速慢、大图片很多时,同时加载的图片少一些反而更快。更多 fabricjs 性能优化 的数据见兼容性,图片缺失的处理见图片和字体。如果要渲染很多份文档,而不是打开一份大文档,请看批量渲染。