Repository navigation
Component events repaint only when the rendered props change - #236
Merged
Merged
Conversation
Every Mob.Component handle_event/handle_info sent {:component_changed}
and the screen painted unconditionally, committing the whole native
tree. A native view emitting an event per key press (FMutate's
instrument) committed twice per press: 5-15 ms BEAM plus 10-20 ms of
Compose recomposition each on a Moto G Power 2021.
- Mob.ComponentServer notifies only when assigns changed and render/1
differs from the props the screen last fetched via render_props/1.
- Mob.Screen.Server repaints on {:component_changed} through
repaint_if_changed/1, so queued notices commit once.
Decision: decisions/2026-11-12-component-events-repaint-only-on-changed-props.md
- Mob.CrashReport.format_status/1 redacts a component's :rendered (the props render/1 last returned) like its assigns; crash reports and :sys.get_status/1 no longer print app data held there (MOB-310). - mix.lock: ecto 3.14.0 -> 3.14.3 (security advisory). - Regression test: a component state from before :rendered existed (kept across mix mob.push) still notifies and renders. - Correct the stale repaint comment; CHANGELOG documents that native views no longer get identical-props re-commits after rejected input. Reviewer: reviewer subagent (Codex rate-limited until 9:31 PM): SHIP. Device-verified on Moto G Power (Android 11) and iPhone SE (3rd gen).
Owner
Author
|
Follow-up
Device verification (fixture native component;
Reviewer: reviewer subagent (Codex rate-limited until 9:31 PM): SHIP. |
… device verification The MOB-310 record said format_status/1 "redacts the state"; it rewrites only the fields it names. Note added in place so the next state field holding app data gets added to redact_state/1.
…he parent-screen consequence
Merged
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.
Problem
Every
Mob.Componenthandle_event/3/handle_info/2sent{:component_changed, id, module}, whatever the callback returned, andMob.Screen.Serveranswered withpaint(state, :none). That paint never skips: it renders, expands every component, and commits the whole tree throughset_root.FMutate's instrument is a native view that sends an event per key press, key release and knob move, and its handler returns the socket unchanged for key events. On a Moto G Power 2021, 6 presses produced 12 commits (RenderStats). Each cost 5-15 ms on the BEAM side and 10-20 ms of Compose recomposition on the UI thread, and that was the main source of jank left.
Change
Mob.ComponentServernotifies the screen only when assigns changed (!==) andrender/1of the new assigns differs (!==) from the props the screen last fetched withrender_props/1. A callback that returns the socket untouched, assigns the same value again, or changes only an assignrender/1does not read sends nothing.:renderedis read withMap.getand written withMap.put, so a component started before this change keeps working aftermix mob.push.Mob.Screen.Serverhandles{:component_changed, ...}withrepaint_if_changed/1, the fingerprint skip that forwarded messages already use. Several notices queued behind one paint now commit once. Navigation, activation, hot reload and:syncstill paint unconditionally.handle_infopath was already checked. Since 2026-09-03 it skips the commit when the tree is unchanged, but it still runsrender/1and the expansion passes. That part is unchanged and recorded as out of scope in the decision.Behaviour change
A component's
render/1is now treated as a function of its assigns, as in LiveView. Anything else it reads is picked up on the next screen paint, not by a callback that changed no assign. Identical props are also no longer re-sent to a native view. Screens have behaved this way since the 2026-09-03 decision. The moduledoc and CHANGELOG cover both.Decision:
decisions/2026-11-12-component-events-repaint-only-on-changed-props.md. I added an in-place note to2026-09-03-skip-the-repaint-when-the-tree-is-unchanged.md, because its "onlyforward/2may skip" no longer holds.Tests
test/mob/component_server_test.exs, describe "telling the screen to repaint", covers:render_props/1fetchhandle_infoanddispatch/3follow the same ruletest/mob/component_repaint_test.exs(new) countsset_rootat a stub NIF, end to end through a screen with a native view:{:component_changed}whose tree is already on screen: 0 commitsMutation-checked:
component_server.exfails 6 of the new component-server tests.screen/server.exfails 2 of the component-repaint tests.Full suite:
mix test, 2170 passed (28 doctests, 2142 tests), 38 excluded, 0 failures.mix format --check-formatted,mix credo --strictandmix compile --warnings-as-errorsare clean, and the pre-push hook passed. A pre-commit adversarial review found nothing blocking.Verification rung: host BEAM tests only. Not yet measured on a device.