Why is Xamarin.Forms so slow when displaying a few labels (especially on Android)?

We are trying to release some productive Apps with Xamarin.Forms but one of our main issues is the overall slowness between button pressing and displaying of content. After a few experiments, we discovered that even a simple ContentPage with 40 labels take more than 100 ms to show up:

public static class App
{
    public static DateTime StartTime;

    public static Page GetMainPage()
    {    
        return new NavigationPage(new StartPage());
    }
}

public class StartPage : ContentPage
{
    public StartPage()
    {
        Content = new Button {
            Text = "Start",
            Command = new Command(o => {
                App.StartTime = DateTime.Now;
                Navigation.PushAsync(new StopPage());
            }),
        };
    }
}

public class StopPage : ContentPage
{
    public StopPage()
    {
        Content = new StackLayout();
        for (var i = 0; i < 40; i++)
            (Content as StackLayout).Children.Add(new Label{ Text = "Label " + i });
    }

    protected override void OnAppearing()
    {
        ((Content as StackLayout).Children[0] as Label).Text = "Stop after " + (DateTime.Now - App.StartTime).TotalMilliseconds + " ms";

        base.OnAppearing();
    }
}

Especially on Android it get’s worse the more labels you’re trying to display. The first button press (which is crucial for the user) even takes ~300 ms. We need to show something on the screen in less than 30 ms to create a good user experience.

  • RecyclerView scrollToPosition not trigger scrollListener
  • How to resolve the “ADB server didn't ACK” error?
  • Xamarin C# - Android - Prevent an AlertDialog from closing on PositiveButton click
  • OpenCV in Android Studio
  • Android keyboard next button issue on EditText
  • ImageView src with drawable selector ignores enabled state
  • Why does it take so long with Xamarin.Forms to display some simple labels? And how to work around this issue to create a shippable App?

    Experiments

    The code can be forked on GitHub at https://github.com/perpetual-mobile/XFormsPerformance

    I’ve also written a small example to demonstrate that similar code utilizing the native APIs from Xamarin.Android is significantly faster and does not get slower when adding more content: https://github.com/perpetual-mobile/XFormsPerformance/tree/android-native-api

  • How to hide Android soft keyboard on EditText
  • Disable scrolling of a ListView contained within a ScrollView
  • How to set ImageButton property of app:srcCompat=“@drawable/pic” programmatically?
  • Android emulator not showing the app- it only shows the skin
  • Android - How do i get sharedpreferences from another activity?
  • onCreate() after finish() in onStop()
  • 3 Solutions collect form web for “Why is Xamarin.Forms so slow when displaying a few labels (especially on Android)?”

    Xamarin Support Team wrote me:

    The team is aware of the issue, and they are working on optimising the
    UI initialisation code. You may see some improvements in upcoming
    releases.

    Update: after seven month of idling, Xamarin changed the bug report status to ‘CONFIRMED’.

    Good to know. So we have to be patient. Fortunately Sean McKay over in Xamarin Forums suggested to override all layouting code to improve performance: https://forums.xamarin.com/discussion/comment/87393#Comment_87393

    But his suggestion also means that we have to write the complete label code again. Here is an version of a FixedLabel which does not do the costly layout cycles and has a some features like bindable properties for text and color. Using this instead of Label improves performance by 80% and more depending on the number of labels and where they occur.

    public class FixedLabel : View
    {
        public static readonly BindableProperty TextProperty = BindableProperty.Create<FixedLabel,string>(p => p.Text, "");
    
        public static readonly BindableProperty TextColorProperty = BindableProperty.Create<FixedLabel,Color>(p => p.TextColor, Style.TextColor);
    
        public readonly double FixedWidth;
    
        public readonly double FixedHeight;
    
        public Font Font;
    
        public LineBreakMode LineBreakMode = LineBreakMode.WordWrap;
    
        public TextAlignment XAlign;
    
        public TextAlignment YAlign;
    
        public FixedLabel(string text, double width, double height)
        {
            SetValue(TextProperty, text);
            FixedWidth = width;
            FixedHeight = height;
        }
    
        public Color TextColor {
            get {
                return (Color)GetValue(TextColorProperty);
            }
            set {
                if (TextColor != value)
                    return;
                SetValue(TextColorProperty, value);
                OnPropertyChanged("TextColor");
            }
        }
    
        public string Text {
            get {
                return (string)GetValue(TextProperty);
            }
            set {
                if (Text != value)
                    return;
                SetValue(TextProperty, value);
                OnPropertyChanged("Text");
            }
        }
    
        protected override SizeRequest OnSizeRequest(double widthConstraint, double heightConstraint)
        {
            return new SizeRequest(new Size(FixedWidth, FixedHeight));
        }
    }
    

    The Android Renderer looks like this:

    public class FixedLabelRenderer : ViewRenderer
    {
        public TextView TextView;
    
        protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.View> e)
        {
            base.OnElementChanged(e);
    
            var label = Element as FixedLabel;
            TextView = new TextView(Context);
            TextView.Text = label.Text;
            TextView.TextSize = (float)label.Font.FontSize;
            TextView.Gravity = ConvertXAlignment(label.XAlign) | ConvertYAlignment(label.YAlign);
            TextView.SetSingleLine(label.LineBreakMode != LineBreakMode.WordWrap);
            if (label.LineBreakMode == LineBreakMode.TailTruncation)
                TextView.Ellipsize = Android.Text.TextUtils.TruncateAt.End;
    
            SetNativeControl(TextView);
        }
    
        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Text")
                TextView.Text = (Element as FixedLabel).Text;
    
            base.OnElementPropertyChanged(sender, e);
        }
    
        static GravityFlags ConvertXAlignment(Xamarin.Forms.TextAlignment xAlign)
        {
            switch (xAlign) {
                case Xamarin.Forms.TextAlignment.Center:
                    return GravityFlags.CenterHorizontal;
                case Xamarin.Forms.TextAlignment.End:
                    return GravityFlags.End;
                default:
                    return GravityFlags.Start;
            }
        }
    
        static GravityFlags ConvertYAlignment(Xamarin.Forms.TextAlignment yAlign)
        {
            switch (yAlign) {
                case Xamarin.Forms.TextAlignment.Center:
                    return GravityFlags.CenterVertical;
                case Xamarin.Forms.TextAlignment.End:
                    return GravityFlags.Bottom;
                default:
                    return GravityFlags.Top;
            }
        }
    }
    

    And here the iOS Render:

    public class FixedLabelRenderer : ViewRenderer<FixedLabel, UILabel>
    {
        protected override void OnElementChanged(ElementChangedEventArgs<FixedLabel> e)
        {
            base.OnElementChanged(e);
    
            SetNativeControl(new UILabel(RectangleF.Empty) {
                BackgroundColor = Element.BackgroundColor.ToUIColor(),
                AttributedText = ((FormattedString)Element.Text).ToAttributed(Element.Font, Element.TextColor),
                LineBreakMode = ConvertLineBreakMode(Element.LineBreakMode),
                TextAlignment = ConvertAlignment(Element.XAlign),
                Lines = 0,
            });
    
            BackgroundColor = Element.BackgroundColor.ToUIColor();
        }
    
        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Text")
                Control.AttributedText = ((FormattedString)Element.Text).ToAttributed(Element.Font, Element.TextColor);
    
            base.OnElementPropertyChanged(sender, e);
        }
    
        // copied from iOS LabelRenderer
        public override void LayoutSubviews()
        {
            base.LayoutSubviews();
            if (Control == null)
                return;
            Control.SizeToFit();
            var num = Math.Min(Bounds.Height, Control.Bounds.Height);
            var y = 0f;
            switch (Element.YAlign) {
                case TextAlignment.Start:
                    y = 0;
                    break;
                case TextAlignment.Center:
                    y = (float)(Element.FixedHeight / 2 - (double)(num / 2));
                    break;
                case TextAlignment.End:
                    y = (float)(Element.FixedHeight - (double)num);
                    break;
            }
            Control.Frame = new RectangleF(0, y, (float)Element.FixedWidth, num);
        }
    
        static UILineBreakMode ConvertLineBreakMode(LineBreakMode lineBreakMode)
        {
            switch (lineBreakMode) {
                case LineBreakMode.TailTruncation:
                    return UILineBreakMode.TailTruncation;
                case LineBreakMode.WordWrap:
                    return UILineBreakMode.WordWrap;
                default:
                    return UILineBreakMode.Clip;
            }
        }
    
        static UITextAlignment ConvertAlignment(TextAlignment xAlign)
        {
            switch (xAlign) {
                case TextAlignment.Start:
                    return UITextAlignment.Left;
                case TextAlignment.End:
                    return UITextAlignment.Right;
                default:
                    return UITextAlignment.Center;
            }
        }
    }
    

    What you are measuring here is the sum of:

    • the time it takes to create the page
    • to layout it
    • to animate it on screen

    On a nexus5, I observe times in the magnitude of 300ms for the first call, and of 120ms for subsequent ones.

    This is because the OnAppearing() will only be invoked when the view is fully animated in place.

    You can easily measure the animation time by replacing your app with:

    public class StopPage : ContentPage
    {
        public StopPage()
        {
        }
    
        protected override void OnAppearing()
        {
            System.Diagnostics.Debug.WriteLine ((DateTime.Now - App.StartTime).TotalMilliseconds + " ms");        
            base.OnAppearing();
        }
    }
    

    and I observe times like:

    134.045 ms
    2.796 ms
    3.554 ms
    

    This gives some insights:
    – there’s no animation on PushAsync on android (there’s on iPhone, taking 500ms)
    – the first time you push the page, you pay a 120ms tax, due to a new allocation.
    – XF is doing a good job at reusing page renderers if possible

    What we’re interested into is the time for displaying the 40 labels, nothing else. Let’s change the code again:

    public class StopPage : ContentPage
    {
        public StopPage()
        {
        }
    
        protected override void OnAppearing()
        {
            App.StartTime = DateTime.Now;
    
            Content = new StackLayout();
            for (var i = 0; i < 40; i++)
                (Content as StackLayout).Children.Add(new Label{ Text = "Label " + i });
    
            System.Diagnostics.Debug.WriteLine ((DateTime.Now - App.StartTime).TotalMilliseconds + " ms");
    
            base.OnAppearing();
        }
    }
    

    times observed (on 3 calls):

    264.015 ms
    186.772 ms
    189.965 ms
    188.696 ms
    

    That’s still a bit too much, but as the ContentView is set first, this is measuring 40 layout cycles, as each new Label is redrawing the screen. Let’s change that:

    public class StopPage : ContentPage
    {
        public StopPage()
        {
        }
    
        protected override void OnAppearing()
        {
            App.StartTime = DateTime.Now;
    
            var layout = new StackLayout();
            for (var i = 0; i < 40; i++)
                layout.Children.Add(new Label{ Text = "Label " + i });
    
            Content = layout;
            System.Diagnostics.Debug.WriteLine ((DateTime.Now - App.StartTime).TotalMilliseconds + " ms");
    
            base.OnAppearing();
        }
    }
    

    And here are my measurements:

    178.685 ms
    110.221 ms
    117.832 ms
    117.072 ms
    

    This is becoming very reasonable, esp. given that you’re drawing (instantiating, and measuring) 40 labels when your screen can only display 20.

    There’s indeed yet some room for improvement, but the situation is not as bad as it seems. The 30ms rule for mobile says that everything that takes more than 30ms should be async and not block the UI. Here it takes a bit more than 30ms to switch a page, but from a user point of view, I don’t perceive this as slow.

    In the setters of the Text and TextColor properties of the FixedLabel class, the code says:

    If the new value is “different” from the current one, then do nothing!

    It should be with the opposite condition, so that if the new value is the same as the current one, then there is nothing to do.

    Android Babe is a Google Android Fan, All about Android Phones, Android Wear, Android Dev and Android Games Apps and so on.