diff options
| -rw-r--r-- | frontend-vanilla/public/themes/codex.css | 11 | ||||
| -rw-r--r-- | frontend-vanilla/public/themes/sakura.css | 3 | ||||
| -rw-r--r-- | frontend-vanilla/public/themes/terminal.css | 42 | ||||
| -rw-r--r-- | frontend-vanilla/src/main.ts | 26 | ||||
| -rw-r--r-- | frontend-vanilla/src/polling.test.ts | 6 | ||||
| -rw-r--r-- | frontend-vanilla/src/regression.test.ts | 4 | ||||
| -rw-r--r-- | frontend-vanilla/src/style.css | 4 |
7 files changed, 55 insertions, 41 deletions
diff --git a/frontend-vanilla/public/themes/codex.css b/frontend-vanilla/public/themes/codex.css index ece9e2d..50942e6 100644 --- a/frontend-vanilla/public/themes/codex.css +++ b/frontend-vanilla/public/themes/codex.css @@ -49,8 +49,11 @@ body { background-color: var(--bg-color); color: var(--text-color); -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; - font-feature-settings: 'liga' 1, 'kern' 1, 'onum' 1; + /* text-rendering: optimizeLegibility triggers expensive kerning/ligature + computation on all text. On mobile with long feed content this causes + significant layout slowdowns during scroll. The default 'auto' lets + the browser optimize per-element. font-feature-settings similarly + forces the shaper to run on every glyph. Removed for performance. */ } /* ---- Sidebar: Table of Contents ---- */ @@ -236,8 +239,8 @@ body { font-size: 1rem; line-height: 1.75; color: var(--text-color); - hyphens: auto; - -webkit-hyphens: auto; + /* hyphens: auto removed -- requires dictionary lookups during layout for + every line break, expensive with long feed content during scroll. */ } .item-description a { diff --git a/frontend-vanilla/public/themes/sakura.css b/frontend-vanilla/public/themes/sakura.css index f0fc990..48a1c0a 100644 --- a/frontend-vanilla/public/themes/sakura.css +++ b/frontend-vanilla/public/themes/sakura.css @@ -57,7 +57,8 @@ body { background-color: var(--bg-color); color: var(--text-color); -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; + /* text-rendering: optimizeLegibility removed -- causes expensive text + shaping on all content, leading to scroll jank on mobile. */ } /* ---- Sidebar ---- */ diff --git a/frontend-vanilla/public/themes/terminal.css b/frontend-vanilla/public/themes/terminal.css index dd9c1b2..48164c9 100644 --- a/frontend-vanilla/public/themes/terminal.css +++ b/frontend-vanilla/public/themes/terminal.css @@ -52,23 +52,31 @@ body { -webkit-font-smoothing: antialiased; } -/* Subtle scanline overlay -- only in dark mode */ -.theme-dark body::after { - content: ''; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; - z-index: 9999; - background: repeating-linear-gradient( - to bottom, - transparent, - transparent 2px, - rgba(0, 0, 0, 0.03) 2px, - rgba(0, 0, 0, 0.03) 4px - ); +/* Subtle scanline overlay -- only in dark mode, desktop only. + The fixed full-viewport pseudo-element with a repeating gradient + forces GPU compositing on every scroll frame. On mobile this causes + severe jank and memory pressure, so we limit it to large screens + and promote it to its own layer with will-change to avoid repainting + the content beneath it. */ +@media (min-width: 1025px) { + .theme-dark body::after { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 9999; + background: repeating-linear-gradient( + to bottom, + transparent, + transparent 2px, + rgba(0, 0, 0, 0.03) 2px, + rgba(0, 0, 0, 0.03) 4px + ); + will-change: transform; + } } /* ---- Sidebar ---- */ diff --git a/frontend-vanilla/src/main.ts b/frontend-vanilla/src/main.ts index 2be9354..75707ac 100644 --- a/frontend-vanilla/src/main.ts +++ b/frontend-vanilla/src/main.ts @@ -340,18 +340,20 @@ export function renderItems() { function checkReadItems(scrollRoot: HTMLElement) { const containerRect = scrollRoot.getBoundingClientRect(); - store.items.forEach((item) => { - if (item.read) return; - - const el = document.querySelector(`.feed-item[data-id="${item._id}"]`); - if (el) { - const rect = el.getBoundingClientRect(); - // Mark as read if the bottom of the item is above the top of the container - if (rect.bottom < containerRect.top) { - updateItem(item._id, { read: true }); - } + // Batch DOM query: select all feed items at once instead of O(n) individual + // querySelector calls with attribute selectors per scroll tick. + const allItems = scrollRoot.querySelectorAll('.feed-item'); + for (const el of allItems) { + const id = parseInt(el.getAttribute('data-id')!); + const item = store.items.find(i => i._id === id); + if (!item || item.read) continue; + + const rect = el.getBoundingClientRect(); + // Mark as read if the bottom of the item is above the top of the container + if (rect.bottom < containerRect.top) { + updateItem(item._id, { read: true }); } - }); + } } // Polling fallback for infinite scroll (matches V1 behavior) @@ -395,7 +397,7 @@ if (typeof window !== 'undefined') { } } - }, 1000); + }, 3000); } // ... (add this variable at module level or inside renderSettings if possible, but module level is safer for persistence across clicks if renderSettings re-runs? No, event flow is synchronous: click button -> click file input. User selects file. Change event fires. diff --git a/frontend-vanilla/src/polling.test.ts b/frontend-vanilla/src/polling.test.ts index fa4b62f..44ad7da 100644 --- a/frontend-vanilla/src/polling.test.ts +++ b/frontend-vanilla/src/polling.test.ts @@ -44,8 +44,8 @@ describe('Infinite Scroll Polling', () => { json: async () => [] } as Response); - // Wait for interval (1000ms) + buffer - await new Promise(resolve => setTimeout(resolve, 1100)); + // Wait for interval (3000ms) + buffer + await new Promise(resolve => setTimeout(resolve, 3100)); // Check if apiFetch was called expect(apiFetch).toHaveBeenCalledWith(expect.stringContaining('/api/stream')); @@ -58,7 +58,7 @@ describe('Infinite Scroll Polling', () => { Object.defineProperty(scrollRoot, 'clientHeight', { value: 200, configurable: true }); Object.defineProperty(scrollRoot, 'scrollTop', { value: 100, configurable: true }); - await new Promise(resolve => setTimeout(resolve, 1100)); + await new Promise(resolve => setTimeout(resolve, 3100)); expect(apiFetch).not.toHaveBeenCalled(); }); diff --git a/frontend-vanilla/src/regression.test.ts b/frontend-vanilla/src/regression.test.ts index 972b221..7f72ed5 100644 --- a/frontend-vanilla/src/regression.test.ts +++ b/frontend-vanilla/src/regression.test.ts @@ -202,8 +202,8 @@ describe('Scroll-to-Read Regression Tests', () => { // Dispatch scroll on WINDOW, not mainContent window.dispatchEvent(new Event('scroll')); - // Wait for potential debounce/poll - await new Promise(resolve => setTimeout(resolve, 1100)); + // Wait for potential debounce/poll (3000ms interval + buffer) + await new Promise(resolve => setTimeout(resolve, 3100)); // Expect it to handle it expect(apiFetch).toHaveBeenCalledWith(expect.stringContaining('/api/item/12345'), expect.objectContaining({ diff --git a/frontend-vanilla/src/style.css b/frontend-vanilla/src/style.css index 0f7b909..fd51eff 100644 --- a/frontend-vanilla/src/style.css +++ b/frontend-vanilla/src/style.css @@ -163,7 +163,7 @@ html { padding: 0.3rem 0.8rem; margin: 0.1rem 0; border-radius: 8px; - transition: all 0.2s ease; + transition: background-color 0.2s ease, color 0.2s ease, opacity 0.2s ease, transform 0.2s ease; font-weight: 500; font-size: 0.85rem; /* Explicitly smaller sidebar links */ @@ -703,7 +703,7 @@ button, font-family: var(--font-heading); background-color: var(--bg-color); cursor: pointer; - transition: all 0.2s; + transition: background-color 0.2s, color 0.2s, border-color 0.2s; color: var(--text-color); text-decoration: none; display: inline-block; |
