Skip to content

[Bug]: Rapid tab switching stalls for 1.3-1.6s — redundant per-switch DB reloads serialize on the single-connection store #3422

Description

@freefrank

Current Behavior

Switching tabs quickly (clicking through 3+ tabs, or holding the switch-tab shortcut) intermittently freezes the UI for 1.3–1.6 seconds. Single, slow-paced switches feel fine; the stall appears once switches come in quick succession, and it gets worse with more tabs/blocks in the workspace. Observed on Windows; the mechanism below is platform-independent, but slower disk I/O (NTFS + Defender) makes it much more visible there.

Expected Behavior

Tab switching stays in the low-milliseconds range regardless of how fast you switch or how many tabs the workspace has.

Steps To Reproduce

  1. Windows 11, Wave with one workspace containing ~5+ tabs, each with a few terminal blocks
  2. Rapidly switch between tabs (click tabs in quick succession or repeat the tab-switch keybinding)
  3. UI stalls for over a second; profiling the backend shows SetActiveTab spiking to ~1.5s

Wave Version

v0.14.5 (also reproduced on recent main)

Platform

Windows

OS Version/Distribution

Windows 11

Architecture

x64

Anything else?

I profiled this and the cost is not the WebContentsView swap (setBounds is ~0.1ms) — it's a flood of DB operations triggered on every switch, all serialized on the single-connection store:

  1. reinitWave reloads everything on each switch: client, window, workspace, the switched-to tab, its layout, plus every other tab in the workspace — N+5 DB reads per switch. These are almost entirely redundant: the tab's webContents stays alive off-screen with live WPS subscriptions, so its cache is already fresh.
  2. WorkspaceService.SetActiveTab reads the tab and all of its blocks to build a return value that no caller consumes (the frontend signature is already Promise<void>).

Under rapid switching these reads queue up on the store and everything behind them waits.

Measured results after fixing both (reloading only the switched-to tab + layout as a safety net, and dropping the unused return-value reads):

  • SetActiveTab backend time: max 1576ms → ~20ms (zero spikes >100ms)
  • Switch median: 16ms → 6.4ms

Working fix in my fork:

  • freefrank/waveterm@9a8d0ee (the core change)
  • freefrank/waveterm@71ea570 (follow-up: keeps the post-switch globalRefocus(), and adds a throttled background resync of client/window/workspace/tabs so a websocket reconnect while a tab is hidden can't leave its cache permanently stale)

Two design points worth flagging if you want this upstreamed:

  • The follow-up uses a 30s-throttled background resync as the stale-cache safety net; an alternative (arguably cleaner) is resync-on-reconnect in wos.ts, which didn't exist as a hook when I wrote this.
  • Removing SetActiveTab's return value is a service-contract change; I verified the only caller discards it, but calling it out explicitly.

Happy to open a PR with the fix if this direction works for you.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions