Bug type
Performance / UX regression
OpenClaw version
2026.5.22
Environment
- OS: macOS Darwin 25.5.0 (arm64, MacBook Air)
- Node: v22.22.3
- Runtime: Local (non-Docker)
- Active sessions: ~10 across multiple agents (main, lockman, dev, ops, etc.)
Description
Switching between sessions in the Control UI dashboard takes 8–10 seconds. The delay is noticeable on every switch, not just the first load.
What we already tried
Session file cleanup (helped but did not fix)
- Cleaned
main agent sessions dir: ~90 MB → 4.6 MB (removed old trajectories, checkpoints, .reset/.deleted/.bak files)
- Cleaned
lockman agent sessions dir: 5.4 MB → 560 KB
- Cleaned all other agents' stale files
- Restarted gateway
- Result: Marginal improvement, but switching still takes 8–10 seconds
Backend is not the bottleneck
openclaw sessions list --agent main responds in ~2s (includes CLI startup overhead)
- Gateway API responses for session metadata are fast
- The bottleneck is clearly in the frontend rendering pipeline
Frontend source analysis
Analyzed the bundled Control UI (dist/control-ui/assets/index-BtIuF4zW.js, 1.2 MB):
1. No virtual scrolling
The chat message list renders all messages via a full .map() — there is no virtual list, no IntersectionObserver-based windowing, and no lazy rendering. Searching for "virtual" in the bundle only returns syntax-highlighting keyword lists for C++/Rust, not any virtual scroll library.
2. Full history load on every switch
The session switch handler (bV) sets the new sessionKey, then triggers Tx() which calls chat.history with hardcoded parameters:
ex = 100 // limit: 100 messages
tx = 4000 // maxChars: 4000 per message
This fetches up to 100 messages × 4000 chars = up to 400 KB of text on every switch.
3. Full DOM re-render
After loading, all messages are processed through projectChatDisplayMessages() → sanitization → DOM creation. There is no incremental/differential rendering — the entire message list is rebuilt from scratch.
4. No code splitting for chat view
The core session/chat view is in the main bundle. While locale/agent chunks are split out, the chat rendering path is not lazy-loaded.
Suggested improvements
-
Add virtual scrolling to the message list (e.g., @tanstack/virtual, react-virtuoso, or a custom IntersectionObserver implementation). This alone would eliminate the bulk of the 8–10s delay for sessions with many messages.
-
Lazy-load / paginate chat history. Instead of loading 100 messages upfront, load the most recent 20–30 and fetch more on scroll-up. The backend already supports cursor-based pagination (nextCursor).
-
Incremental DOM updates. On session switch, render a skeleton/placeholder immediately, then populate messages asynchronously. Avoid blocking the UI thread with full message processing.
-
Consider reducing the default limit/maxChars for the initial load, since the backend supports pagination.
Impact
This makes the dashboard feel sluggish and hurts productivity when frequently switching between sessions (e.g., monitoring multiple agent conversations).
Bug type
Performance / UX regression
OpenClaw version
2026.5.22
Environment
Description
Switching between sessions in the Control UI dashboard takes 8–10 seconds. The delay is noticeable on every switch, not just the first load.
What we already tried
Session file cleanup (helped but did not fix)
mainagent sessions dir: ~90 MB → 4.6 MB (removed old trajectories, checkpoints, .reset/.deleted/.bak files)lockmanagent sessions dir: 5.4 MB → 560 KBBackend is not the bottleneck
openclaw sessions list --agent mainresponds in ~2s (includes CLI startup overhead)Frontend source analysis
Analyzed the bundled Control UI (
dist/control-ui/assets/index-BtIuF4zW.js, 1.2 MB):1. No virtual scrolling
The chat message list renders all messages via a full
.map()— there is no virtual list, noIntersectionObserver-based windowing, and no lazy rendering. Searching for"virtual"in the bundle only returns syntax-highlighting keyword lists for C++/Rust, not any virtual scroll library.2. Full history load on every switch
The session switch handler (
bV) sets the newsessionKey, then triggersTx()which callschat.historywith hardcoded parameters:This fetches up to 100 messages × 4000 chars = up to 400 KB of text on every switch.
3. Full DOM re-render
After loading, all messages are processed through
projectChatDisplayMessages()→ sanitization → DOM creation. There is no incremental/differential rendering — the entire message list is rebuilt from scratch.4. No code splitting for chat view
The core session/chat view is in the main bundle. While locale/agent chunks are split out, the chat rendering path is not lazy-loaded.
Suggested improvements
Add virtual scrolling to the message list (e.g.,
@tanstack/virtual,react-virtuoso, or a customIntersectionObserverimplementation). This alone would eliminate the bulk of the 8–10s delay for sessions with many messages.Lazy-load / paginate chat history. Instead of loading 100 messages upfront, load the most recent 20–30 and fetch more on scroll-up. The backend already supports cursor-based pagination (
nextCursor).Incremental DOM updates. On session switch, render a skeleton/placeholder immediately, then populate messages asynchronously. Avoid blocking the UI thread with full message processing.
Consider reducing the default
limit/maxCharsfor the initial load, since the backend supports pagination.Impact
This makes the dashboard feel sluggish and hurts productivity when frequently switching between sessions (e.g., monitoring multiple agent conversations).