Defer live preview widget mount until viewport

This commit is contained in:
flash555588 2026-06-27 04:08:29 +08:00
parent 6b630d426c
commit 2248b73f36
4 changed files with 31 additions and 4 deletions

View file

@ -13,7 +13,7 @@
- Performance: register reading-mode `3d`/`3dgrid` code blocks through lightweight lazy handlers so heavy inline preview modules load only when a rendered block needs them.
- Performance: serialize Live Preview and reading-mode model loads through an inline preview queue and defer `3dgrid` model preparation until the grid enters the viewport, reducing startup and multi-model I/O spikes.
- Performance: defer Live Preview runtime imports for preview backends, annotation managers, conversion preparation, and load feedback until a visible embed actually starts loading.
- Performance: register Live Preview embeds through a lightweight lazy widget so workspace startup no longer imports the full embed runtime before a model embed mounts.
- Performance: register Live Preview embeds through a lightweight lazy widget so workspace startup no longer imports the full embed runtime before a model embed approaches the viewport.
- Performance: lazy-load the full settings tab UI and model import modal so normal workspace startup only registers lightweight entry points.
- Performance: combine model profile normalization and compact-state change detection into one pass, reducing startup work for large `data.json` registered-part lists.
- Performance: reuse already-normalized registered part arrays while loading model profiles, avoiding repeated object allocation for compact `data.json` state.

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
/**
* Lightweight CM6 extension for Live Preview embeds.
* The full model widget is imported only when an embed is mounted into the DOM.
* The full model widget is imported only when an embed approaches the viewport.
*/
import type { App } from "obsidian";
@ -25,6 +25,7 @@ function loadLivePreviewModule(): Promise<LivePreviewModule> {
class LazyModelEmbedWidget extends WidgetType {
private mountedWidget: LivePreviewWidget | null = null;
private mountedDom: HTMLElement | null = null;
private viewportObs: IntersectionObserver | null = null;
private destroyed = false;
constructor(
@ -75,13 +76,15 @@ class LazyModelEmbedWidget extends WidgetType {
const placeholder = activeDocument.createElement("div");
placeholder.className = "ai3d-embed-preview ai3d-cm-widget ai3d-embed-preview-lazy";
placeholder.setAttribute("contenteditable", "false");
placeholder.style.setProperty("--ai3d-embed-height", `${this.height}px`);
void this.mount(placeholder);
this.watchViewport(placeholder);
return placeholder;
}
override destroy(): void {
this.destroyed = true;
this.stopViewportWatch();
this.mountedWidget?.destroy();
this.mountedWidget = null;
this.mountedDom?.remove();
@ -92,6 +95,26 @@ class LazyModelEmbedWidget extends WidgetType {
return true;
}
private watchViewport(placeholder: HTMLElement): void {
if (typeof IntersectionObserver === "undefined") {
void this.mount(placeholder);
return;
}
this.viewportObs = new IntersectionObserver((entries) => {
if (this.destroyed || this.mountedWidget) return;
if (!entries.some((entry) => entry.isIntersecting || entry.intersectionRatio > 0)) return;
this.stopViewportWatch();
void this.mount(placeholder);
}, { rootMargin: "240px" });
this.viewportObs.observe(placeholder);
}
private stopViewportWatch(): void {
this.viewportObs?.disconnect();
this.viewportObs = null;
}
private async mount(placeholder: HTMLElement): Promise<void> {
let module: LivePreviewModule;
try {

View file

@ -970,6 +970,10 @@ body {
border-radius: var(--radius-m);
}
.ai3d-embed-preview-lazy {
min-height: var(--ai3d-embed-height, 300px);
}
.ai3d-embed-canvas {
display: block;
width: 100%;