Skip to content

perf: a new browser tab shows its toolbar on its first frame (perceived speed) - #21059

Merged
teamleaderleo merged 14 commits into
feat-cmux-nextfrom
cx-next-handoff/browser-first-frame
Oct 20, 2026
Merged

teamleaderleo merged 14 commits into
feat-cmux-nextfrom
cx-next-handoff/browser-first-frame

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

A new browser tab (Cmd-Shift-L) didn't open settled. In recordings on feat c9c01bd (9 runs), the tab's chrome kept changing for up to 500 ms after it appeared:

  • Toolbar missing on the first frame (4 of 9 runs). The strip showed the new tab while the pane was still empty, and the sidebar row still had the old title. The toolbar and the row title newtab arrived a frame or more later. With Chromium already running, TabContentCache.browser(for:) still built the page in a Task, which runs on a later main-actor turn, so the pane got no content when it first showed the tab.
  • Chrome flicker during the New Tab page load (every run). The New Tab page's own load showed Stop, then flipped to Reload, while a progress line swept and faded under the toolbar. This took 3 repaints and 2 to 23 dropped frames, and settled at 196 to 509 ms.

Changes:

  • When Chromium is running, a tab on the New Tab page gets its page and chrome during the same turn that first shows it (ChromiumNewTabNow). Its configuration matches the async path for a page that needs nothing async: a closed-tab history, the proxied store check and the remote-localhost guard. Any other address, a proxied or remote-localhost store, or a Chromium that's still starting keeps the async path.
  • Loading the New Tab page no longer shows Stop or the progress line (BrowserTabState.showsLoad). Chromium hides its throbber on its New Tab page the same way. A real page still shows Stop and the line while it loads.

The tab strip is unchanged. Its tab icon still spins briefly on the New Tab page; that's Lawrence's area and is left as a follow-up.

Before / after

Measured with scripts/cmux-next/perceived-speed-slot.sh --surface browser, 3 runs per build, on DEV builds of c9c01bd plus these commits. Current feat DEV builds crash at launch in LocalRouterClient.exchange (#20878).

build frame 1 vs settled (area) first frame settled repaints dropped
c9c01bd (before) 0.12% 124 ms 348 ms 3 3
+ quiet New Tab load only 0.00% 126 ms 129 ms 0 0
+ both changes (shipped) 0.00% 102 ms 143 ms 0 0

Each row is the harness median over its 3 runs. In 1 of 3 runs, the quiet-load-only build still showed the strip with an empty pane for one frame (0.99% area, 8 dropped). That's the race the second change removes. On the build with both changes, every run's first frame has the toolbar, the address bar and the Reload button in their final state. One run showed the sidebar row's old title for one frame (0.07%, 4 dropped). This PR doesn't change where that title comes from. Two of three runs had no change at all after the first frame.

Tests

Each regression test was committed before its fix and run on the fleet with scripts/ci/run-swift-testing-suites.sh Packages/macOS/CmuxNext:

  • NewTabQuietLoadTests: red on aaa5f03 (Stop label and progress line on a chrome://newtab/ and an about:blank load; a real page still shows its load) in step 8ca017e40956a70e156866ed. Green on 45b13d4 in step 90854336e25cc8d31f6c440c.
  • ChromiumNewTabFirstFrameTests: red on 69e85e5 (browser(for:) returned nil with Chromium running) in step 7ce377877734a41deb851e92. Green on 5e22c90 in step 9755e88ac26ff388ccb7ce3c.

The only other failure in those runs is ClassicShortcutsUpgradeTests, a known failure on the base.

Local checks passed: check-app-size.sh --base origin/feat-cmux-next, check-concurrency.sh, check-no-godfiles.sh --only swift --base origin/feat-cmux-next (TabContentCache and BrowserChromeView didn't grow), crash_ratchet.py, and lint-ios-package-conventions.sh (its only ERROR is the known one on the base, BuiltInAdBlockerSetting).

Release compile (check-release-compile.sh): passed on 45b13d4 in step ca8191ae848a800251193e21. Passed on the head 5e22c90 in step 896512db1605ca7ef185ef56.

Follow-up (lane takeover, 2026-10-19)

  • App size check: the synchronous New Tab page added 22 source lines to CmuxNextApp, and the size check refuses any growth. The page stays. ClosedBrowserHistories, which is self-contained browser history kept across closes, moves unchanged from CmuxNextApp to CmuxNextBrowserHistory (Browser + Daemon, no UI). Only its public surface changed. CmuxNextApp now has 83,214 source lines against 83,246 on its base (scripts/cmux-next/count-swift-code-lines.py).
  • Stop button kept for real pages (review): a load the New Tab page starts itself (a tile, a link, a redirect) begins with no address, so showsLoad hid Stop and the progress line until a slow site committed. Now only the tab's opening load is quiet (BrowserTabState.hasEndedLoad). NewTabQuietLoadTests.aPageOpenedFromTheNewTabPageShowsItsLoadBeforeItCommits was committed red at f968390.
  • Warm start (review): the synchronous path records the tab open (noteTabOpened) the same way makeUserTab does.

Swift tests run in CI only. Nothing was built locally.

Changelog

  • Fixed: A new browser tab shows its toolbar on its first frame, with no Stop button or progress line flashing while the New Tab page loads.

🤖 Generated with Claude Code

teamleaderleo and others added 4 commits October 15, 2026 04:26
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Loading the New Tab page is quiet, like Chromium's: no Stop button and
no progress line, so a new tab's chrome does not change over its first
frames. A real page still shows its load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds ChromiumNewTabNow (not wired in yet) so the test can name its seam.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Chromium running, a tab on the New Tab page needs nothing
asynchronous, so its page and toolbar are made in the turn the pane
first shows it instead of a main-actor turn later, which left one frame
with the strip selected and no content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: b6266258-2317-4714-8dfb-c9529a99a7e7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 82c97c1: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr21059 -f ref=cx-next-handoff/browser-first-frame -f platform=macos -f backend_mode=local

Not verified: macOS jobs did not run: compile and app tests were skipped on 82c97c1c1a; ci-status is green without them.

CI passes on 82c97c1c1a (run 39330073217 attempt 1).
macOS jobs did not run: compile and app tests were skipped: Fast static checks skipped (the macos job needs both to succeed). Re-run CI once they pass, before merging.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Bead cx-kl8hm created for this PR by the cmux coordinator.

@blacksmith-sh

blacksmith-sh Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Found 1 test failure on Blacksmith runners:

Failure

Test View Logs
NEXT post-publication dependency contract/NEXT post-publication dependency contract View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@teamleaderleo teamleaderleo added area:browser area:performance difficulty:2 Focused: one package or feature boundary labels Oct 16, 2026
# Conflicts:
#	Packages/macOS/CmuxNext/Sources/CmuxNextApp/TabContentCache.swift
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

all web/size checks green

teamleaderleo and others added 4 commits October 19, 2026 12:49
… red

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s toward warm start

A load the New Tab page starts names no address until it commits, so
showsLoad hid Stop and the progress line for a slow site opened from a
tile. The quiet load is now the tab's opening one only. The sync New Tab
path also records the tab open like makeUserTab does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It grows CmuxNextApp, which the app size check refuses, and it needs its
App-only configuration moved out of the App target first. This PR keeps
the quiet New Tab load. The removed path is at 4d3cb99.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; both previous findings are addressed and no new blocking issue was found.

Summary

New Chromium tabs can show their toolbar immediately when Chromium already runs. The opening New Tab load stays quiet, while later loads still show Stop and progress.

  • A running Chromium New Tab page appears with its toolbar right away.
  • An opening New Tab load leaves the toolbar in its settled state.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Request browser tab] --> B{New Tab page and Chromium running?}
  B -->|No| E[Use asynchronous creation]
  B -->|Yes| C{Store checks allow immediate creation?}
  C -->|No| E
  C -->|Yes| D[Apply saved history]
  D --> F[Create and install page now]
Loading

Reviews (4) · Last reviewed commit: "browser: a New Tab page claims a closed ..." · Reviewed by Greptile

teamleaderleo and others added 3 commits October 19, 2026 13:03
The synchronous New Tab page grows CmuxNextApp by 22 source lines, which
the app size check refuses. ClosedBrowserHistories is self-contained
browser history across closes, so it moves to CmuxNextBrowserHistory
(Browser + Daemon, no UI). It is unchanged apart from the public surface
the App uses. CmuxNextApp now has fewer source lines than its base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines +14 to +15
/// Makes the page when Chromium runs, nil while it starts (a seam for tests).
static var make: (CEFEngine, BrowserTabConfiguration) -> (any BrowserTab)? = { $0.makeTabIfRunning($1) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Test hook changes every cache

ChromiumNewTabNow.make adds a mutable test hook to production source. ChromiumNewTabFirstFrameTests replaces it to pretend Chromium is running, changing tab creation for every cache in the process.

The repository forbids new test/debug seams under Sources/. Pass the factory through a scoped instance dependency instead. This requirement must be satisfied before merging.

Rule Used: Do not add new test/debug seams (ForTesting-style members, properties, or methods) to production source files under Sources/. Tests must reach internal state via @testable import instead. Existing occurrences are grandfathered but new ones are ... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

…for the later page

ChromiumNewTabNow claims a reopened tab's closed back/forward history
before its bail-outs, so when Chromium is still starting (or the tab is
proxied or needs a remote-localhost store) the asynchronous path finds
nothing to restore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cursor

cursor Bot commented Oct 19, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

#expect(ChromiumNewTabNow(cache: cache).page(tab, url: url) == nil)
let later = histories.restoring(BrowserTabConfiguration(id: BrowserTabID(rawValue: tab.id), profile: .default, initialURL: url),
url: url, local: true, tab: tab.id)
#expect(later.restoreState == .chromium("back-forward"), "the asynchronous path's page restores the closed tab's history")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 New test cannot pass

The new test fails against the current implementation. ChromiumNewTabNow.page calls closedHistories.restoring before the mocked make returns nil. That removes the saved history, so the second restoring call returns no restoreState and this assertion fails.

Keep the regression test, but include the fix that leaves history unclaimed when no page is created.

…makes now

ChromiumNewTabNow now checks that Chromium runs, the proxy plan and the
remote-localhost store first and claims a reopened tab's closed history
last, so every bail-out leaves it for the asynchronous path's page. The
make seam moves from a process-wide static to TabContentCache.chromiumNow,
an instance seam like makeCEFTab (REVIEW.md: no global test-only state).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review fixes (test first), from the correctness review of 2c119d26ce:

  1. ChromiumNewTabNow.page claimed a reopened tab's closed back/forward history before its bail-outs (proxied tab, remote-localhost store, Chromium still starting), so the asynchronous path's page lost it. It now checks those first and claims the history last.
  2. The static var make test seam (process-wide test-only state) is now an instance seam, TabContentCache.chromiumNow, like makeCEFTab. TabContentCache.swift keeps its line count.

Focused command (fleet): cmux-ci run --class light --script scripts/ci/package-test-lane.sh --arg=suite --arg=Packages/macOS/CmuxNext --arg=ChromiumNewTabFirstFrameTests

  • Red at 0d0410bc3c (test only): aNewTabPageNotMadeNowLeavesItsClosedHistoryForTheLaterPage failed, later.restoreState → nil.
  • Green at 82c97c1c1a (fix): both tests in the suite pass.

Base reds on this PR, not touched by its files: the same six cmux-next checks fail on #22146, which changes only .github/workflows/ci-web.yml (literal state root in cmux-tui remote_host/config.rs, sync NSWorkspace in HomeHostView.swift, missing agentPane/sidebar.doc translations, raw font sizes in HomeFirstRunSwitchCard/TerminalSizedForChip, cmux-tui tree inputs, syncMotion.gallery.tsx bundle key), plus the release-notary guard (#22015).

Observed / Not run

  • Observed: the red and green fleet runs above.
  • Not run: the full CmuxNextApp suite and the fast tier, which wait on CI.

@teamleaderleo
teamleaderleo merged commit 2c44688 into feat-cmux-next Oct 20, 2026
@teamleaderleo
teamleaderleo deleted the cx-next-handoff/browser-first-frame branch October 20, 2026 00:59
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

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

Labels

area:browser area:performance difficulty:2 Focused: one package or feature boundary

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant