Skip to content

Only measure selected bonsplit tab frame#22

Open
lawrencecchen wants to merge 1 commit intomainfrom
task-workspace-typing-lag
Open

Only measure selected bonsplit tab frame#22
lawrencecchen wants to merge 1 commit intomainfrom
task-workspace-typing-lag

Conversation

@lawrencecchen
Copy link

@lawrencecchen lawrencecchen commented Mar 11, 2026

Summary

Only publish the selected tab frame preference in .

Why

The cmux workspace typing-lag stress test showed the tab bar spending too much main-thread time recomputing frame preferences for every tab during dense workspace, split, and Bonsplit-tab churn.

Testing

  • Exercised from cmux's against a tagged dev build on
  • Verified the hot path stopped dominating follow-up captures

Summary by cubic

Measure and publish the tab frame only for the selected tab in TabBarView to cut main-thread work and reduce typing lag during heavy tab churn. Removes unnecessary GeometryReader work and preference updates for non-selected tabs, improving responsiveness in the cmux workspace stress test.

Written for commit 085411e. Summary will update on new commits.

Summary by CodeRabbit

  • Refactor
    • Optimized tab bar rendering by reducing unnecessary view creation and computations.

@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c25d63a9-f201-4e92-a273-a4fcb25f4751

📥 Commits

Reviewing files that changed from the base of the PR and between 505181f and 085411e.

📒 Files selected for processing (1)
  • Sources/Bonsplit/Internal/Views/TabBarView.swift

📝 Walkthrough

Walkthrough

The TabBarView's background modifier is refactored to use conditional view-building instead of always instantiating a GeometryReader. The preference key for the selected tab frame is now only set when a tab is actively selected, optimizing the view hierarchy.

Changes

Cohort / File(s) Summary
TabBarView Background Modifier Optimization
Sources/Bonsplit/Internal/Views/TabBarView.swift
Refactored background modifier from unconditional GeometryReader wrapper to conditional .background { } block. Preference key update for tab frame now only occurs when the tab is selected, preserving observable behavior while optimizing view construction.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A hop through SwiftUI's sleek refrain,
Where tabs now build with more disdain,
For empty views they'll skip with grace,
Selected frames take pride of place,
A clever conditional does remain! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Only measure selected bonsplit tab frame' directly describes the main change: limiting frame measurements to the selected tab. This aligns with the PR's objective to optimize performance by reducing redundant frame preference computations.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch task-workspace-typing-lag

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Mar 11, 2026

Greptile Summary

This PR narrows the GeometryReader used to track the selected tab's frame — moving the conditional check outside the GeometryReader rather than inside it. The net result is that only the currently-selected tab instantiates a GeometryReader in its .background, reducing geometry measurement during dense tab churn from O(n tabs) to O(1).

Key points:

  • Correctness preserved: SelectedTabFramePreferenceKey.reduce and defaultValue = nil are unchanged; the accumulation logic is unaffected because at most one view ever publishes a non-nil value (both before and after this change).
  • Separator gap behavior: tabBarBackground derives the separator gap from selectedTabFrameInBar, which is driven by onPreferenceChange. This chain continues to work correctly — when no tab is selected the preference reverts to nil in both implementations.
  • Minor behavioral delta: Because the GeometryReader for the newly-selected tab is freshly inserted each time selection changes (rather than already being present), there is a theoretical one-frame window where the preference is nil before the first layout measurement completes. This could cause a momentary full-width separator render, though it should be imperceptible at normal frame rates.

Confidence Score: 4/5

  • Safe to merge; the optimization is logically sound and well-tested, with only a minor theoretical one-frame visual edge case.
  • The change is minimal and targeted — one conditional guard moved outside a GeometryReader. The PreferenceKey reduce logic and all downstream consumers are unchanged. The PR has been tested against a dev build under the stress scenario that motivated it. The only concern is a possible single-frame separator flicker during tab switches, which is likely imperceptible and easily mitigated if it surfaces.
  • No files require special attention beyond the noted one-frame flicker risk in Sources/Bonsplit/Internal/Views/TabBarView.swift.

Important Files Changed

Filename Overview
Sources/Bonsplit/Internal/Views/TabBarView.swift Refactors the SelectedTabFramePreferenceKey publishing strategy in tabItem(for:at:) so that the GeometryReader is only instantiated for the currently selected tab. Previously, a GeometryReader was inserted into every tab's .background and published nil for non-selected tabs; now unselected tabs contribute no view at all, eliminating O(n) geometry measurements per layout pass.

Sequence Diagram

sequenceDiagram
    participant P as pane.selectedTabId
    participant FB as ForEach (tabs)
    participant BG as .background view
    participant GR as GeometryReader
    participant PK as SelectedTabFramePreferenceKey
    participant SB as tabBarBackground separator

    Note over FB,PK: Old approach — GeometryReader present for ALL tabs
    FB->>BG: render N tab backgrounds
    BG->>GR: create N GeometryReaders
    GR->>PK: publish frame (selected tab) or nil (others)
    PK->>SB: reduce → non-nil frame wins

    Note over FB,PK: New approach (this PR) — GeometryReader only for selected tab
    P->>FB: selectedTabId changes
    FB->>BG: render N tab backgrounds
    BG->>GR: create 1 GeometryReader (selected tab only)
    GR->>PK: publish frame for selected tab
    PK->>SB: reduce → selected tab frame (defaultValue=nil when none selected)
Loading

Last reviewed commit: 085411e

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

Comment on lines +262 to +271
.background {
if pane.selectedTabId == tab.id {
GeometryReader { geometry in
Color.clear.preference(
key: SelectedTabFramePreferenceKey.self,
value: geometry.frame(in: .named("tabBar"))
)
}
}
)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential one-frame separator flicker on tab switch

With the old approach every tab always had a live GeometryReader in its background, so when selection moved from tab A to tab B the new GeometryReader (for B) was already laid out and could publish frame_B in the same render pass that tab A's GeometryReader went silent. With the new approach the GeometryReader for tab B is inserted for the first time on that render pass. SwiftUI needs to lay it out before it can publish a frame, which may introduce a single-frame window where no view publishes a preference value and SelectedTabFramePreferenceKey temporarily resolves to its defaultValue of nil. During that frame, tabBarBackground would render the separator as full-width (no gap) before the correct gap is restored on the next layout pass.

In practice this flicker (≤16 ms at 60 fps) is likely imperceptible during normal tab switching, but it is a subtle behavioral difference worth being aware of. If it ever becomes noticeable, one mitigation is to keep the previous frame cached in tabBarBackground until the preference produces a non-nil value:

.onPreferenceChange(SelectedTabFramePreferenceKey.self) { frame in
    if let frame {
        selectedTabFrameInBar = frame
    }
    // Intentionally skip nil to avoid a one-frame gap during tab transitions.
}

This keeps selectedTabFrameInBar at the last-known good frame during the brief transition window.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant