Engineering performance audit

T3 Code’s web UI can plausibly drive sustained CPU load and laptop heat.

Live Chrome profiling found a WebSocket-driven main-thread stall, while source analysis found several multiplicative paths that can turn active agent streams into continuous decoding, history scans, state rewrites, persistence, timeline derivation, and highlighting.

July 8, 2026 Repository SHA 18a41388eb69 Production + local build Worktree remained clean

Executive summary

Verdict: the architecture contains multiple sustained-CPU mechanisms.

The bad interaction was delayed primarily by WebSocket delivery and synchronous client work, not by painting. The most serious issue is a fan-out loop: the sidebar prewarms up to ten full thread subscriptions; stale subscriptions can each replay the global event log; every event is decoded, reduced into a whole-thread object, queued for full snapshot persistence, and allowed to invalidate history-wide derivations.

One interaction trace proves a burst. Source inspection shows why bursts can become sustained during active agent sessions. Direct OS-level CPU and thermal telemetry was not recorded, so the report distinguishes measured evidence from high-confidence causal inference.

HIGH user-visible and thermal risk

Fix first

Reduce sidebar prewarming and stop performing a global event-log replay for every thread subscription.

Fix second

Batch client events, incrementalize thread state and shell summaries, and move full snapshot encoding away from the hot path.

Then optimize

Stop reparsing and re-highlighting cumulative streaming output, split cold-start code, and lazily mount the diff worker pool.

Observed evidence

Measurements below came from Chrome’s Performance panel against the open T3 Code Nightly production tab, plus a production build of the pinned local repository. A later malformed DevTools INP value was discarded rather than used.

Interaction to Next Paint 689 ms Poor interaction responsiveness.
Input delay 580 ms The main thread was unavailable before handling input.
WebSocket task 102 ms A single received message produced a long main-thread task.
RPC decode 73 ms Approximate self time in Effect RPC serialization decode.
Selected 1.03 s window 878 ms System + scripting work; strong CPU burst evidence.
Warm thread switch 177 ms Scripting, plus 81 ms rendering and painting.
Initial JS 1.29 MB Gzip across the main and preloaded textarea chunks.
Heap sample 171 MB Directional only; DevTools and retained traces add overhead.

Selected interaction CPU composition

System
659 ms
Scripting
219 ms
Rendering
30 ms
Painting
9 ms

The system category is not fully attributable to application JavaScript, but its timing overlaps the interaction and WebSocket processing window. The decisive observation is that paint is small relative to delivery, decode, and scripting.

Which findings can explain CPU spikes and overheating?

Heat requires sustained energy use, not merely one slow click. The following mechanisms can repeat for the entire duration of one or more busy agent sessions.

Mechanism Pattern Thermal plausibility How to recognize it
Background thread subscriptions Continuous while agents emit events, including threads not currently open. Very high CPU rises with any busy agent and remains elevated while viewing another thread.
Global catch-up replay Large burst after reconnect, wake, navigation, or stale cache recovery. Very high CPU and network spike together after reconnect; UI becomes unresponsive.
Whole-thread reduction and persistence Repeats every stream event and every 500 ms persistence window. High CPU scales with thread history and event frequency; IndexedDB work appears often.
Markdown and code highlighting Can reprocess the cumulative answer for every streaming update. High when enabled CPU climbs while a long code-heavy answer streams and drops when it finishes.
Diff workers Two to six workers retained for the lifetime of the chat view. Medium Multiple Chrome worker processes/threads stay active after diff use.
Cold bundle parse Primarily startup and reload. Short spike CPU spikes after reload, then settles.
Minimap and composer work Scroll- or typing-bound. Localized Load follows rapid scrolling or typing rather than agent activity.

Profiling caveat

Chrome DevTools was open with retained Performance recordings. DevTools can increase CPU and memory use, so thermal confirmation must be repeated with DevTools closed and Chrome Task Manager or OS telemetry collecting separately. The source-level amplification paths remain valid regardless of profiler overhead.

The amplification path

The performance risk is multiplicative. Each stage feeds more work into the next, and several stages scale with the total history of a thread rather than with the size of the newest event.

Ranked findings and recommended fixes

Confidence reflects the combination of live evidence and direct source confirmation. Impact estimates should be validated with the measurement gates later in this report.

01 Critical

Sidebar prewarming opens too many full live thread subscriptions.

The sidebar prewarms the first ten visible threads using the same full detail hook as an opened thread. Thread state has a five-minute idle TTL, allowing previously visible subscriptions and their cached state to linger. This multiplies snapshots, live events, decode work, reducers, persistence, and downstream invalidation.

Evidence

SIDEBAR_THREAD_PREWARM_LIMIT = 10; full thread prewarmer is mounted for every selected reference.

Recommended design

Prewarm only active, hovered, or next-likely threads. Prefer a snapshot fetch without opening a durable stream.

Success gate

At idle, one shell stream and at most one detail stream. Background threads produce no client CPU work.

02 Critical

Each stale cursor can replay the global event log before filtering.

Shell and thread catch-up both call readEvents(afterSequence, Number.MAX_SAFE_INTEGER). Per-thread filtering happens after the read. Live events arriving during catch-up are placed into unbounded queues. Ten stale detail streams can therefore multiply the same global scan and retain additional live data while replay is in progress.

Evidence

Direct source confirmation plus a measured WebSocket/decode long task.

Recommended design

Add aggregate-indexed reads, a maximum replay gap, snapshot fallback, batched frames, and bounded queues.

Success gate

Reconnect cost scales with events for the selected thread, not all events since its cursor.

03 Critical

Shell summaries are rebuilt from complete thread history.

Several ordinary thread events call a projection routine that loads all messages, plans, activities, and approvals, then rescans them to derive a handful of shell fields. Broad shell upserts can then make the client reprocess and re-sort sidebar state even when the visible summary barely changed.

Evidence

Four repository list operations occur together inside refreshThreadShellSummary.

Recommended design

Maintain counters and timestamps incrementally. Filter shell-irrelevant events and coalesce upserts.

Success gate

Assistant streaming chunks do not issue history-wide shell reads or sidebar upserts.

04 High

Every detail event can rewrite, publish, and persist the whole thread.

Message events search and map the message array; activity events filter, append, and sort activities. The resulting thread object is published through a subscription reference and queued for complete schema encoding and IndexedDB persistence after a 500 ms debounce. Debouncing limits write frequency but not the size of each encode.

Evidence

Direct reducer and persistence path inspection; consistent with main-thread scripting in the trace.

Recommended design

Index by ID, append monotonic events, batch updates per frame, and persist deltas or snapshots during idle time in a worker.

Success gate

Per-event processing remains approximately constant as a thread grows from 100 to 10,000 activities.

05 High

Timeline derivation repeats full-history passes and sorts.

Activities are already sorted by the reducer, then copied and sorted again for work-log derivation. Messages, plans, and work entries are mapped into new arrays and the combined timeline is sorted again. Additional row derivation performs further passes. DOM virtualization prevents a large mounted tree, but it cannot prevent this upstream work.

Evidence

Only about seven timeline rows were mounted, while scripting remained substantial.

Recommended design

Maintain an incrementally ordered timeline and split subscriptions by messages, activity, session, and composer concerns.

Success gate

Appending one activity does not allocate or sort arrays proportional to total thread history.

06 High when streaming

Streaming markdown and code highlighting can approach quadratic work.

Each update reparses the cumulative message through Markdown, GFM, raw HTML, and sanitization. Streaming bypasses the highlighted-result cache, but still runs Shiki over the full growing code block. Unsupported languages also retry with the original language after obtaining a text fallback, generating repeated exceptions and console warnings.

Evidence

Repeated live warnings were observed for sh and yaml; source confirms full-code highlighting.

Recommended design

Render plain code during streaming, highlight once finalized, throttle Markdown updates, and negative-cache fallback languages.

Success gate

CPU stays nearly flat as a streaming code block grows; no fallback exceptions appear.

07 High at startup

The cold-start JavaScript payload is too large.

The main bundle is approximately 3.425 MB minified / 1.028 MB gzip, and the preloaded textarea chunk adds about 850 KB / 265 KB gzip. The main bundle statically contains xterm, Pierre diffs, parse5, Lexical, and broad application code even when terminal, diff, file preview, and settings surfaces are closed.

Evidence

Production Vite build completed with chunk-size warnings and source-map attribution.

Recommended design

Lazy-load terminal, diff/highlighter, settings, account, and command-palette implementations. Add gzip budgets.

Success gate

Initial route ships under 500 KB gzip JavaScript, excluding code loaded only after explicit feature use.

08 Medium

The diff worker pool and syntax caches are mounted too eagerly.

Every chat view is wrapped in a provider configured for two to six workers and a large AST cache. Workers may be created lazily by the dependency, but once used they remain tied to the chat-view lifetime. This can contribute to multi-core load and heap retention after a diff was viewed.

Evidence

Worker tracks existed in the live trace; exact active time and retained cache size were not isolated.

Recommended design

Mount on first diff use, lower the default pool, adapt to device class, and terminate after an idle timeout.

Success gate

No diff workers exist before a diff is opened, and they disappear after the configured idle interval.

09 Medium

Composer persistence can put serialization directly in the typing path.

Each prompt update changes the persisted Zustand store. The partializer walks all retained drafts and clones nested contexts, annotations, and comments before JSON storage hands the final write to a debounced backend. With many retained drafts, every keystroke can do work proportional to all draft state.

Evidence

Code-backed risk; a large-draft typing profile was not captured.

Recommended design

Debounce before partialization and serialization, persist the active draft delta, use idle work, and prune stale drafts.

Success gate

Typing latency remains stable with hundreds of retained drafts and large review-comment state.

10 Lower priority

Several smaller recurring costs should be cleaned up.

Sidebar and command palette each run an independent two-second bootstrap polling hook. The timeline minimap checks every item and mutates datasets during scroll. Large public source maps increase deployment and tooling overhead. Markdown component factories also trigger existing unstable-nested-component lint warnings.

Evidence

Source confirmed; none individually dominated the sampled long task.

Recommended design

Centralize polling, compare before publishing, update only changed minimap ranges, and use hidden or disabled production maps.

Success gate

An idle tab performs no recurring React updates or list-wide DOM writes.

Recommended remediation roadmap

Sequence the work by removing amplification before micro-optimizing render code. Each phase should ship behind measurements that allow before/after comparison on the same fixture.

Instrument and pin

  • Align client and server versions.
  • Add marks around decode, reduce, derive, encode, and commit.
  • Record payload bytes, replay gaps, stream count, and queue depth.
  • Create 100-, 1,000-, and 10,000-event fixtures.

Stop fan-out

  • Drop full-detail prewarm from ten to one.
  • Add aggregate-indexed replay.
  • Snapshot when replay gap exceeds a threshold.
  • Bound live buffers and coalesce shell updates.

Make updates incremental

  • Batch incoming items per frame.
  • Normalize thread collections by ID.
  • Maintain shell counters and timeline order incrementally.
  • Persist deltas or encode snapshots off-thread.

Reduce optional work

  • Use plain code during streaming.
  • Lazy-load terminal and diff code.
  • Mount workers on demand.
  • Fix composer, minimap, polling, and source maps.

Suggested first pull request

  1. Change sidebar detail prewarming from ten threads to the selected thread plus, optionally, one hover-triggered snapshot.
  2. Add development counters for live detail subscriptions and replayed event counts.
  3. Add a regression test asserting that rendering ten sidebar rows does not open ten detail streams.
  4. Profile the same production-like fixture before and after. Do not bundle replay redesign into this first PR.

This is intentionally narrow: it should reduce CPU immediately and provide cleaner evidence for the aggregate-indexed replay work that follows.

Verification and observability plan

Performance fixes should be evaluated with sustained-load measurements, not only click traces. The user-reported failure mode is high CPU and heat, so thermal proxies and idle behavior matter.

Browser instrumentation

  • performance.measure for RPC decode, event application, timeline derivation, and cache encoding.
  • Long Task observer with current thread size and active stream count.
  • INP p75 segmented by thread message/activity counts and streaming setting.
  • Worker count, worker busy time, JS heap, and IndexedDB write bytes.

Server instrumentation

  • Replay events scanned versus events emitted per subscription.
  • Replay gap, duration, live-buffer high-water mark, and cancellation latency.
  • Shell summary query duration and records scanned.
  • Shell upserts emitted, coalesced, and suppressed as unchanged.

Thermal reproduction matrix

  • DevTools closed; Chrome Task Manager or OS sampler external to the tab.
  • Idle, one active agent, four active agents, and reconnect after a 30-minute stale cursor.
  • Short and 10,000-activity threads; streaming off and on.
  • Fresh load, warm navigation, diff viewed, and 10-minute post-diff idle.

Release gates

  • Idle foreground tab averages under 1% CPU after settling.
  • One busy background thread does not increase current-tab scripting materially.
  • No application long task above 50 ms in a warm thread switch fixture.
  • Initial-route JavaScript under 500 KB gzip and no workers before feature use.

Existing strengths to preserve

The timeline already uses virtualization, HTTP snapshots are gzip-friendly, resume cursors exist, and assistant streaming defaults to disabled. These are solid foundations. The recommended work should preserve those mechanisms while removing fan-out, redundant history scans, and hot-path full-state serialization.

Appendix: source evidence and audit boundaries

Audit boundaries

  • The production client was 0.0.29-nightly.20260709.763 while the bb-1 server was 0.0.29-nightly.20260701.697.
  • The live trace establishes bursty main-thread pressure, not laptop-wide sustained CPU utilization or temperature.
  • The heap sample included DevTools overhead and is not a leak diagnosis.
  • The local production build and focused web typecheck passed.
  • vp check passed with 29 pre-existing warnings.
  • Repository-wide typecheck was blocked outside the web app by unresolved Noble imports in infra/relay.
  • No application source files were changed as part of the audit.