Skip to content

perf(dialog): add render window to prevent long-conversation DOM freeze#3

Open
lingion wants to merge 15 commits into
masterfrom
fix/long-conversation-freeze
Open

perf(dialog): add render window to prevent long-conversation DOM freeze#3
lingion wants to merge 15 commits into
masterfrom
fix/long-conversation-freeze

Conversation

@lingion

@lingion lingion commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Problem

Long conversations (500+ messages) freeze the entire app because all messages are rendered into the DOM simultaneously with full KaTeX markdown rendering.

Root causes addressed

  1. All messages rendered to DOM (no cap) — v-for over entire chain → 500+ message-item components
  2. Each message runs full KaTeX — md-editor-v3 blocks the main thread per-message
  3. DOM post-processing on every message — onHtmlChanged triggers 3 DOM-walk operations per message

Solution

1. Render window (use-message-window.ts)

  • Initial render: last 100 messages (INITIAL_LIMIT)
  • Hard cap: 200 messages max in DOM (MAX_RENDER_CAP)
  • Infinite scroll-up via IntersectionObserver sentinel at top
  • Scroll position preserved during loadMore() via scrollHeight delta compensation
  • 500ms settle delay + 600ms cooldown prevent observer spam

2. Lazy markdown (lazyPlainText prop)

  • Messages >30 positions from renderEnd render as plain text
  • Skips md-editor-v3 / KaTeX entirely for off-screen content
  • Reuses existing streaming plain-text pipeline

3. Stable keys

  • Visible slice uses message IDs as Vue keys → no remounting of existing nodes

Files changed

File Change
src/composables/use-message-window.ts New — 125 lines
src/views/DialogView.vue Template + script integration
src/components/MessageItem.vue +5 lines lazyPlainText prop

Build

npx quasar build → Build succeeded

Kerry1020 added 15 commits July 18, 2026 16:13
Long conversations (500+ messages) would freeze because all
messages were rendered into the DOM with full KaTeX markdown.

Three optimizations applied:

1. Render window (useMessageWindow composable):
   - Initial render: last 100 messages, max cap: 200
   - Infinite scroll-up via IntersectionObserver sentinel
   - Scroll position preserved during loadMore() via height delta

2. Lazy markdown for off-screen messages:
   - Messages >30 positions from renderEnd render as plain text
   - Skips md-editor-v3 KaTeX rendering (costly per-message)
   - Reuses existing streaming plain-text pipeline

3. Stable keys: visible slice uses message IDs as keys so Vue
   doesn't remount existing items when the window shifts
…d gate

- Update version in package.json and build.gradle (2.0.8.9 → 2.0.8.12)
- Add v-if guards in DialogView template: msgRoute/chain access
  now checked for undefined before rendering message-item
- Add scrollTop > 60 guard in loadMore() to prevent false
  triggers when soft keyboard appears and shrinks viewport
pageFhStyle sets overflow-y:auto inline on q-page. When the Android
soft keyboard appears, the viewport shrinks and the q-page height
decreases. The browser then scrolls the PAGE to keep the focused
input visible, which drags the absolutely-positioned composer dock
with it — causing it to appear to fly to the top.

Fix: !important on .dialog-page overflow:hidden to beat the inline
style. All scrolling is handled by the inner scrollContainer.
Root cause: WebView resize triggers pageFhStyle recalculation BEFORE
Capacitor keyboardWillShow sets --sab. Two-phase jump: page height
shrinks -> composer snaps, then --sab adds padding. User sees dock fly.

Fix: CSS transition on padding-bottom so --sab-driven padding change
animates smoothly. Revert !important overflow experiment.
Changed .dialog-composer-area from position:absolute (anchored to
q-page which gets wrong height from pageFhStyle during keyboard
resize) to position:fixed (anchored to viewport). This prevents
the dock from flying to the top when soft keyboard opens.
With position:fixed, the viewport already shrinks above the keyboard.
The --sab keyboard-height padding was designed for position:absolute
(where the page keeps full height and the composer sits behind the
keyboard). Adding --sab padding on a fixed composer pushed content
far above the input area — this was the actual "height error."

Now padding only uses env(safe-area-inset-bottom) for nav bar.
When soft keyboard opens, the browser attempts to scroll the page
to keep the focused input visible. Since pageFhStyle sets overflow-y:auto
on q-page, the entire page scrolls — dragging the absolutely-positioned
composer dock with it, making it appear to fly to the top.

Fix: overflow:hidden!important on .dialog-page to beat the inline
overflow-y:auto from pageFhStyle. All scrolling is handled by the
inner scrollContainer — the page itself should never scroll.
pageFhStyle calculates page height from Quasar internals which may
produce wrong values during Android soft-keyboard transitions,
causing the absolutely-positioned composer (bottom:0) to fly to
the top. Replace with explicit height:100dvh — dynamic viewport
height correctly accounts for keyboard presence on Chrome 108+.
Use adjustNothing so the WebView keeps its full height, then anchor the
fixed composer with bottom: var(--sab). Convert Capacitor Android
keyboard/status-bar physical-pixel heights to CSS pixels before writing
safe-area variables; raw native values were roughly devicePixelRatio
too large and pushed the dock almost entirely off-screen.

Remove the incorrect 100dvh page override and keep pageFhStyle for the
page layout.
Use keyboardDidShow instead of keyboardWillShow so the dock offset is
applied after the IME reaches its final position. Keep the fixed dock
above the IME using the CSS-pixel-converted keyboard inset and avoid
adding the inset to the dock padding.
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.

2 participants