perf(core): hot-path optimizations across events, properties, CSS matching and iOS layout#11307
Merged
Conversation
…ching and iOS layout - Observable.notify skips deprecated global-handler machinery (constructor name resolution + 4 hash lookups per event) when no global handlers are registered; listener dispatch uses .call instead of .apply([data]); hasListeners uses a direct read; _indexOfListener drops the closure. - isResetValue/isCssWideKeyword bail out with a single typeof check for non-string values (they run at the top of every property setter). - getSelectorCandidates replaces O(n*buckets) reduce+concat with push-based accumulation; AttributeSelector normalizes ignoreCase value once at construction; media-query string splits are cached; selector match paths use indexed loops instead of every/forEach closures and drop string-keyed method dispatch. - addPseudoClass/deletePseudoClass no longer allocate an aliased-states array per visual-state transition; classNameProperty.valueChanged drops per-change closures; fix latent getViewByDomId bug comparing the root's _domId instead of each child's. - iOS toDevicePixels/toDeviceIndependentPixels/getDisplayDensity cache the screen scale instead of resolving window->screen->scale through the ObjC bridge on every conversion (mirrors the Android density cache). - Add vitest micro-benchmarks (packages/core/__tests__/benchmarks) and unit tests locking dispatch order, thisArg binding, self-removal during dispatch, global-handler semantics, ignoreCase attribute matching and query result ordering. Measured: automated iOS suite 1808 OK / 0 failed before and after; duration 49,891ms -> 45,469ms / 43,019ms (two runs). Micro-benchmarks: selector query +19-39%, Observable.notify +22-34%, attribute match +19%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…re pass The config referenced @typescript-eslint/no-extra-semi, which was removed in typescript-eslint v8, causing a rule-not-found error on every linted file. Removing it surfaced pre-existing issues, addressed as follows: - Disable the v8 successors of the already-disabled ban-types rule (no-unsafe-function-type, no-wrapper-object-types, no-empty-object-type) plus no-unsafe-declaration-merging (interface+class merging is a core NativeScript pattern), and allow short-circuit/ternary expression statements, matching existing code style. - Stop linting generated iOS typings (platforms/) and the vendored css parser (css/lib/) in packages/core. - Drop parserOptions.project from packages/core lint config: no type-aware rules are in use, and it caused parsing errors for ~20 handwritten d.ts files not included in any tsconfig. - Code fixes: do..while(true) -> while(true) in CSS3Parser, prefer-const in inspector_modules, module -> namespace in file-system/trace d.ts files, eslint-disable for the required triple-slash reference in index.d.ts. nx lint core, nx test core (192 passed) and nx build core all pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
View your CI Pipeline Execution ↗ for commit ce4ef5a
💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗ ☁️ Nx Cloud last updated this comment at |
CatchABus
reviewed
Jul 17, 2026
| // (window resolution + screen + scale) on every dp/px conversion during | ||
| // measure and layout passes. The scale of the device's main screen never | ||
| // changes at runtime, and `Screen.mainScreen` relies on the same invariant. | ||
| let mainScreenScale = 0; |
Contributor
There was a problem hiding this comment.
I don't see the android counterpart for screen scale caching.
Actually, I just noticed android used to cache density before iOS started doing so but it still seems strange that only one of them has the internal screen scale helper helper.
CatchABus
requested changes
Jul 17, 2026
CatchABus
approved these changes
Jul 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Event system (
data/observable)notify()now skips the deprecated static/global event-handler machinery entirely (including thethis.constructor.nameresolution and 4 hash lookups per event) when no global handlers are registered, tracked via a simple module-level counter. Global handler semantics are unchanged when they are used._handleListenerEntryusescallback.call(thisArg, data)instead ofcallback.apply(thisArg, [data]), removing a throwaway array allocation on every listener invocation.hasListeners()uses a direct property read instead of theinoperator._indexOfListeneruses an indexed loop instead offindIndex+ closure (runs on everyaddEventListener).Property system (
ui/core/properties)isResetValue()/isCssWideKeyword()— these run at the top of every property setter. They now bail out with a singletypeofcheck for the overwhelmingly common case of non-string values (numbers, booleans, Colors, objects) instead of comparing against every CSS reset keyword.CSS selector matching (
ui/styling)SelectorScope.getSelectorCandidates()replaced an O(n·buckets)reduce+concat(which re-copied the accumulator on every bucket) with a single push-based accumulation. This runs for every view on load and on every class/id change.AttributeSelectorwithignoreCasenormalizes its value once at construction instead of lowercasing (and reassigning) it on every match.matchMediaQueryString()caches media-query string splits (they come from parsed stylesheets, so the distinct set is small and stable) and avoids per-call closures.SimpleSelectorSequence.match/mayMatch/trackChanges,ComplexSelector.match/trackChangesandSelector.ChildGroup.getMatchingNodeuse indexed loops instead ofevery/forEachclosures;getMatchingNodealso drops the string-keyed method dispatch (sel[funcName]).CssState.updateDynamicState/setPropertyValuesavoid per-call closure allocations (hoisted kebab-case replacement func, indexed filter loop). These run on every pseudo-class change (every tap).View base (
ui/core/view-base)addPseudoClass/deletePseudoClassno longer allocate an aliased-states array on every visual-state transition (every tap toggles:highlighted/:active/:pressed).classNameProperty.valueChangeduses indexed loops instead offorEachclosures (hot in list-item recycling).getViewByDomId(): the descendants callback comparedview._domId(the root argument) instead ofchild._domId, so descendant lookups never matched and always walked the entire subtree.iOS layout (
utils/layout-helper)toDevicePixels/toDeviceIndependentPixels/getDisplayDensitynow cache the screen scale instead of resolvingwindow → screen → scalethrough the ObjC bridge on every dp/px conversion (these are called several times per view per measure/layout pass). The device main-screen scale never changes at runtime —Screen.mainScreenalready relies on the same invariant, and the Android implementation already caches density this way.Measured results
Micro-benchmarks (vitest bench, committed under
packages/core/__tests__/benchmarks/)Node v25, darwin arm64, same machine/settings for both runs:
StyleSheetSelectorScope.query— button w/ id + 4 classesStyleSheetSelectorScope.query— plain labelObservable.notify— 1 listener (thisArg)Observable.notify— 3 listeners (thisArg)Observable.notify— no listenersnotifyPropertyChangeAttributeSelector.match(ignoreCase)ComplexSelector.match(child combinator)hasListeners/on/offcyclesReproduce with:
cd packages/core && npx vitest bench --runAutomated iOS test suite (
npx nx debug apps-automated ios, iPhone 17 Pro simulator)