Skip to content

perf(core): skip-unchanged CSS re-application, traversal cleanups#11308

Open
NathanWalker wants to merge 3 commits into
mainfrom
perf/core-optimizations-phase2
Open

perf(core): skip-unchanged CSS re-application, traversal cleanups#11308
NathanWalker wants to merge 3 commits into
mainfrom
perf/core-optimizations-phase2

Conversation

@NathanWalker

@NathanWalker NathanWalker commented Jul 17, 2026

Copy link
Copy Markdown
Contributor
  • CssState.setPropertyValues: fix the pre-existing ordering bug where delete oldProperties[property] ran before the "skip unchanged" check, making it dead code — unchanged declarations were re-applied through the property system on every dynamic-state change (every :active toggle). Scoped css-variables are still always re-registered because resetScopedCssVariables clears them earlier in the same pass.
  • eachLayoutChild allocates 2 closures per measure/layout pass instead of N+1; onLoaded/onUnloaded traversals use shared module-level callbacks (dispatching via child.parent to preserve loadView/unloadView overrides, e.g. TabView).
  • query() appends media-query candidates via indexed loop instead of push-spread (avoids arg-spread limits on large candidate sets).
  • Benchmarks: theme-scale stylesheet + recycled-rows query bench; new spec tests for repeated-query consistency incl. media-query non-accumulation.

Considerations

Additional notes to be considered aside (above/beyond) phase 1 perf opts here: #11307

Note: selector candidate memoization keyed by (cssType, id, classes) was prototyped and measured to be a net LOSS at realistic scale (cached lookup 273k ops/s vs 294k direct; recycled rows -21%); string key building plus Map hashing costs more than the push-based rebuild from phase 1, which already made candidate sets cheap.

Benchmark Direct rebuild (phase 1) Memoized Δ
query — button w/ id + 4 classes 294,492 ops/s 273,512 ops/s −7%
query — 20 recycled list rows (4 classes each) 15,572 ops/s 12,324 ops/s −21%

Building the string cache key (per-class concatenation) plus hashing it for Map.get costs more than the push-based candidate rebuild that phase 1 already made cheap; per-node candidate sets are bounded (universal + id + type + k class buckets, realistically < 100 entries), and accumulateChanges+sort dominate query cost regardless. Mentioning here so it isn't re-attempted without new evidence.

NathanWalker and others added 3 commits July 17, 2026 13:31
…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>
…, traversal cleanups

- CssState.setPropertyValues: fix the pre-existing ordering bug where
  delete oldProperties[property] ran before the "skip unchanged" check,
  making it dead code — unchanged declarations were re-applied through the
  property system on every dynamic-state change (every :active toggle).
  Scoped css-variables are still always re-registered because
  resetScopedCssVariables clears them earlier in the same pass.
- eachLayoutChild allocates 2 closures per measure/layout pass instead of
  N+1; onLoaded/onUnloaded traversals use shared module-level callbacks
  (dispatching via child.parent to preserve loadView/unloadView overrides,
  e.g. TabView).
- query() appends media-query candidates via indexed loop instead of
  push-spread (avoids arg-spread limits on large candidate sets).
- Benchmarks: theme-scale stylesheet + recycled-rows query bench; new spec
  tests for repeated-query consistency incl. media-query non-accumulation.

Note: selector candidate memoization keyed by (cssType, id, classes) was
prototyped and measured to be a net LOSS at realistic scale (cached lookup
273k ops/s vs 294k direct; recycled rows -21%) — string key building plus
Map hashing costs more than the push-based rebuild from phase 1, which
already made candidate sets cheap. Rejected; documented here so it is not
re-attempted without new evidence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nx-cloud

nx-cloud Bot commented Jul 17, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 37d2b8e

Command Status Duration Result
nx test apps-automated -c=android ✅ Succeeded 3m 41s View ↗
nx run-many --target=test --configuration=ci --... ✅ Succeeded <1s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-07-17 22:42:10 UTC

@NathanWalker NathanWalker changed the title perf(core): phase-2 optimizations — skip-unchanged CSS re-application, traversal cleanups perf(core): skip-unchanged CSS re-application, traversal cleanups Jul 17, 2026
Base automatically changed from perf/core-optimizations to main July 18, 2026 01:19
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