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.
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
Limited Access To Open Source Libraries. Xamarin requires the use of elements provided by the platform, as well as . NET open source libraries. As you would expect, the choice is not as rich as it is for iOS and Android native development, and you might end up with a lot of native coding.
Startup Tracing If you go into the Android Options of your Xamarin projects, you'll see this: If you enable the AOT Compilation (Ahead of time), your app performances will improve significantly and so the startup time, but your apk size will increase as well.
Generally, the main distinction between the two platforms is that Xamarin. Forms allows reusing the same UI code for multiple OS, whereas Xamarin Native is adapted to APIs, specific to a specific platform – Windows, iOS, Android.
Xamarin allows you to create flawless experiences using platform-specific UI elements. It's also possible to build cross-platform apps for iOS, Android, or Windows using Xamarin. Forms tool, which converts app UI components into the platform-specific interface elements at runtime.
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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With