feat(markdown): use a Mermaid cache to avoid flicker while editing something else

This commit is contained in:
Elian Doran
2026-04-17 07:41:30 +03:00
parent 9464e2aff5
commit daf5740610
2 changed files with 51 additions and 6 deletions

View File

@@ -101,13 +101,55 @@ export async function rewriteMermaidDiagramsInContainer(container: HTMLDivElemen
}
}
/**
* Per-container cache of rendered mermaid SVG keyed by diagram source text.
* Populated after each successful render; reused on subsequent renders to
* avoid flicker when the preview HTML is regenerated (e.g. live markdown
* editing). Entries for diagrams no longer present in the container are
* evicted on each run so the cache can't grow unbounded.
*/
const mermaidSvgCache = new WeakMap<HTMLElement, Map<string, string>>();
export async function applyInlineMermaid(container: HTMLDivElement) {
// Initialize mermaid
const nodes = Array.from(container.querySelectorAll<HTMLElement>("div.mermaid-diagram"));
if (!nodes.length) return;
let cache = mermaidSvgCache.get(container);
if (!cache) {
cache = new Map();
mermaidSvgCache.set(container, cache);
}
// Paint cached SVGs upfront so unchanged diagrams don't flicker, and collect
// only the new/changed diagrams for an actual mermaid render pass.
const pendingSources = new Map<HTMLElement, string>();
const seen = new Set<string>();
for (const node of nodes) {
const source = (node.textContent ?? "").trim();
seen.add(source);
const cached = cache.get(source);
if (cached) {
node.innerHTML = cached;
node.setAttribute("data-processed", "true");
} else {
pendingSources.set(node, source);
}
}
// Evict entries whose source is no longer present.
for (const key of [ ...cache.keys() ]) {
if (!seen.has(key)) cache.delete(key);
}
if (!pendingSources.size) return;
const mermaid = (await import("mermaid")).default;
mermaid.initialize(getMermaidConfig());
const nodes = Array.from(container.querySelectorAll<HTMLElement>("div.mermaid-diagram"));
try {
await mermaid.run({ nodes });
await mermaid.run({ nodes: [ ...pendingSources.keys() ] });
for (const [ node, source ] of pendingSources) {
cache.set(source, node.innerHTML);
}
} catch (e) {
console.log(e);
}

View File

@@ -6,7 +6,7 @@ import "@triliumnext/ckeditor5";
import clsx from "clsx";
import { RefObject } from "preact";
import { useEffect, useMemo } from "preact/hooks";
import { useEffect, useLayoutEffect, useMemo } from "preact/hooks";
import appContext from "../../../components/app_context";
import FNote from "../../../entities/fnote";
@@ -76,8 +76,11 @@ export function ReadOnlyTextContent({ html, ntxId, dir, className, contentRef: e
document.body.style.setProperty("--code-block-tab-width", codeBlockTabWidth || "4");
}, [codeBlockTabWidth]);
// Apply necessary transforms.
useEffect(() => {
// Apply necessary transforms. Runs in a layout effect so the synchronous
// DOM mutations (mermaid rewrite + cached-SVG repaint, math, etc.) happen
// before the browser paints — prevents a flash of raw `<pre>` content
// during live preview re-renders.
useLayoutEffect(() => {
const container = contentRef.current;
if (!container) return;