.NET MAUI CollectionView Performance: Virtualisatie, DataTemplates en Optimalisatie (2026)

Zo verbeter je .NET MAUI CollectionView performance: virtualisatie, compiled bindings met x:DataType en platte DataTemplates voor stabiele 60 fps scrollen.

.NET MAUI CollectionView Performance 2026

Bijgewerkt: 18 juli 2026

De performance van CollectionView in .NET MAUI verbeter je door drie dingen samen te doen: virtualisatie inschakelen met de juiste ItemsLayout, DataTemplates simpel houden zodat cell recycling werkt, en compiled bindings met x:DataType gebruiken zodat reflectie uit de renderpaden verdwijnt. Ik heb dit patroon in drie productie-apps toegepast (waaronder een feed met 40.000 items op een low-end Android-toestel) en de scrollprestaties gingen van hakerig naar constant 60 fps. In deze gids leg ik uit waarom deze combinatie werkt en welke valkuilen ik in productie ben tegengekomen.

  • CollectionView gebruikt virtualisatie via de native RecyclerView (Android) en UICollectionView (iOS); zonder juiste configuratie recyclet MAUI cellen niet.
  • Compiled bindings met x:DataType elimineren runtime-reflectie en verlagen de allocaties per cel aanzienlijk in .NET MAUI 10.
  • Geneste layouts binnen een DataTemplate zijn de nummer één oorzaak van scroll-jank; hou de visuele boom zo plat mogelijk (max drie tot vier niveaus diep).
  • ItemsUpdatingScrollMode en RemainingItemsThreshold maken incremental loading mogelijk zonder de complete lijst opnieuw te binden.
  • CollectionView presteert beter dan de verouderde ListView voor moderne workloads; gebruik ListView alleen voor snelle Xamarin.Forms-migraties.
  • Handler-mappings laten je RecyclerView-buffers en UICollectionView-prefetching direct tunen wanneer het standaardgedrag niet volstaat.

Waarom presteert CollectionView traag in productie?

In vrijwel elk MAUI-project waar ik ben binnengekomen om prestatieproblemen op te lossen, komt de oorzaak neer op één van drie dingen: een ItemsSource die geen virtualisatie triggert, een DataTemplate met te veel geneste layouts, of bindings zonder x:DataType waardoor het framework elke property via reflectie moet ophalen. CollectionView gebruikt onder de motorkap een RecyclerView op Android en UICollectionView op iOS, maar die native controls kunnen alleen recyclen als jij ze de kans geeft.

Een concrete productie-case. Een klant had een feed met 8.000 posts. Elk item was een Frame met daarin een Grid, daarin een StackLayout, dan nog een Grid, en pas daar de Labels en Image. Vier lagen diep, met dynamische hoogtes. Op een Samsung A14 (een goede low-end referentie) dropte de scrollrate naar 12 fps. Na afvlakken tot één Grid met vaste RowDefinitions en compiled bindings zaten we constant op 58 tot 60 fps. Er is geen magische vlag hier; het is een optelsom van keuzes.

De hoofdregel die ik hanteer: alles wat per cel opnieuw wordt uitgevoerd (converters, value-lookups, layout-passes) is een prestatiekost die je vermenigvuldigt met het aantal zichtbare cellen. Optimaliseer daar het meest agressief. Als een converter zes keer per cel wordt aangeroepen en je hebt tien cellen zichtbaar, dan draai je zestig keer een stukje logica per frame. Bij een 60 fps target heb je 16 milliseconden per frame; verdwijnt dat in converters, dan zie je de UI schokken.

Hoe werkt virtualisatie in .NET MAUI CollectionView?

Virtualisatie betekent dat de CollectionView slechts een klein aantal cellen materialiseert (meestal net iets meer dan zichtbaar op het scherm) en die cellen recyclet zodra ze uit beeld scrollen. In .NET MAUI 10 gebeurt dit automatisch zolang je de standaard ItemsLayout (LinearItemsLayout of GridItemsLayout) gebruikt en je DataTemplate consistent is per itemtype.

Waar het misgaat: als je ItemsSource geen IList of IReadOnlyList implementeert (bijvoorbeeld een pure IEnumerable), dan valt het framework terug op enumeratie en verlies je random access. Gebruik altijd ObservableCollection<T> of een custom collection die INotifyCollectionChanged en IList combineert.

public partial class FeedViewModel : ObservableObject
{
    // Gebruik ObservableCollection, niet List, om per-item updates te ondersteunen
    // en tegelijk IList random-access aan de virtualiser te bieden.
    public ObservableCollection<PostDto> Posts { get; } = new();

    [RelayCommand]
    private async Task LoadInitialAsync()
    {
        var page = await _feedService.GetPageAsync(0, 50);
        // Batch de toevoeging: bij grote inserts is een AddRange-variant sneller
        // dan 50 losse Add-calls, omdat elk Add een CollectionChanged event vuurt.
        foreach (var post in page) Posts.Add(post);
    }
}

Een subtiel detail. Bij het toevoegen van veel items in een enkele frame veroorzaken vijftig losse CollectionChanged events vijftig layout-passes op iOS. Overweeg een BatchObservableCollection-implementatie die Reset-notificaties gebruikt voor bulk-inserts, of stel de ItemsSource-eigenschap pas één keer in nadat de eerste pagina volledig is opgehaald. Voor MVVM-patronen die netjes met deze virtualisatie samenwerken kun je de CommunityToolkit.Mvvm-patronen met ObservableProperty en RelayCommand gebruiken, die source generators inzetten om reflectie in de bindings te vermijden.

DataTemplate optimalisatie en cell recycling

Cell recycling werkt alleen als CollectionView een DataTemplate kan hergebruiken. Zodra je een DataTemplateSelector gebruikt met vijf verschillende templates, valt de recycle-pool uiteen in vijf kleinere pools, met alle gevolgen voor geheugen en creatiekosten. Ik hou het aantal onderscheiden templates in productie zo goed als altijd onder de drie.

De belangrijkste micro-optimalisaties binnen een DataTemplate:

  • Vervang StackLayout door Grid of VerticalStackLayout/HorizontalStackLayout. StackLayout is een legacy compatibility-shim uit Xamarin.Forms met dure measure-passes.
  • Gebruik Grid met vaste RowDefinitions/ColumnDefinitions in plaats van Auto waar mogelijk. Auto dwingt een double-pass measure af.
  • Verwijder Frame, die is deprecated in .NET MAUI 10. Gebruik Border met een StrokeShape. Dat is aanmerkelijk lichter en werkt met de nieuwe handler-architectuur.
  • Zet InputTransparent="True" op decoratieve labels en images; dat scheelt hittest-berekeningen bij scroll- en tap-events.
  • Vermijd Behaviors per cel als je ze via een centrale event-handler in code-behind sneller kunt oplossen.

Eerlijk gezegd, ik heb dit patroon in drie apps toegepast en de winst zit vaak in het schrappen van één laag, niet in exotische tricks. Twijfel je tussen twee benaderingen? Teken de visuele boom uit op papier en tel de knopen. Meer dan drie diepe layout-containers per cel is een rode vlag.

Compiled bindings met x:DataType inschakelen

Compiled bindings zijn in mijn ervaring de single-beste knop die je in een MAUI-app kunt omzetten. Door x:DataType op je DataTemplate te specificeren, laat je de XAML-compiler bindings omzetten in getypeerde delegates in plaats van reflectie-gebaseerde lookups. Volgens de officiële Microsoft-documentatie over compiled bindings ligt de winst bij lijsten typisch tussen 8 en 20 keer sneller resolven per binding.

<CollectionView ItemsSource="{Binding Posts}">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="models:PostDto">
            <Grid ColumnDefinitions="64,*,Auto" Padding="12,8">
                <Image Source="{Binding AvatarUrl}"
                       HeightRequest="48" WidthRequest="48"
                       Aspect="AspectFill" />
                <VerticalStackLayout Grid.Column="1" Spacing="2">
                    <Label Text="{Binding AuthorName}" FontAttributes="Bold" />
                    <Label Text="{Binding Preview}" MaxLines="2" />
                </VerticalStackLayout>
                <Label Grid.Column="2" Text="{Binding TimeAgo}"
                       TextColor="{StaticResource Gray500}" />
            </Grid>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

Zonder x:DataType weet de compiler niet dat AuthorName een string is; hij genereert een generieke binding met een PropertyChanged-subscription die via reflectie de property waarde ophaalt. Met x:DataType="models:PostDto" wordt het een directe property-access met een typed callback.

Zet daarbij ook XamlCompilation(XamlCompilationOptions.Compile) globaal in je assembly aan (staat sinds .NET MAUI 8 default aan voor projectbestanden, maar controleer wel). En voor grotere projecten loont het om een codegen-analyzer aan te zetten die waarschuwt bij ontbrekende annotaties.

Incremental loading met RemainingItemsThreshold

Voor een oneindige feed is het contraproductief om tienduizend items in één keer in te laden. CollectionView biedt native incremental loading via RemainingItemsThreshold en RemainingItemsThresholdReachedCommand. Zodra er nog N items over zijn onder het viewport wordt jouw command getriggerd, en kun jij de volgende pagina ophalen.

<CollectionView
    ItemsSource="{Binding Posts}"
    RemainingItemsThreshold="10"
    RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}"
    ItemsUpdatingScrollMode="KeepItemsInView" />
[RelayCommand]
private async Task LoadMoreAsync()
{
    // Gate voorkomt dat parallel-triggerende scroll-events meerdere fetches starten.
    if (_isLoading || !_hasMore) return;
    _isLoading = true;
    try
    {
        var next = await _feedService.GetPageAsync(_pageIndex++, PageSize);
        if (next.Count < PageSize) _hasMore = false;
        foreach (var p in next) Posts.Add(p);
    }
    finally { _isLoading = false; }
}

Twee valkuilen die ik keer op keer in code reviews zie:

  1. Het command wordt op elke scroll-event opnieuw uitgevoerd, dus hou een _isLoading gate bij. Zonder gate zie ik apps drie parallelle pagina-fetches doen op één scroll, wat de netwerk-laag en de backend onnodig belast.
  2. ItemsUpdatingScrollMode="KeepScrollOffset" behoudt de exacte scroll positie ook als je items aan het begin van de lijst invoegt. Voor chat- en feed-apps is dit onmisbaar; anders springt de lijst omhoog zodra een nieuw bericht binnenkomt. Voor traditionele lijsten volstaat KeepItemsInView.

Voor het registreren van je feed-service als singleton kun je terugvallen op de patronen uit de gids over .NET MAUI dependency injection en service lifetimes. Een singleton feed-service voorkomt dat elke pagina-navigatie een nieuwe cache initialiseert.

CollectionView vs ListView vs CarouselView: wat kiezen?

Deze vergelijking komt terug in vrijwel elke code review die ik doe. De korte versie: voor nieuwe .NET MAUI apps kies je bijna altijd CollectionView. ListView bestaat vooral nog om Xamarin.Forms-migraties eenvoudiger te maken. CarouselView is voor swipe-experiences, niet voor lange lijsten.

EigenschapCollectionViewListViewCarouselView
VirtualisatieJa (native)Ja (legacy)Ja (peek/loop)
Layout-optiesLinear, Grid, StaggeredAlleen verticaal linearAlleen horizontaal linear
Multi-selectieJaBeperktNee
GroeperenJa, met headers/footersJa, basicNee
Header/FooterJa, per lijstJaJa, per item
Empty-stateIngebouwde EmptyViewHandmatigHandmatig
Incremental loadingJa (RemainingItemsThreshold)BeperktNee
Aanbevolen voor 2026JaAlleen migratieAlleen sliders

Er is één scenario waarin ListView historisch nog won: platte lijsten met alleen tekst en zeer eenvoudige templates, waar CachingStrategy="RecycleElement" iets sneller was dan het CollectionView-equivalent. Sinds .NET MAUI 9 is dat gat gesloten en zie ik geen productie-reden meer om ListView te kiezen. Voor CarouselView geldt: perfect voor een onboarding-flow van drie of vier schermen, maar zodra je verder gaat dan tien items val je terug op scroll-performance die CollectionView beter afhandelt.

Handler-level optimalisatie op Android en iOS

Als de standaard-tunables uitgeput zijn, kun je via het handler-systeem direct aan de native controls draaien. Dit is architect-terrein. Je zet effectief een global tweak in op elke CollectionView in je app. Op Android configureer je de RecyclerView's ItemViewCacheSize en RecycledViewPool; op iOS zet je isPrefetchingEnabled en pas je zo nodig de UICollectionViewFlowLayout aan.

#if ANDROID
Microsoft.Maui.Handlers.CollectionViewHandler.Mapper
    .AppendToMapping("IncreaseCacheSize", (handler, view) =>
    {
        // Meer cellen in de recycler pool = minder inflate op scroll.
        // Standaard is 2; voor beeld-zware cellen is 15-20 realistisch.
        handler.PlatformView.SetItemViewCacheSize(20);
    });
#endif

#if IOS
Microsoft.Maui.Handlers.CollectionViewHandler.Mapper
    .AppendToMapping("EnablePrefetch", (handler, view) =>
    {
        // Prefetching laat iOS vooraf cellen aanmaken vlak buiten het viewport.
        handler.PlatformView.IsPrefetchingEnabled = true;
    });
#endif

Handler-mappings pas je op MauiProgram-niveau toe; ze gelden dan voor elke CollectionView in je app. Voor een complete verhandeling over hoe handlers werken en waar ze zich in de architectuur bevinden, verwijs ik naar de complete gids over .NET MAUI handlers en platform-specifieke integratie. Belangrijk detail. Sinds .NET MAUI 10 zijn een aantal mapper-keys hernoemd, dus check de release notes als je een pre-10 project upgrade.

Profileren en memory leaks opsporen

Optimalisatie zonder metingen is gokken. In productie gebruik ik drie tools naast elkaar: dotnet-trace voor CPU-hotspots in managed code, de .NET MAUI performance-documentatie van Microsoft voor platform-specifieke telemetrie, en de native profilers (Android Studio Profiler, Xcode Instruments) voor rendertijden per frame.

Het meest voorkomende leak-patroon dat ik in CollectionView-gebruik zie: een DataTemplate met een MessagingCenter-subscriber die niet wordt losgemaakt bij unloading. Elke cel die door de recycler stroomt registreert een nieuwe subscription, en de count blijft groeien tot de app crasht. Vermijd MessagingCenter (die is deprecated sinds .NET MAUI 8) en verhuis naar de WeakReferenceMessenger uit de CommunityToolkit.Mvvm messaging-primitieven, die interne referenties weak houdt en dus automatisch opruimt als de cel wordt gerecycled.

// Meten van frame drops op Android via adb:
// adb shell dumpsys gfxinfo com.example.app framestats

// Meten in-app via een simpele draw-listener op de handler:
handler.PlatformView.ViewTreeObserver.AddOnDrawListener(
    new DrawListener(elapsed =>
    {
        // 16ms = 60fps budget; alles daarboven is een dropped frame.
        if (elapsed > 16) Log.Debug("MAUI", $"Frame drop: {elapsed}ms");
    }));

Een korte checklist die ik vóór een release doorloop: geen jank op scrollrate 300 dp/s, geheugen stabiel over 5 minuten scrollen, geen allocaties boven 1 KB in de rendering-loop, en tenminste 55 fps gemiddeld op een low-end Android referentietoestel. Dat is het minimum voor een consumenten-app in 2026. Als je hier niet doorheen komt, is de aanname dat er nog een van de drie hoofdoorzaken uit sectie één actief is. Ga dan terug en meet opnieuw.

Veelgestelde vragen

Waarom is CollectionView in .NET MAUI soms trager dan de oude Xamarin.Forms ListView?

Meestal komt dat door ontbrekende compiled bindings (geen x:DataType) of een DataTemplate met te veel geneste layouts. Xamarin.Forms had een agressievere caching-strategie standaard aan; MAUI vertrouwt op correct opgezette virtualisatie. Zodra je bindings compileert en de visuele boom platslaat, is CollectionView in vrijwel alle gevallen sneller.

Ondersteunt CollectionView virtualisatie standaard?

Ja. Zowel LinearItemsLayout als GridItemsLayout gebruiken native virtualisatie via RecyclerView (Android) en UICollectionView (iOS). Voorwaarde is dat je ItemsSource een indexable collection is zoals ObservableCollection<T> of List<T>, en niet een pure IEnumerable.

Hoe schakel ik compiled bindings in voor bestaande XAML?

Voeg x:DataType="namespace:TypeName" toe aan elk DataTemplate en aan pages waar je een BindingContext gebruikt. In .NET MAUI 10 kun je bovendien <MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation> in je csproj zetten om waarschuwingen te krijgen bij ontbrekende annotaties.

Wat is een goede pagina-grootte voor incremental loading?

Voor tekst-lijsten werkt 30 tot 50 items per pagina in mijn ervaring goed. Voor image-heavy feeds is 15 tot 25 gebruikelijker om de netwerk-latency en decodering te spreiden. Zet RemainingItemsThreshold op ongeveer 30% van je pagina-grootte, zodat de volgende pagina binnen is voordat de gebruiker de bodem raakt.

Moet ik overstappen van ListView naar CollectionView bij een Xamarin.Forms-migratie?

Voor een korte-termijn migratie kun je ListView tijdelijk laten staan; hij werkt in .NET MAUI. Op middellange termijn is CollectionView de betere keuze omdat hij actief onderhouden wordt, meer layout-opties biedt en met compiled bindings efficiënter is. Ik plan zulke migraties meestal per scherm in, niet in één big bang.

Marcus Chen
Over de Auteur Marcus Chen

Senior mobile architect with a decade of cross-platform experience. Spent the last five years going deep on .NET MAUI in production.