From f86ab0f41b0ff33923ed7805fa7742e00a788cdc Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 7 Feb 2026 07:35:21 +0100 Subject: [PATCH] more performance enhancement --- public/healthz.json | 4 +- .../content/MarkdownRenderer.svelte | 63 +++++++++++-- src/lib/components/layout/Header.svelte | 1 + src/lib/modules/feed/FeedPage.svelte | 69 +++++++++++++-- src/lib/services/cache/event-cache.ts | 11 +-- src/lib/services/cache/indexeddb-store.ts | 18 +++- src/lib/services/cache/markdown-cache.ts | 88 +++++++++++++++++++ vite.config.ts | 40 ++++++++- 8 files changed, 268 insertions(+), 26 deletions(-) create mode 100644 src/lib/services/cache/markdown-cache.ts diff --git a/public/healthz.json b/public/healthz.json index 9f7326d..f428507 100644 --- a/public/healthz.json +++ b/public/healthz.json @@ -2,7 +2,7 @@ "status": "ok", "service": "aitherboard", "version": "0.2.0", - "buildTime": "2026-02-07T06:20:23.861Z", + "buildTime": "2026-02-07T06:35:08.691Z", "gitCommit": "unknown", - "timestamp": 1770445223861 + "timestamp": 1770446108691 } \ No newline at end of file diff --git a/src/lib/components/content/MarkdownRenderer.svelte b/src/lib/components/content/MarkdownRenderer.svelte index a33a07f..04fcafe 100644 --- a/src/lib/components/content/MarkdownRenderer.svelte +++ b/src/lib/components/content/MarkdownRenderer.svelte @@ -13,6 +13,7 @@ import hljs from 'highlight.js'; // Use VS Code theme for IDE-like appearance import 'highlight.js/styles/vs2015.css'; + import { getCachedMarkdown, cacheMarkdown } from '../../services/cache/markdown-cache.js'; import EmbeddedEvent from './EmbeddedEvent.svelte'; let mountingEmbeddedEvents = $state(false); // Guard for mounting @@ -582,16 +583,34 @@ } // Render markdown or AsciiDoc to HTML - function renderMarkdown(text: string): string { + async function renderMarkdown(text: string): Promise { if (!content) return ''; - // Check cache first - const cached = markdownCache.get(content); + // Ensure content is defined (TypeScript narrowing) + const contentToRender: string = content; + + // Check IndexedDB cache first (persistent) + const cachedFromDB = await getCachedMarkdown(contentToRender); + if (cachedFromDB) { + // Also update in-memory cache for faster subsequent access + if (markdownCache.size >= MAX_CACHE_SIZE) { + // Remove oldest entry (simple FIFO) + const firstKey = markdownCache.keys().next().value; + if (firstKey !== undefined) { + markdownCache.delete(firstKey); + } + } + markdownCache.set(contentToRender, cachedFromDB); + return cachedFromDB; + } + + // Check in-memory cache (faster for same session) + const cached = markdownCache.get(contentToRender); if (cached !== undefined) { return cached; } - const processed = processContent(content); + const processed = processContent(contentToRender); let html: string; @@ -752,12 +771,44 @@ markdownCache.delete(firstKey); } } - markdownCache.set(content, sanitized); + markdownCache.set(contentToRender, sanitized); + + // Cache in IndexedDB asynchronously (don't await to avoid blocking) + cacheMarkdown(contentToRender, sanitized).catch(err => { + console.debug('Failed to cache markdown in IndexedDB:', err); + }); return sanitized; } - const renderedHtml = $derived(renderMarkdown(content)); + // Rendered HTML state (async rendering with cache) + let renderedHtml = $state(''); + + // Render markdown when content changes (with async cache support) + $effect(() => { + if (!content) { + renderedHtml = ''; + return; + } + + // Start with in-memory cache for instant display + const cached = markdownCache.get(content); + if (cached) { + renderedHtml = cached; + } + + // Then check IndexedDB and re-render if needed (only if content is defined) + if (content) { + renderMarkdown(content).then(html => { + if (html !== renderedHtml) { + renderedHtml = html; + } + }).catch(err => { + console.error('Error rendering markdown:', err); + renderedHtml = content || ''; // Fallback to raw content + }); + } + }); // Mount ProfileBadge components after rendering function mountProfileBadges() { diff --git a/src/lib/components/layout/Header.svelte b/src/lib/components/layout/Header.svelte index 7412a87..5cb53d7 100644 --- a/src/lib/components/layout/Header.svelte +++ b/src/lib/components/layout/Header.svelte @@ -41,6 +41,7 @@ class="w-full h-full object-cover object-center opacity-90 dark:opacity-70" style="object-position: center;" loading="eager" + fetchpriority="high" />
diff --git a/src/lib/modules/feed/FeedPage.svelte b/src/lib/modules/feed/FeedPage.svelte index 3a4e78b..7827100 100644 --- a/src/lib/modules/feed/FeedPage.svelte +++ b/src/lib/modules/feed/FeedPage.svelte @@ -43,6 +43,28 @@ // Preloaded referenced events (e, a, q tags) - eventId -> referenced event let preloadedReferencedEvents = $state>(new Map()); + // Virtual scrolling for performance + let Virtualizer: any = $state(null); + let virtualizerLoading = $state(false); + let virtualizerContainer = $state(null); + + async function loadVirtualizer() { + if (Virtualizer) return Virtualizer; + if (virtualizerLoading) return null; + + virtualizerLoading = true; + try { + const module = await import('@tanstack/svelte-virtual'); + Virtualizer = module.Virtualizer; + return Virtualizer; + } catch (error) { + console.error('Failed to load virtual scrolling:', error); + return null; + } finally { + virtualizerLoading = false; + } + } + // Filtered events based on filterResult let filteredEvents = $derived.by(() => { if (!filterResult.value) { @@ -454,6 +476,9 @@ isMounted = true; loadInProgress = false; + // Load virtualizer for better performance with large feeds + loadVirtualizer(); + // Use a small delay to ensure previous page cleanup completes const initTimeout = setTimeout(() => { if (!isMounted) return; @@ -472,7 +497,8 @@ // Register j/k navigation shortcuts if (browser) { const unregisterJ = keyboardShortcuts.register('j', (e) => { - if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA' || document.activeElement?.isContentEditable) { + const activeElement = document.activeElement as HTMLElement | null; + if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA' || activeElement?.isContentEditable) { return; // Don't interfere with typing } e.preventDefault(); @@ -481,7 +507,8 @@ }); const unregisterK = keyboardShortcuts.register('k', (e) => { - if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA' || document.activeElement?.isContentEditable) { + const activeElement = document.activeElement as HTMLElement | null; + if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA' || activeElement?.isContentEditable) { return; // Don't interfere with typing } e.preventDefault(); @@ -555,12 +582,34 @@

No posts found.

{:else} -
- {#each events as event (event.id)} - {@const referencedEvent = getReferencedEventForPost(event)} - - {/each} -
+ {#if Virtualizer && events.length > 50} + + {@const V = Virtualizer} +
+ virtualizerContainer} + estimateSize={() => 300} + overscan={5} + > + {#each Array(events.length) as _, i} + {@const event = events[i]} + {@const referencedEvent = getReferencedEventForPost(event)} +
+ +
+ {/each} +
+
+ {:else} + +
+ {#each events as event (event.id)} + {@const referencedEvent = getReferencedEventForPost(event)} + + {/each} +
+ {/if}