Skip to content

perf(desktop): shiki HTML cache + preload-hydrated stash — kill the highlight flash, instant first clicks - #368

Merged
Kyzcreig merged 3 commits into
mainfrom
perf/shiki-cache-stash-hydrate
Jul 16, 2026
Merged

Kyzcreig merged 3 commits into
mainfrom
perf/shiki-cache-stash-hydrate

Conversation

@Kyzcreig

Copy link
Copy Markdown
Collaborator

Symptoms (user re-report after #363/#366)

  1. Still occasional flashing in the chat pane.
  2. Still slow first clicks after reopening the app.

Diagnosis (CDP: layout-shift observer + Shiki-swap MutationObserver + screencast)

  • Flash: 128 code-fence re-highlights per switch, 5 inside the viewport at t+6.8-13s. The SWR wholesale replace and the idle budget raise both REMOUNT code cards; each card rendered plain-first → swapped to highlighted ~120ms later, in view. That plain→color pop is the flash. CLS entries 0.04-0.12 from the same remounts.
  • Slow first clicks: the perf(desktop): in-memory transcript stash — instant, flash-free session switches #366 in-memory stash only covered sessions visited during the current app run. After a reopen, every click was cold again (async disk-cache read behind a setMessages([]) blank).

Fix

  1. shiki-html-cache.ts — module LRU (400) of highlighted HTML keyed on (lang, code). Cache hit renders colored HTML synchronously in the mount commit; remounts can never show the plain→highlighted swap. Chat path drops the react-shiki component (right-rail preview + diff-lines unchanged). Fail-open on tokenize errors.
  2. Preload hydrates the stash — the boot preloader (pinned + visible 12) now feeds the in-memory stash alongside the disk cache, so first clicks after launch paint synchronously. Stash cap 12→20 (exported constant + behavior-contract test).

Tests

6 new shiki-cache tests (sync remount hit, single tokenize per fence, text fallback, fail-open, LRU) + preload-hydrates-stash test + stash tests rewritten against the exported cap. Full desktop suite 165 files green, tsc clean, eslint clean.

E2E

Post-deploy switch loop + swap-observer re-run on the MBP to follow in comments.

…ighlight flash, instant first clicks

Post-#366 diagnosis on the MBP (layout-shift + Shiki-swap observers over
CDP): the remaining 'occasional flash' was code cards re-highlighting from
scratch on EVERY remount — the SWR wholesale replace and the idle budget
raise both remount cards, and each rendered plain-first then swapped to
highlighted ~120ms later, visibly, in the viewport (128 swaps per switch,
5 in-view; CLS entries 0.04-0.12 from the same remounts).

1. shiki-html-cache.ts: module-level LRU (400 entries) of highlighted HTML
   keyed on (lang, code). A fence highlighted once this app run renders its
   colored HTML SYNCHRONOUSLY in the mount commit — remounts never show the
   plain->color pop. Replaces the react-shiki component in the chat path
   (right-rail preview + diff-lines keep react-shiki). Fail-open: tokenize
   errors leave plain code.

2. transcript-preload.ts also hydrates the in-memory stash (not just the
   disk cache), so the FIRST click on a pinned/visible session after app
   launch paints synchronously — previously the stash only covered sessions
   visited this run, which is why a fresh reopen still felt slow. Stash cap
   20 (holds MAX_PRELOAD=12 + a click working set); cap now an exported
   constant with a behavior-contract test instead of hardcoded 8s.
@Kyzcreig
Kyzcreig enabled auto-merge (squash) July 16, 2026 07:32
@greptile-apps

greptile-apps Bot commented Jul 16, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR eliminates the remaining highlight flash and slow-first-click issues by introducing two complementary caching layers. A module-level Shiki HTML LRU (shiki-html-cache.ts, 400 entries) makes remounts of already-seen code fences render their colored HTML synchronously in the same React commit, and the boot preloader now also hydrates the in-memory transcript stash so the first click after relaunch paints without a loader pass.

  • shiki-html-cache.ts: new Map-based LRU keyed on (lang, code); useCachedShikiHtml initialises React state from the cache synchronously, falls back to plain code on miss, and populates the cache after the async tokenise resolves.
  • transcript-preload.ts: after writing each preloaded transcript to the disk render cache, also calls stashTranscript so the in-memory stash is warm for first-click paints — stash cap raised from 8 to 20.
  • Tests: 6 new shiki-cache tests, 1 preload-hydrates-stash integration test, and stash tests rewritten against the exported MAX_STASH_ENTRIES constant with afterEach cleanup.

Confidence Score: 5/5

Safe to merge — both caching layers are fail-open, LRU implementations are correct, and the async cleanup logic in the React hook handles all cases without state leaks.

The two caching layers are narrow, well-bounded changes with correct LRU implementations, proper async cleanup via the alive guard, and comprehensive test coverage for all documented contracts. No logic path omits its fail-open or cleanup handling.

No files require special attention.

Important Files Changed

Filename Overview
apps/desktop/src/components/chat/shiki-html-cache.ts New module-level LRU (400 entries) for Shiki highlighted HTML. Cache-first hook initialises state synchronously so remounts skip the plain-to-color swap. Fail-open on tokenise errors, alive guard prevents stale async updates.
apps/desktop/src/components/chat/shiki-html-cache.test.tsx New test file covering all documented contracts: miss-to-async fill, synchronous remount hit, unknown-language fallback, independent caching per code string, fail-open, and LRU eviction boundary.
apps/desktop/src/components/chat/shiki-highlighter.tsx Replaces react-shiki with the new CachedHighlight component; SHIKI_THEME/SHIKI_COLOR_REPLACEMENTS moved to shiki-html-cache.ts and re-exported here for back-compat.
apps/desktop/src/app/transcript-preload.ts Adds stashTranscript(session.id, rows) call after every successful disk-cache push, so the in-memory stash is hydrated at boot.
apps/desktop/src/app/transcript-stash.ts MAX_ENTRIES raised from 8 to 20 (exported as MAX_STASH_ENTRIES). LRU logic unchanged and correct.
apps/desktop/src/app/transcript-stash.test.ts Tests rewritten against exported MAX_STASH_ENTRIES with regression guard asserting cap >= MAX_PRELOAD + 4.
apps/desktop/src/app/transcript-preload.test.ts Adds afterEach cleanup and new integration test verifying preload rows are readable synchronously from the stash.

Reviews (2): Last reviewed commit: "Merge branch 'main' into perf/shiki-cach..." | Re-trigger Greptile

Comment thread apps/desktop/src/components/chat/shiki-html-cache.test.tsx
Comment thread apps/desktop/src/app/transcript-stash.test.ts Outdated
Comment thread apps/desktop/src/app/transcript-preload.test.ts
@Kyzcreig
Kyzcreig merged commit 9c4e910 into main Jul 16, 2026
24 checks passed
@Kyzcreig
Kyzcreig deleted the perf/shiki-cache-stash-hydrate branch July 16, 2026 08:03
@Kyzcreig

Copy link
Copy Markdown
Collaborator Author

E2E on the MBP (build 9c4e910c installed + relaunched, CDP-driven)

Flash (in-viewport Shiki plain→highlight swaps + layout shifts):

Switch CLS entries in-view Shiki swaps
pre-fix baseline 0.04–0.12 5
warm-up pass (this build) 0–1 0
measured pass ×4 0 0

First-click latency (blank/loader frames after click):

Switch Blank frames
Cold A (first visit) 0 (preload-hydrated stash)
Cold B 0
Warm back→A/B 0

The plain→color pop is gone (highlighted HTML now renders synchronously from the module cache on every remount), and the preload→stash hydration means even first clicks after launch paint with zero blank frames. Debug port closed, app relaunched clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant