Problem
Two distinct performance problems make typing feel laggy with occasional 1-second freezes:
1-second lockups: Memory.forciblyPurgeJavaScriptMemory via CDP calls V8::LowMemoryNotification() synchronously in the renderer, blocking the JS event loop for 100 ms–1 s+ depending on heap size. This fires:
- From the PSI monitor every 5 s when
avg10 > 5 % — no cooldown, so it can fire repeatedly during a long typing session
- From
changeEvent(WindowDeactivate) on every focus shift (notifications, alt-tab, clicking into a dialog) on KDE/Wayland
General jankiness: autoResize (ui.js) reads getComputedStyle, textarea.offsetWidth, and clone.scrollHeight on every keystroke — 2 forced layout reflows per character. At 8 chars/sec this is 16 forced layout reflows/sec in QtWebEngine, which is slower than a desktop browser.
Fix
static/js/ui.js — autoResize: Replace clone-based synchronous measurement with requestAnimationFrame-coalesced height: 'auto' + scrollHeight. N keystrokes per 16 ms frame → 1 layout reflow (vs. 2 per keystroke before). Drop clone entirely.
qt_wrapper.py — async GC overhaul:
- Remove
_cdp_purge_memory (the synchronous stop-the-world variant)
- Replace all three trigger sites with
gc({ type: 'major', execution: 'async' }) via page.runJavaScript() — already enabled by --expose-gc; non-blocking, incremental
- Add 500 ms debounce on focus-loss (skips transient focus shifts)
- Add 30 s cooldown on PSI purge (prevents repeated GC bursts under sustained pressure)
- Node-threshold trigger calls
runJavaScript directly (already on Qt main thread)
- PSI monitor (background thread) uses a 250 ms drain timer on the main thread — correct cross-thread dispatch without needing
QTimer.singleShot from a non-Qt thread
Files
static/js/ui.js — autoResize rewrite
qt_wrapper.py — remove _cdp_purge_memory, add _request_async_gc, update PSI/focus-loss/node-threshold
Tests
- New
tests/test_ui_auto_resize_js.py (5 tests)
- Updated
tests/test_qt_cdp_listener_audit.py (−7 old, +9 new = 43 total)
Problem
Two distinct performance problems make typing feel laggy with occasional 1-second freezes:
1-second lockups:
Memory.forciblyPurgeJavaScriptMemoryvia CDP callsV8::LowMemoryNotification()synchronously in the renderer, blocking the JS event loop for 100 ms–1 s+ depending on heap size. This fires:avg10 > 5 %— no cooldown, so it can fire repeatedly during a long typing sessionchangeEvent(WindowDeactivate)on every focus shift (notifications, alt-tab, clicking into a dialog) on KDE/WaylandGeneral jankiness:
autoResize(ui.js) readsgetComputedStyle,textarea.offsetWidth, andclone.scrollHeighton every keystroke — 2 forced layout reflows per character. At 8 chars/sec this is 16 forced layout reflows/sec in QtWebEngine, which is slower than a desktop browser.Fix
static/js/ui.js— autoResize: Replace clone-based synchronous measurement withrequestAnimationFrame-coalescedheight: 'auto'+scrollHeight. N keystrokes per 16 ms frame → 1 layout reflow (vs. 2 per keystroke before). Drop clone entirely.qt_wrapper.py— async GC overhaul:_cdp_purge_memory(the synchronous stop-the-world variant)gc({ type: 'major', execution: 'async' })viapage.runJavaScript()— already enabled by--expose-gc; non-blocking, incrementalrunJavaScriptdirectly (already on Qt main thread)QTimer.singleShotfrom a non-Qt threadFiles
static/js/ui.js—autoResizerewriteqt_wrapper.py— remove_cdp_purge_memory, add_request_async_gc, update PSI/focus-loss/node-thresholdTests
tests/test_ui_auto_resize_js.py(5 tests)tests/test_qt_cdp_listener_audit.py(−7 old, +9 new = 43 total)