perf: faster startup and long pages
- open the window while the notes folder loads; rebuild the search index after the page list is sent - load mermaid and katex on demand - vault.list reads files in parallel batches - resolveLink uses a per-page-list lookup table - very long pages (>300 blocks) skip layout of off-screen blocks - perf-large: add a long-page case, fix the tasks poll Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fcad23afa9
commit
d5d25eaf46
10 files changed
+147
-64
No files matched your search
@@ -312,6 +312,10 @@ kbd{font:500 10.5px/1 var(--f-mono);padding:3px 5px;border-radius:4px;background
|
||||
/* blocks */
|
||||
.blocks{position:relative;padding-top:4px}
|
||||
.block{position:relative;display:flex;align-items:flex-start;gap:6px;margin:1px 0;border-radius:6px}
|
||||
/* very long pages (editor.js marks them): blocks off screen are not laid out or painted. The clip margin keeps the
|
||||
gutter, which sits outside its block, visible. */
|
||||
.long-page>.block{content-visibility:auto;contain-intrinsic-size:auto 32px;overflow-clip-margin:64px}
|
||||
.long-page>.block.menu-open{content-visibility:visible}
|
||||
.block.stagger{animation:rise .5s var(--ease) both;animation-delay:calc(var(--i,0)*26ms + 90ms)}
|
||||
.block.enter-b{animation:blockIn .3s var(--ease)}
|
||||
.block.morph{animation:morph .36s var(--spring)}
|
||||
@@ -1692,3 +1696,4 @@ tr:hover .db-row-x,.db-lrow:hover .db-row-x,.db-row-x:focus-visible{transform:no
|
||||
.keys.rebind .kd-btn.capturing{box-shadow:inset 0 0 0 1px var(--blush-line);background:var(--blush-soft)}
|
||||
.keys.rebind .kd-btn em{font-style:normal;color:var(--ink-2);font-size:12.5px}
|
||||
.tree:focus-visible{outline-offset:-2px}
|
||||
.math-pending{opacity:.6}
|
||||
@@ -7,7 +7,7 @@
|
||||
import { staticHtml } from '$lib/embed.js';
|
||||
import { api } from '$lib/api.js';
|
||||
import { unesc } from '$lib/text.js';
|
||||
import katex from 'katex';
|
||||
import { renderTex } from '$lib/katex.js';
|
||||
import { parseBoard, boardToMd, card, column } from '$lib/kanban.js';
|
||||
import { optionColor } from '$lib/database.js';
|
||||
import { TAG_COLORS } from '$lib/tagColors.js';
|
||||
@@ -187,7 +187,7 @@
|
||||
// images, tables, callouts, math, bold / italic / highlight / links … exactly as on a page.
|
||||
const withMath = (html) =>
|
||||
html.replace(/<code class="language-math">([\s\S]*?)<\/code>/g, (m, c) => {
|
||||
try { return katex.renderToString(unesc(c), { throwOnError: false }); } catch { return m; }
|
||||
try { return renderTex(unesc(c)); } catch { return m; }
|
||||
});
|
||||
const cardHtml = (t) => withMath(staticHtml(parseMarkdown(t), { imageUrl: (src) => api.mediaUrl(pageId || store.currentId, src) }));
|
||||
// a picture that can't be loaded shows its "not found" note, as in the editor
|
||||
|
||||
+10
-4
@@ -11,9 +11,10 @@ import { paintFootnotes } from './footnotes.js';
|
||||
import { scrollToBlock } from './scroll.js';
|
||||
import { staticHtml } from './embed.js';
|
||||
import { linkLabel, IMAGE_FILE, OTHER_FILE, headingOf, refPage, linkHeading, matchesHeading } from './links.js';
|
||||
import katex from 'katex';
|
||||
import mermaid from 'mermaid';
|
||||
mermaid.initialize({ startOnLoad: false, theme: 'dark' });
|
||||
import { loadKatex } from './katex.js';
|
||||
// Mermaid is several MB of script: load it the first time a diagram is drawn, not at startup.
|
||||
let mermaidLib = null;
|
||||
const loadMermaid = () => (mermaidLib ||= import('mermaid').then((m) => m.default));
|
||||
|
||||
|
||||
|
||||
@@ -197,6 +198,7 @@ export function createEditor(host, { blocks: initial, animate = true, onChange =
|
||||
`<button class="tbl-add tbl-add-col" tabindex="-1" aria-label="Add column" title="Add column">${ic('plus')}</button>` +
|
||||
`<button class="tbl-add tbl-add-row" tabindex="-1" aria-label="Add row" title="Add row">${ic('plus')}</button></div>`;
|
||||
}
|
||||
const LONG_PAGE = 300; // blocks; above this the page skips drawing what is off screen (.long-page in app.css)
|
||||
function make(b) {
|
||||
const w = document.createElement('div');
|
||||
w.className = 'block b-' + b.type + (b.type === 'todo' && b.checked ? ' done' : '');
|
||||
@@ -224,9 +226,10 @@ export function createEditor(host, { blocks: initial, animate = true, onChange =
|
||||
const run = () => (b.type === 'math' ? renderMath(box, b.text) : renderMermaid(box, b.text));
|
||||
delay ? previewTimers.set(box, setTimeout(run, delay)) : run();
|
||||
}
|
||||
function renderMath(box, text) {
|
||||
async function renderMath(box, text) {
|
||||
if (!String(text || '').trim()) { box.innerHTML = '<span class="preview-note">The formula appears here as you type it below.</span>'; return; }
|
||||
try {
|
||||
const katex = await loadKatex();
|
||||
box.innerHTML = katex.renderToString(text, { throwOnError: false, displayMode: true, errorColor: cssVar('--danger') || '#ea9188' });
|
||||
} catch (e) {
|
||||
box.innerHTML = `<div class="preview-error">${esc(e.message)}</div>`;
|
||||
@@ -238,6 +241,7 @@ export function createEditor(host, { blocks: initial, animate = true, onChange =
|
||||
if (!String(text || '').trim()) { box.innerHTML = '<span class="preview-note">The diagram appears here as you type it below.</span>'; return; }
|
||||
const id = 'mermaid-' + mine;
|
||||
try {
|
||||
const mermaid = await loadMermaid();
|
||||
// Follow the app's theme each time (it can change between renders).
|
||||
mermaid.initialize({ startOnLoad: false, theme: document.documentElement.dataset.theme === 'light' ? 'default' : 'dark' });
|
||||
await mermaid.parse(text);
|
||||
@@ -318,6 +322,7 @@ export function createEditor(host, { blocks: initial, animate = true, onChange =
|
||||
function paintLinks(root) {
|
||||
for (const a of root.querySelectorAll('.wl')) a.classList.toggle('missing', !links.resolve(a.dataset.ref));
|
||||
}
|
||||
host.classList.toggle('long-page', blocks.length > LONG_PAGE);
|
||||
blocks.forEach((b, i) => {
|
||||
const el = make(b);
|
||||
if (animate && !reduce) { el.style.setProperty('--i', Math.min(i, 16)); el.classList.add('stagger'); }
|
||||
@@ -505,6 +510,7 @@ export function createEditor(host, { blocks: initial, animate = true, onChange =
|
||||
last = { kind: null, id: null, at: 0 };
|
||||
host.querySelectorAll('.block, .cols').forEach((x) => x.remove());
|
||||
for (const b of blocks) host.append(make(b));
|
||||
host.classList.toggle('long-page', blocks.length > LONG_PAGE);
|
||||
layout();
|
||||
const el = s.caret && elOf(s.caret.id);
|
||||
const c = el && el.querySelector(s.caret.cell ? `.tcell[data-r="${s.caret.cell[0]}"][data-c="${s.caret.cell[1]}"]` : s.caret.c2 ? '.c2' : '.c');
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
// headingsOf(blocks) the headings of a page, for pickers and the outline
|
||||
// sectionOf(blocks, text) a heading plus everything under it, up to the next heading of the same or a higher level
|
||||
// staticHtml(blocks, opts) read-only HTML for a list of blocks, styled by the same CSS as the editor's blocks
|
||||
import katex from 'katex';
|
||||
import { renderTex } from './katex.js';
|
||||
import { ic } from './icons.js';
|
||||
import { esc, htmlText } from './text.js';
|
||||
import { highlight } from './highlight.js';
|
||||
@@ -75,7 +75,7 @@ function one(b, imageUrl) {
|
||||
case 'mermaid': return row(`<div class="code"><div class="code-bar"><span class="lang">mermaid</span></div><pre class="c code-c">${highlight(b.text || '')}</pre></div>`);
|
||||
case 'math': {
|
||||
let out;
|
||||
try { out = katex.renderToString(b.text || '', { throwOnError: false, displayMode: true }); } catch { out = esc(b.text || ''); }
|
||||
try { out = renderTex(b.text || '', true); } catch { out = esc(b.text || ''); }
|
||||
return row(`<div class="math"><div class="math-preview">${out}</div></div>`);
|
||||
}
|
||||
case 'divider': return row('<div class="hr"></div>');
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// KaTeX is loaded the first time a formula is drawn, not at startup.
|
||||
// renderTex() answers at once: the finished HTML when KaTeX is already loaded, else a placeholder that is
|
||||
// replaced as soon as it arrives (so callers that build HTML strings need no async plumbing).
|
||||
import { esc } from './text.js';
|
||||
|
||||
let lib = null;
|
||||
let loading = null;
|
||||
|
||||
export const loadKatex = () => (loading ||= import('katex').then((m) => (lib = m.default)));
|
||||
|
||||
function typeset() {
|
||||
if (typeof document === 'undefined' || !lib) return;
|
||||
for (const el of document.querySelectorAll('.math-pending')) {
|
||||
const display = el.dataset.display === '1';
|
||||
try { el.outerHTML = lib.renderToString(el.dataset.tex || '', { throwOnError: false, displayMode: display }); } catch { el.classList.remove('math-pending'); }
|
||||
}
|
||||
}
|
||||
|
||||
export function renderTex(tex, displayMode = false) {
|
||||
if (lib) return lib.renderToString(tex, { throwOnError: false, displayMode });
|
||||
if (typeof document !== 'undefined') loadKatex().then(() => setTimeout(typeset, 0), () => {});
|
||||
return `<span class="math-pending" data-display="${displayMode ? 1 : 0}" data-tex="${esc(tex)}">${esc(tex)}</span>`;
|
||||
}
|
||||
+24
-5
@@ -39,14 +39,33 @@ export const linkLabel = (ref) => {
|
||||
return name && h ? `${name} › ${h}` : name || h || String(ref || '');
|
||||
};
|
||||
|
||||
// Lookup tables for one list of pages (the app replaces the list when pages change, so a list's table never
|
||||
// goes stale): lower-cased full id -> id, and lower-cased file name -> its shortest id.
|
||||
const indexes = new WeakMap();
|
||||
function indexOf(pages) {
|
||||
let ix = indexes.get(pages);
|
||||
if (ix && ix.n === pages.length) return ix;
|
||||
const exact = new Map();
|
||||
const byName = new Map();
|
||||
for (const p of pages) {
|
||||
const id = p.id;
|
||||
const low = id.toLowerCase();
|
||||
if (!exact.has(low)) exact.set(low, id);
|
||||
const name = low.slice(low.lastIndexOf('/') + 1);
|
||||
const cur = byName.get(name);
|
||||
if (cur === undefined || id.length < cur.length) byName.set(name, id);
|
||||
}
|
||||
ix = { n: pages.length, exact, byName };
|
||||
indexes.set(pages, ix);
|
||||
return ix;
|
||||
}
|
||||
|
||||
export function resolveLink(ref, pages) {
|
||||
const want = refPage(ref).toLowerCase();
|
||||
if (!want) return null;
|
||||
const exact = pages.find((p) => p.id.toLowerCase() === want);
|
||||
if (exact) return exact.id;
|
||||
// by name: a note's title, or a canvas's file name ("Map.canvas"), so [[Map]] never means a canvas
|
||||
const byTitle = pages.filter((p) => p.id.slice(p.id.lastIndexOf('/') + 1).toLowerCase() === want).sort((a, b) => a.id.length - b.id.length);
|
||||
return byTitle.length ? byTitle[0].id : null;
|
||||
const { exact, byName } = indexOf(pages);
|
||||
// exact id first; else by name: a note's title, or a canvas's file name ("Map.canvas"), so [[Map]] never means a canvas
|
||||
return exact.get(want) ?? byName.get(want) ?? null;
|
||||
}
|
||||
|
||||
// The shortest text that links to `id`: its title when that's unique, else the full path.
|
||||
|
||||
+6
-2
@@ -2,8 +2,12 @@
|
||||
// Used by the outline and by [[Page#Heading]] links. Returns how long (ms) smooth scrolling will keep running.
|
||||
export function scrollToBlock(scroller, el) {
|
||||
const reduce = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
const top = el.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop - 24;
|
||||
scroller.scrollTo({ top: Math.max(0, top), behavior: reduce ? 'auto' : 'smooth' });
|
||||
const where = () => Math.max(0, el.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop - 24);
|
||||
if (el.closest('.long-page')) {
|
||||
// Blocks off screen only have an estimated height until they are drawn, so jump, then correct once they are.
|
||||
scroller.scrollTo({ top: where(), behavior: 'auto' });
|
||||
for (let i = 0; i < 2; i++) requestAnimationFrame(() => scroller.scrollTo({ top: where(), behavior: 'auto' }));
|
||||
} else scroller.scrollTo({ top: where(), behavior: reduce ? 'auto' : 'smooth' });
|
||||
el.classList.remove('flash');
|
||||
void el.offsetWidth;
|
||||
el.classList.add('flash');
|
||||
|
||||
Reference in new issue
Block a user