Skip to content

fix(app): reduce streaming CPU, scroll thrashing, and session switch overhead#31517

Open
BYK wants to merge 3 commits into
anomalyco:devfrom
BYK:byk/perf-streaming-session-switch
Open

fix(app): reduce streaming CPU, scroll thrashing, and session switch overhead#31517
BYK wants to merge 3 commits into
anomalyco:devfrom
BYK:byk/perf-streaming-session-switch

Conversation

@BYK

@BYK BYK commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #31548

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Fixes several performance bottlenecks in the web UI that cause high CPU during streaming, layout thrashing, and session switch lag.

Skip Shiki WASM during streaming (marked.tsx, markdown.tsx): The markdown parser is split into a light parser (no Shiki) and a full parser. During streaming, the light parser runs — no WASM syntax highlighting on the main thread. Code blocks appear as plain <pre><code> while streaming and get syntax-highlighted when the stream ends. This eliminates the biggest main-thread blocker during streaming.

Single store mutation per SSE delta (event-reducer.ts): Removes the redundant produce() mutation on the part store during message.part.delta events. Only the flat part_text_accum_delta store is updated per delta. readPartText() already prefers accum over part.text, so the UI sees the latest text without the second write. This halves reactive propagation on the hottest streaming path.

Reduce scroll anchor thrashing (message-timeline.tsx): The bottom-anchor rAF loop is reduced from 90 frames (~1.5s) to 12 (~200ms), and the active streaming cap from 12 to 6. Each frame was reading scrollHeight and writing scrollTop, causing forced layout reflows.

Lightweight content version counter (message-timeline.tsx): Replaces activeAssistantContentVersion which concatenated all active assistant message content into a massive string on every SSE delta. Now it just increments a counter — same dependency tracking, zero allocation.

Enable diff virtualization (message-timeline.tsx): Sets virtualize={true} on diff views inside tool parts so large file diffs don't render thousands of DOM nodes.

Remove duplicate session loading (bootstrap.ts, directory-layout.tsx): Removes a duplicate loadSessions() call in bootstrap (called at both start and end of the array) and a duplicate createResource in directory-layout.tsx that called sync.session.sync(id) on every session switch — the same call already exists in session.tsx.

How did you verify your code works?

  • Ran bun typecheck from packages/opencode — no new errors (all existing errors are pre-existing in test files)
  • Built the binary with OPENCODE_CHANNEL=latest bun run script/build.ts --single — build succeeded, smoke test passed
  • Verified chunk splitting in build output: katex (265KB), shiki (9.4MB), kobalte (205KB), effect (40KB), luxon (70KB), pierre (278KB) are now separate chunks
  • Tested session switching between cached sessions
  • Tested streaming responses with code blocks (plain text during stream, highlighted after)

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

BYK added 3 commits June 9, 2026 13:52
…store mutation per delta

- Split marked parser into light (no Shiki) and full parser. During streaming,
  use the light parser to avoid WASM syntax highlighting on the main thread.
  Code blocks render as plain <pre><code> during streaming and get highlighted
  when the stream ends.
- Remove the redundant produce() mutation on the part store during
  message.part.delta events. Only the lightweight part_text_accum_delta store
  is updated per delta now — readPartText() prefers accum over part.text, so
  the UI sees latest text without a second store write per delta.
… lightweight content version

- Replace activeAssistantContentVersion string fingerprint with a lightweight
  counter. Previously concatenated all active assistant message content into a
  massive string on every SSE delta; now increments a number.
- Reduce scroll anchor rAF loop from 90 frames (~1.5s) to 12 (~200ms), and
  the active streaming cap from 12 to 6. Reduces layout thrashing from
  repeated scrollHeight reads and scrollTop writes.
- Enable diff virtualization (virtualize={true}, virtualizeDiff={true}) so
  large file diffs in tool results don't render thousands of DOM nodes inline.
- Remove duplicate loadSessions() call in bootstrapDirectory — sessions
  were loaded at both the start and end of the bootstrap array.
- Remove duplicate createResource in directory-layout.tsx that called
  sync.session.sync(id) on every params.id change. Session sync is
  already handled by session.tsx; the duplicate added unnecessary
  reactive tracking overhead and Promise creation on every session switch.
@BYK BYK requested review from Brendonovich and Hona as code owners June 9, 2026 13:52
@github-actions github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. contributor needs:issue labels Jun 9, 2026
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions github-actions Bot removed needs:compliance This means the issue will auto-close after 2 hours. needs:issue labels Jun 9, 2026
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Web UI: high CPU during streaming, layout thrashing, and session switch lag

1 participant