Skip to content

Find controllers by element in O(1): 1,000 outlets connect 5x faster - #926

Open
czj wants to merge 1 commit into
hotwired:mainfrom
czj:perf/context-lookup-by-element
Open

czj wants to merge 1 commit into
hotwired:mainfrom
czj:perf/context-lookup-by-element

Conversation

@czj

@czj czj commented Oct 28, 2026

Copy link
Copy Markdown

Connecting a list of outlets is quadratic, because two lookups scan everything already connected:

  • Router#getContextForElementAndIdentifier, behind getControllerForElementAndIdentifier and OutletObserver#getOutlet, copied every connected context of the identifier into an array and searched it for the element.
  • OutletObserver#hasOutlet then scanned every connected outlet of that name, through getOutletFromMap, whenever the element had no connected controller yet.

Outlets run both for each element matching the outlet selector, so a list of 1,000 rows declared as outlets of a parent controller took 1.4 s to start on a fast laptop, and a list of 3,000 more than 30 s.

Module now keeps its connected contexts in a Map keyed by element (an element has at most one context per module, since ScopeObserver maps an element and identifier to a single scope), and hasOutlet checks outletElementsByName, which connectOutlet and disconnectOutlet keep in step with outletsByName. Ordering of application.controllers is unchanged.

What remains is OutletObserver#start refreshing the parent's selector observers, a full querySelectorAll of its subtree, each time one outlet connects. Fixing that needs a per-element refresh in the observers, so I left it out of this PR.

Two tests count the reads of Module#contexts and of the outlets map, and fail on main. Sibling PR: #925, which adds a test at the same place in application_tests.ts, so whichever lands second has a trivial conflict.

Measurements

Application#start on a page where a list controller declares static outlets = ["row"], with data-list-row-outlet="[data-controller=row]" around N row controllers, each with 2 targets, 2 values, 1 class and a button with 2 actions. main at 4145d26 and this branch, bundled with esbuild, Chrome 156 on an Apple Silicon Mac, median of 9 page loads.

Outlet rows main This branch
300 70 ms 34 ms
1,000 1,401 ms 266 ms
3,000 > 30 s (timed out) 2,184 ms

Would a per-element refresh for the remaining part be welcome as a follow-up?


🤖 Generated with Claude Code, but prompted, read, and verified by @czj, a caring human :)

`getControllerForElementAndIdentifier` copied and scanned every connected
context of the identifier, and matching an element against an outlet
selector scanned every connected outlet. Outlets do both for each
matching element, so a list of N outlets cost O(N²) to connect. Index
connected contexts by element, and check outlet elements by membership.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant