dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.26k stars 1.76k forks source link

[Performance issue] Creation and display of ContentView takes too long when displayed for the first time #25754

Open MAUIoxo opened 2 weeks ago

MAUIoxo commented 2 weeks ago

Description

I have a ContentView in XAML that is displayed after switching a tab. The ContentView is not too complicated in my opinion:

<Grid VerticalOptions="StartAndExpand">

    <!-- Can't use the BackgroundImageView here since there is a Bug that draws the image too big there is a gray stipe at the bottom -->
    <Image x:Name="BackgroundImage" Source="background_image.jpg" VerticalOptions="StartAndExpand" HorizontalOptions="FillAndExpand" Aspect="AspectFill" Margin="{OnPlatform Android='0, -40, 0, 0', Default='0'}"/>

    <Border x:Name="OverviewBorder" Grid.Row="0" Style="{StaticResource BorderStyleView}" VerticalOptions="StartAndExpand">

        <Grid RowDefinitions="Auto, *">

            <!--Search...-->
            <SearchBar Grid.Row="0" ios:SearchBar.SearchBarStyle="Minimal" Text="{Binding SearchText}" Placeholder="Search Text ..." />

            <!-- CollectionView: Margin to get the ScrollBar to the right and closer to the Border -->
            <dx:DXCollectionView Grid.Row="1" ItemsSource="{Binding AvailableItems, Mode=TwoWay}" AllowGroupCollapse="False" FilterExpression="{Binding SearchFilterExpression}" Margin="10, 5, 10, 15">

                <!-- Group Description: FieldName used to group the items and Order in which they are grouped -->
                <dx:DXCollectionView.GroupDescription>
                    <dx:GroupDescription FieldName="Name" GroupInterval="Alphabetical"/>
                </dx:DXCollectionView.GroupDescription>

                <!-- Group Header: to group items -->
                <dx:DXCollectionView.GroupHeaderTemplate>
                    <DataTemplate>
                        <VerticalStackLayout>
                            <Label Text="{Binding Value, StringFormat=' {0}'}" Style="{StaticResource GroupHeaderStyle}" />
                            <BoxView Style="{StaticResource GroupHeaderUnderlineStyle}"/>
                        </VerticalStackLayout>
                    </DataTemplate>
                </dx:DXCollectionView.GroupHeaderTemplate>

                <!-- Group Items -->
                <dx:DXCollectionView.ItemTemplate>
                    <DataTemplate x:DataType="{x:Type databaseModels:Food}">

                        <controls:CustomSwipeView SwipeViewOpenFlag="{Binding SwipeViewOpenFlag, Source={RelativeSource AncestorType={x:Type viewModels:OverviewViewModel}}, Mode=TwoWay}">

                            <Border Padding="20, 15, 20, 10" Style="{StaticResource BorderRoundedCornerStyle}">
                                <VerticalStackLayout x:DataType="databaseModels:Food">

                                    <!-- Name -->
                                    <Label Text="{Binding Name}" FontSize="13" FontAttributes="Bold" />

                                    <!-- Brand -->
                                    <Label Text="{Binding Brand}" FontSize="12" FontAttributes="Bold" TextColor="{StaticResource Gray500}" Margin="0, 5, 0, 0"/>

                                    <!-- Bottom Row for values -->
                                    <HorizontalStackLayout Margin="0, 15, 0, 0">
                                        <HorizontalStackLayout HorizontalOptions="StartAndExpand">
                                            <Image Source="dotnet_bot.png" HeightRequest="12" WidthRequest="12" Margin="0, 1, 5, 5"/>
                                            <Label Text="Value 1:" Style="{StaticResource SelectionDetailsLabelStyle}" />
                                            <labelControls:CultureBasedNumberLabel Text="{Binding Fats, Mode=OneWay, StringFormat='  {0:0.###}'}" Style="{StaticResource SelectionDetailsValueStyle}" />
                                        </HorizontalStackLayout>

                                        <HorizontalStackLayout HorizontalOptions="CenterAndExpand" Margin="5, 0, 0, 0">
                                            <Image Source="dotnet_bot.png" HeightRequest="20" WidthRequest="20" Margin="0, -2, 0, 0" />
                                            <Label Text="Value 2:" Style="{StaticResource SelectionDetailsLabelStyle}"/>
                                            <labelControls:CultureBasedNumberLabel Text="{Binding Carbohydrates, Mode=OneWay, StringFormat='  {0:0.###}'}" Style="{StaticResource SelectionDetailsValueStyle}" />
                                        </HorizontalStackLayout>

                                        <HorizontalStackLayout HorizontalOptions="EndAndExpand" Margin="5, 0, 0, 0">
                                            <Image Source="dotnet_bot.png" HeightRequest="16" WidthRequest="16" Margin="5, -1, 3, 2" />
                                            <Label Text="Value 3:" Style="{StaticResource SelectionDetailsLabelStyle}"/>
                                            <labelControls:CultureBasedNumberLabel Text="{Binding Proteins, Mode=OneWay, StringFormat='  {0:0.###}'}" Style="{StaticResource SelectionDetailsValueStyle}" />
                                        </HorizontalStackLayout>
                                    </HorizontalStackLayout>

                                </VerticalStackLayout>
                            </Border>

                            <SwipeView.RightItems>
                                <SwipeItems>
                                    <SwipeItemView>

                                        <Grid RowDefinitions="*" ColumnDefinitions="*, *" WidthRequest="160" HeightRequest="100" ColumnSpacing="0">
                                            <Label Grid.Column="0" Text="Edit" BackgroundColor="LightGrey" Style="{StaticResource SwipeItemStyle}">
                                                <Label.GestureRecognizers>
                                                    <TapGestureRecognizer Command="{Binding Source={RelativeSource AncestorType={x:Type viewModels:OverviewViewModel}}, Path=EditFoodItemCommand}" CommandParameter="{Binding .}"/>
                                                </Label.GestureRecognizers>
                                            </Label>

                                            <Label Grid.Column="1" Text="Delete" BackgroundColor="{StaticResource DarkOrange1}" TextColor="White" Style="{StaticResource SwipeItemStyle}">
                                                <Label.GestureRecognizers>
                                                    <TapGestureRecognizer Command="{Binding Source={RelativeSource AncestorType={x:Type viewModels:OverviewViewModel}}, Path=DeleteFoodItemCommand}" CommandParameter="{Binding .}"/>
                                                </Label.GestureRecognizers>
                                            </Label>
                                        </Grid>

                                    </SwipeItemView>
                                </SwipeItems>
                            </SwipeView.RightItems>

                        </controls:CustomSwipeView>

                    </DataTemplate>
                </dx:DXCollectionView.ItemTemplate>

            </dx:DXCollectionView>

        </Grid>

    </Border>

</Grid>



Nevertheless, the creation of this ContentView when displayed for the first time takes too long so that there is a bad user experience which is like having a lagging UI:


The example uses Sharpnado.Tabs to switch between the Views. This component allows 3 different kinds of Views, a LazyView, DelayedView (which shows an activity indicator while loading the View) and also a normal .NET MAUI View. I tried all of them and they show the same behavior. LazyView and normal .NET MAUI View feel like they are laggy since the View does not show up instantly after clicking the Tab and they show up with a delay with is too long and not acceptable in my opinion. Even on my local iPhone 14 Pro Max it is laggy after clicking.

The DelayedView switches to the View faster and at least shows the background and some things that show the user that her click was performed. But this View also displays an ActivityIndicator that runs until the View is fully loaded to bridge the wait time for the user and make this delay more acceptable. However, if only 2-3 elements are displayed afterwards in the CollectionView and not hundreds of elements, it still feels strange to the customer why they see a spinning hourglass for only so few items. The question remains, though, why the creation of the View has to take so long when displayed for the first time. For me, this is a fundamental issue in .NET MAUI.

After it was displayed for the first time it is possible to fluently switch back and forth between the tabs:




Steps to Reproduce

  1. The attached minimal reproducible example uses a "DevExpress" DXCollectionView as the performance is usually better
  2. To reproduce the example, you can Obtain a Personal NuGet Feed URL here
  3. Start the App of the attached Project
  4. Click on "Tab 2" and see the user experience after you've clicked the tab
  5. Switch between "Tab 1" and "Tab 2" back and forth and see that the user experience is as it should be
  6. Bottom line is: the first creation and display of a ContentView like that takes too long and is a performance issue

Link to public reproduction project repository

DisplayViewDelay_25754

Version with bug

8.0.92 SR9.2

Is this a regression from previous behavior?

Not sure, did not test other versions

Last version that worked well

Unknown/Other

Affected platforms

iOS, Android

Affected platform versions

iPhone 15 Pro Max iOS 17.4

Did you find any workaround?

No workaround so far

Relevant log output

mattleibow commented 1 week ago

One thing that might help is if you ran a profiler on the app and attach a speedscope: https://github.com/dotnet/maui/wiki/Profiling-.NET-MAUI-Apps

Maybe something will appear like loading some data/image in the constructor or another part of the binding. Maybe there is a lay loading something from the DB in a binding.

MAUIoxo commented 1 week ago

I wanted to do that and ran Time Profiler on XCode connected to my iPhone and I launched the App. After that started the recording when I switched to the second tab. The Time Profile showed a "Hang". I wanted to store that as a speedscope file, but to be honest, I don't know how to do that. When I click on XCode > Time Profile > File > Save As > my_profile.trace I get a folder structure named my_profile.trace When I Drag&Drop that entire folder to the speedscope website, I get an error "Something went wrong. Check the JS console for more details.". How can I convert that? (I am using XCode Version 16.0)