[Bug]: WebChat UI freezes on large sessions — synchronous markdown rendering of 200 messages blocks main thread
Summary
The WebChat Control UI becomes completely unresponsive (browser tab freezes for 5-10+ seconds) on long-lived sessions when any tool call completes. Telegram and other channels continue working normally — confirming this is a client-side rendering issue, not a gateway problem.
The root cause is loadChatHistory() fetching up to 200 messages and running marked.parse() + DOMPurify.sanitize() synchronously on the main thread for every message. On sessions that have grown to 400KB–1.9MB over weeks of use, this creates a main-thread bomb that locks the browser tab.
Steps to reproduce
- Use OpenClaw via WebChat for several weeks (sessions grow to 400KB–1.9MB)
- Ask the agent to perform any tool call (e.g.,
cron.list, file read, etc.)
- When the tool call completes (
state === "final"), the UI calls loadChatHistory()
- Browser tab freezes for 5-10+ seconds (or indefinitely on very large sessions)
- The
node.list polling (every 5s) compounds the freeze by triggering additional re-renders
- Only way to recover is creating a new session
Expected behavior
The WebChat UI should remain responsive regardless of session size. Tool call completions should not trigger a full synchronous re-render of the entire chat history.
Actual behavior
- Browser tab freezes completely (no scrolling, no clicking, no input)
- Telegram channel continues to work fine during the freeze (gateway is healthy)
- DevTools performance trace shows ~7 seconds of synchronous JS execution in the
loadChatHistory() → marked.parse() → DOMPurify.sanitize() → Lit repeat() + unsafeHTML() rendering pipeline
- After the initial freeze,
node.list polling triggers further full re-renders, preventing recovery
Root cause analysis
When a chat run completes (state === "final" at line ~214 in webchat source), the UI calls loadChatHistory() which fetches up to 200 messages from the server. Each message then gets:
- Markdown parsed via
marked.parse() — synchronous, on main thread
- Sanitized via
DOMPurify.sanitize() — synchronous, on main thread
- Rendered into DOM via Lit's
repeat() + unsafeHTML() — synchronous
With 200 messages containing code blocks and long tool outputs, this is a synchronous main-thread bomb.
The MARKDOWN_PARSE_LIMIT is 40K chars per message, and the markdown cache holds 200 entries — but on a fresh history load after tool completion, nothing is cached, so all 200 messages get parsed from scratch.
Note: The existing markdown caching fix (commit d57cb2e — "cache Control UI markdown rendering + memoize chat text extraction to reduce Safari typing jank") does not cover the full history reload path triggered by tool call completion.
Proposed fixes
- Virtualize the message list — only render messages visible in the viewport (biggest win)
- Async/chunked markdown rendering — parse messages in batches via
requestIdleCallback or requestAnimationFrame
- Reduce history fetch limit —
CHAT_HISTORY_RENDER_LIMIT and the server limit: 200 could be lowered to ~50 for immediate relief
- Skip re-render on
node.list — the 5-second node polling should not trigger a full chat re-render if nodes didn't change
- Persist markdown cache across history reloads — so returning to a session doesn't re-parse everything
Environment
- OpenClaw version: 2026.2.3-1
- OS: macOS
- Browser: Chrome (latest)
- Provider: Anthropic (Claude)
- Session sizes: 400KB to 1.9MB after weeks of use
- Interface: WebChat (Control UI)
- Other channels (Telegram): unaffected
Additional context
This is a classic "works fine with small data, dies at scale" issue. When first installed, sessions are empty/tiny and the 200-message synchronous render is fast. After weeks of daily use with tool calls generating large outputs, sessions grow to megabytes, and the rendering pipeline becomes unusable.
Related issues for context:
[Bug]: WebChat UI freezes on large sessions — synchronous markdown rendering of 200 messages blocks main thread
Summary
The WebChat Control UI becomes completely unresponsive (browser tab freezes for 5-10+ seconds) on long-lived sessions when any tool call completes. Telegram and other channels continue working normally — confirming this is a client-side rendering issue, not a gateway problem.
The root cause is
loadChatHistory()fetching up to 200 messages and runningmarked.parse()+DOMPurify.sanitize()synchronously on the main thread for every message. On sessions that have grown to 400KB–1.9MB over weeks of use, this creates a main-thread bomb that locks the browser tab.Steps to reproduce
cron.list, file read, etc.)state === "final"), the UI callsloadChatHistory()node.listpolling (every 5s) compounds the freeze by triggering additional re-rendersExpected behavior
The WebChat UI should remain responsive regardless of session size. Tool call completions should not trigger a full synchronous re-render of the entire chat history.
Actual behavior
loadChatHistory()→marked.parse()→DOMPurify.sanitize()→ Litrepeat()+unsafeHTML()rendering pipelinenode.listpolling triggers further full re-renders, preventing recoveryRoot cause analysis
When a chat run completes (
state === "final"at line ~214 in webchat source), the UI callsloadChatHistory()which fetches up to 200 messages from the server. Each message then gets:marked.parse()— synchronous, on main threadDOMPurify.sanitize()— synchronous, on main threadrepeat()+unsafeHTML()— synchronousWith 200 messages containing code blocks and long tool outputs, this is a synchronous main-thread bomb.
The
MARKDOWN_PARSE_LIMITis 40K chars per message, and the markdown cache holds 200 entries — but on a fresh history load after tool completion, nothing is cached, so all 200 messages get parsed from scratch.Note: The existing markdown caching fix (commit d57cb2e — "cache Control UI markdown rendering + memoize chat text extraction to reduce Safari typing jank") does not cover the full history reload path triggered by tool call completion.
Proposed fixes
requestIdleCallbackorrequestAnimationFrameCHAT_HISTORY_RENDER_LIMITand the serverlimit: 200could be lowered to ~50 for immediate reliefnode.list— the 5-second node polling should not trigger a full chat re-render if nodes didn't changeEnvironment
Additional context
This is a classic "works fine with small data, dies at scale" issue. When first installed, sessions are empty/tiny and the 200-message synchronous render is fast. After weeks of daily use with tool calls generating large outputs, sessions grow to megabytes, and the rendering pipeline becomes unusable.
Related issues for context: