[Android] Scrolling inside shadowed containers regressed in 10.0.90
- Dominant language
- C#
- Stars
- 23.3k
- Forks
- 2k
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 296
Description
### Description
Android scrolling inside a shadowed ancestor regressed between .NET MAUI 10.0.80 and 10.0.90. `CollectionView` and `ScrollView` content can become severely sluggish or temporarily blank because descendant updates repeatedly invalidate and redraw the entire shadowed container.
Original report:
https://www.reddit.com/r/dotnetMAUI/comments/1vjyyjn/net_10_scrolling_performance_issues_on_android/
Author reproduction:
https://github.com/MrLazyLee/ScrollPerformance
The public repository omitted several unrelated application types, so minimal local stubs were required to build it. The reported popup, bindings, resources, visual hierarchy, and scrolling content were left unchanged.
### Hidden hierarchy in the complete reproduction
The XAML excerpt in the Reddit post only showed the `CollectionView`, but the runnable hierarchy adds two large shadow wrappers around it:
1. CommunityToolkit `PopupOptions` supplies a default popup shadow.
2. `ChangeLogPopup.xaml` wraps the list in a `Border` using `Shadow="{StaticResource CenterShadow}"`.
The same root cause applies to the author's reported `ScrollView` replacement because it is not specific to virtualization.
### Faulty commit
The regression was introduced by #35623, release commit [`2e3d581d52a16ea1d30a6de8ff3b88dd23a1ace8`](https://github.com/dotnet/maui/commit/2e3d581d52a16ea1d30a6de8ff3b88dd23a1ace8).
That change made `PlatformWrapperView.onDescendantInvalidated()` call `invalidate()` for every descendant update whenever the wrapper has a shadow. The intended `Switch` fix needs a redraw when the immediate child animates, but scrolling descendants then force the whole popup and border wrappers to re-record their display lists for every item update.
Four automated up/down scroll cycles produced:
| Wrapper | Descendant invalidations | Immediate-child invalidations |
|---|---:|---:|
| Popup, 1079 x 1678 | 1,859 | 3 |
| Border, 1026 x 1306 | 1,803 | 2 |
Only five invalidations originated from immediate children; the other 3,657 caused unnecessary shadow-wrapper redraws.
#36988 fixes a separate software-shadow case involving a fully transparent border stroke. It does not cover this reproduction's opaque `#808080` stroke or the popup's fallback shadow path.
### Performance verification
The author reproduction was tested on the same Android emulator with the same automated eight-cycle scroll sequence and no screen recording. Control and fix AARs were built from the exact 10.0.90 source so the invalidation guard was the only functional difference.
| Variant | 50th percentile | 90th percentile | Slow UI-thread frames |
|---|---:|---:|---:|
| 10.0.80 | 34 ms | 53 ms | 29 |
| 10.0.90 control | 42-44 ms | 53-61 ms | 106-118 |
| 10.0.90 with fix | 34-38 ms | 57-61 ms | 28-47 |
The fix returns the reproduction to the 10.0.80 range.
### Videos
**10.0.80 baseline**
https://github.com/user-attachments/assets/4a7e8b4a-8ebd-499b-9477-718886608ab8
**10.0.90 regression**
https://github.com/user-attachments/assets/fc2dceb7-5f1f-4eae-9461-2c53fcc6bc90
**10.0.90 with fix**
https://github.com/user-attachments/assets/0ea77dab-5cd8-4cb5-956a-169c9b12c31b
### Fix
#37291 limits forced shadow redraws to invalidations from the wrapper's immediate child. Deeper descendants continue through their own Android RenderNodes, preserving the original `Switch` fix while avoiding full-container redraws during scrolling.
Contributor guide
Research direction
Start at PlatformWrapperView.onDescendantInvalidated() and review the linked #37291 change alongside the 10.0.90 regression details. Build the author reproduction, run the automated eight-cycle scroll sequence, and compare shadowed CollectionView and ScrollView behavior with the reported 10.0.80 and fixed-build performance ranges.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, csharp
- Domain
- mobile, performance
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 25/100