Skip to content

perf(native): a media query re-evaluates when its answer changes, not on every resize - #550

Open
NateIsern wants to merge 1 commit into
nativewind:mainfrom
NateIsern:perf/media-query-thresholds
Open

NateIsern wants to merge 1 commit into
nativewind:mainfrom
NateIsern:perf/media-query-thresholds

Conversation

@NateIsern

Copy link
Copy Markdown

Problem

A media feature compared with the window's size (min-width, max-width, width/height ranges, orientation) read vw and vh directly in testMediaQuery. That subscribed every component with a media query to the window's exact size. During a live window resize (desktop windows, iPad split view, an Android freeform window), Dimensions changes on every frame. So every such component re-ran updateRules and re-rendered on every frame, even though no query's answer had changed.

Tailwind's sm:/md:/lg: variants sit on many of an app's elements, so this adds up to a large share of the JavaScript in each resize frame. We measured it on a React Native host (OxyOS, Fabric) resizing a file browser built with NativeWind v5: several styled elements re-rendered on every frame while the window crossed no breakpoint.

Change

Each comparison is now an observable of its answer. windowQueryFamily holds one per query key (vw>=640, vh<=500, landscape, …), derived from vw/vh. Its default Object.is equality means it notifies only when the boolean flips. testMediaQuery reads that observable instead of the raw size. During a resize, the derived observable recomputes a comparison per frame, and the rule effects only run when a component crosses one of its breakpoints.

Nothing else changes. vw/vh units in declarations still follow the size continuously, resolution still reads PixelRatio, and every existing media-query test passes unchanged.

Test

media-query.test.tsx has a new test, "a resize re-renders a media query's element only when the query's answer changes". It wraps two elements (one with a matching min-width query, one with a non-matching one) in a Profiler, steps the width through seven values that cross no breakpoint, and expects no commits. Then it crosses one breakpoint and expects a re-render with the new style, and no further commits as the width keeps changing on the same side.

  • Before this change: 7 commits for the 7 steps (one per frame).
  • After: 0. The full suite passes (87 suites, 1390 tests), and tsc and eslint are clean.

🤖 Generated with Claude Code

… on every resize

A media feature compared with the window's size (min-width, max-width,
width ranges, height, orientation) read `vw`/`vh` directly, so every
component with a media query subscribed its rules to the window's exact
size. A window resized live changes that size on every frame: every such
component re-ran its rules and re-rendered on every frame, though no
query's answer changed (Tailwind's sm:/md:/lg: variants are on most of an
app's elements).

Each comparison is now an observable of its answer (windowQueryFamily),
derived from vw/vh: it notifies only when the answer flips, so a resize
re-renders a component once, when it crosses one of its breakpoints.

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

This branch has not been deployed

No deployments
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