Repository navigation
perf(core): cache media query matches between device changes - #11512
Draft
edusperoni wants to merge 2 commits into
Draft
edusperoni wants to merge 2 commits into
edusperoni wants to merge 2 commits into
Conversation
Every CSS state change re-evaluated each @media selector scope through the native screen and application property getters and re-parsed the query string, so a view tree with many media-scoped rules paid thousands of native calls per class change. Match results are now cached and cleared on orientation and system appearance changes, parsed queries are memoized, and application event listeners requested before the platform installs its handler are replayed once it arrives so the invalidation hook is never lost.
|
View your CI Pipeline Execution ↗ for commit a243425
💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗ ☁️ Nx Cloud last updated this comment at |
commit: |
A media query that fails to parse or names a feature the runtime does not evaluate (color-gamut, hover, prefers-reduced-motion, ...) can never match, so its rules are now dropped when the stylesheet is built instead of being re-checked on every style pass. For the queries that remain, device state is read only when a feature asks for it: a width query no longer reads the orientation and appearance, an orientation query no longer resolves the screen, and the screen is resolved once per evaluation rather than once per feature.
This branch has not been deployed
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.
PR Checklist
What is the current behavior?
Every CSS state change on a view (a class added or removed, a pseudo-class toggled) re-collects selector candidates, and for each
@mediaselector scope in the stylesheet that means callingcheckIfMediaQueryMatches. That function readsScreen.mainScreen.widthPixels/heightPixels, the orientation and the system appearance through native getters, and re-parses the media query string with regexes, on every call, before it even looks at which features the query uses.collectCandidatesonly remembers queries that matched, in an array created fresh per view, so a query that does not match is re-evaluated for every scope that uses it, on every view, on every style pass. A query that uses a feature core does not evaluate at all (for example the@media (color-gamut: p3)blocks that Tailwind 4 and postcss-preset-env emit as fallbacks) is treated the same way: it returns false, so it is never cached, and it is re-checked forever even though it can never become true.In a profile of a screen with ~500 labels whose classes change together, with 52 such media scopes in the stylesheet, ~80% of the main-thread time of each interaction was inside
checkIfMediaQueryMatches(native screen/appearance getters plusparseQuery); the actual selector matching was under 2%.What is the new behavior?
Two commits:
1. Cache match results between device changes
media-query-list: match results are cached per query string. The cache is cleared on the applicationorientationChangedandsystemAppearanceChangedevents (the same eventsMediaQueryListalready uses forchangenotifications), so results stay correct across rotations and dark/light switches.onDeviceChangealso clears the cache before re-evaluating the registered lists, so listener order cannot leave a list reading a stale result.css-mediaquery:matchQuerymemoizes the parsed form of each query string.parseQueryitself is unchanged and still throws on invalid input, which is never cached.application/helpers-common:toggleApplicationEventListenersnow queues listeners requested before the platform installs its handler and replays them insetToggleApplicationEventListenersCallback. This guarantees the cache invalidation hook is registered even when a media query is evaluated beforeapplication.ios/application.androidfinishes loading, and also fixes the pre-existing case where aMediaQueryListcreated that early never receivedchangeevents.2. Drop unevaluable media scopes and read device state lazily
css-mediaquery: newisEvaluableMediaQuery(query)returns whether a query parses and uses only features the runtime evaluates (width,height,device-width,device-height,orientation,prefers-color-scheme, withmin-/max-prefixes).style-scope:_populateRulesskips an@mediablock (and anything nested in it) when its query is not evaluable, with one trace warning in theMediaQuerycategory. Such a block could never match, so this changes no rendering; it only stops the block from being re-checked on every style pass.@supportsblocks were already dropped this way.media-query-list:checkIfMediaQueryMatcheschecks evaluability first and returns false for an unevaluable query without touching native state. For evaluable queries the values object uses lazy getters, so a width query no longer reads orientation or appearance, an orientation query no longer resolves the screen, and the screen is resolved once per evaluation (shared bywidthanddevice-width) rather than once per feature.Unit tests cover: cache reuse and invalidation on device change,
MediaQueryListnotifications staying in sync with the cache, the pending-listener replay,isEvaluableMediaQueryaccept/reject cases, zero device reads for unevaluable queries, feature-driven reads for evaluable ones, and scope dropping (top-level and nested) in_populateRules.Not covered: a change in
Screen.mainScreenmetrics that is not accompanied by an orientation or appearance event (an external display connecting, for example). Previously such a change was picked up on the next style pass; now it is picked up on the next orientation or appearance change.