diff --git a/src/Controls/tests/TestCases.HostApp/Issues/RenderingPerformance.xaml b/src/Controls/tests/TestCases.HostApp/Issues/RenderingPerformance.xaml
new file mode 100644
index 000000000000..840ebbe2d990
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/RenderingPerformance.xaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/RenderingPerformance.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/RenderingPerformance.xaml.cs
new file mode 100644
index 000000000000..c37b918549ec
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/RenderingPerformance.xaml.cs
@@ -0,0 +1,180 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Globalization;
+using Microsoft.Maui;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+using ILayout = Microsoft.Maui.ILayout;
+
+namespace Maui.Controls.Sample.Issues;
+
+public class MeasuredLabel : Label
+{
+ private static readonly TimeSpan ArrangedThreshold = TimeSpan.FromSeconds(1);
+ public static readonly BindableProperty IsMeasuredProperty = BindableProperty.Create(nameof(IsMeasured), typeof(bool), typeof(MeasuredLabel), false);
+
+ public bool IsMeasured
+ {
+ get => (bool)GetValue(IsMeasuredProperty);
+ set => SetValue(IsMeasuredProperty, value);
+ }
+
+ public long? LastArrangedTicks { get; set; }
+
+ public long? GetArrangeTicks() {
+ if (LastArrangedTicks is { } ticks)
+ {
+ var elapsed = Stopwatch.GetElapsedTime(ticks);
+ if (elapsed > ArrangedThreshold)
+ {
+ return ticks;
+ }
+ }
+
+ return null;
+ }
+}
+
+public static class RenderingPerformanceExtensions
+{
+ public static MauiAppBuilder RenderingPerformanceAddMappers(this MauiAppBuilder builder)
+ {
+ builder.ConfigureMauiHandlers(handlers =>
+ {
+ Microsoft.Maui.Handlers.LabelHandler.CommandMapper.AppendToMapping(nameof(IView.Frame), (handler, view, arg) =>
+ {
+ if (view is MeasuredLabel { IsMeasured: true } measuredLabel)
+ {
+ measuredLabel.LastArrangedTicks = Stopwatch.GetTimestamp();
+ }
+ });
+ });
+
+ return builder;
+ }
+}
+
+[XamlCompilation(XamlCompilationOptions.Compile)]
+[Issue(IssueTracker.None, 0, "Rendering performance", PlatformAffected.All)]
+public partial class RenderingPerformance : ContentPage
+{
+ public List Models { get; set; }
+
+ public RenderingPerformance()
+ {
+ Models = GenerateMeasuredItem();
+ BindingContext = this;
+ InitializeComponent();
+ }
+
+ private async void ButtonClicked(object sender, EventArgs e)
+ {
+ var capturedTimes = new List();
+
+ var test1Models = GenerateItems(50, "Test1");
+ var test2Models = GenerateItems(25, "Test2");
+ var resetModel = GenerateMeasuredItem();
+
+ await GetArrangeTicksAsync();
+
+ for (var i = 0; i < 6; i++)
+ {
+ await Task.Delay(200);
+
+ Models = test1Models;
+ var startTicks = Stopwatch.GetTimestamp();
+ OnPropertyChanged(nameof(Models));
+ var endTicks = await Task.Run(GetArrangeTicksAsync);
+ var t1 = (int)Stopwatch.GetElapsedTime(startTicks, endTicks).TotalMilliseconds;
+
+ await Task.Delay(200);
+
+ Models = test2Models;
+ startTicks = Stopwatch.GetTimestamp();
+ OnPropertyChanged(nameof(Models));
+ endTicks = await Task.Run(GetArrangeTicksAsync);
+ var t2 = (int)Stopwatch.GetElapsedTime(startTicks, endTicks).TotalMilliseconds;
+
+ await Task.Delay(200);
+
+ startTicks = Stopwatch.GetTimestamp();
+ BindableContainer.Clear();
+ Models = resetModel;
+ OnPropertyChanged(nameof(Models));
+ endTicks = await Task.Run(GetArrangeTicksAsync);
+ var t3 = (int)Stopwatch.GetElapsedTime(startTicks, endTicks).TotalMilliseconds;
+
+ capturedTimes.Add([t1, t2, t3]);
+ }
+
+ var avg1 = (int)capturedTimes.Average(t => t[0]);
+ var avg2 = (int)capturedTimes.Average(t => t[1]);
+ var avg3 = (int)capturedTimes.Average(t => t[2]);
+ StartButton.Text = $"{avg1},{avg2},{avg3}";
+ }
+
+ async Task GetArrangeTicksAsync()
+ {
+ while (true)
+ {
+ await Task.Delay(100);
+ IView view = BindableContainer;
+ while (view is ILayout { Count: > 0 } layout)
+ {
+ view = layout[^1];
+ }
+
+ if (view is MeasuredLabel measuredLabel && measuredLabel.GetArrangeTicks() is { } arrangeTicks)
+ {
+ measuredLabel.LastArrangedTicks = null;
+ return arrangeTicks;
+ }
+ }
+ }
+
+ static List GenerateItems(int count, string prefix)
+ {
+ return
+ [
+ ..Enumerable.Range(0, count).Select(i => new Model
+ {
+ Content = $"{prefix} Content {i}",
+ Header = $"Header {i}",
+ SubModels = Enumerable.Range(0, 10).Select(j => new SubModel
+ {
+ Content = $"{prefix} SubContent {j}", Header = $"{prefix} SubHeader {j}"
+ }).ToArray()
+ }),
+ ..GenerateMeasuredItem()
+ ];
+ }
+
+ static List GenerateMeasuredItem()
+ {
+ return
+ [
+ new Model
+ {
+ Content = "Measured",
+ Header = "Measured",
+ SubModels =
+ [
+ new SubModel { Content = "Measured", Header = "Measured", IsMeasured = true }
+ ]
+ }
+ ];
+ }
+
+ public class Model : SubModel
+ {
+ public SubModel[] SubModels { get; set; }
+ }
+
+ public class SubModel
+ {
+ public string Header { get; set; }
+ public string Content { get; set; }
+ public bool IsMeasured { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/MauiProgram.cs b/src/Controls/tests/TestCases.HostApp/MauiProgram.cs
index 1f2720c45fde..8d1c8bb8d3a8 100644
--- a/src/Controls/tests/TestCases.HostApp/MauiProgram.cs
+++ b/src/Controls/tests/TestCases.HostApp/MauiProgram.cs
@@ -23,6 +23,7 @@ public static MauiApp CreateMauiApp()
fonts.AddFont("FontAwesome.ttf", "FA");
fonts.AddFont("ionicons.ttf", "Ion");
})
+ .RenderingPerformanceAddMappers()
.Issue21109AddMappers()
.Issue18720AddMappers()
.Issue18720EditorAddMappers()
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/RenderingPerformance.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/RenderingPerformance.cs
new file mode 100644
index 000000000000..83d8ba28a4b3
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/RenderingPerformance.cs
@@ -0,0 +1,38 @@
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues
+{
+ public class RenderingPerformance : _IssuesUITest
+ {
+ public RenderingPerformance(TestDevice device) : base(device) { }
+
+ public override string Issue => "Rendering performance";
+
+ [Test]
+ [Category(UITestCategories.Performance)]
+ public async Task RenderingPerformanceRun()
+ {
+ var button = App.WaitForElement("StartButton");
+ App.Tap("StartButton");
+
+ string? text;
+ while (true)
+ {
+ text = button.GetText();
+ if (string.IsNullOrEmpty(text) || text == "Start")
+ {
+ await Task.Delay(1000);
+ continue;
+ }
+
+ break;
+ }
+
+ var times = text.Split(',');
+
+ TestContext.WriteLine(@$"RenderingPerformance: [{times[0]}, {times[1]}, {times[2]}]");
+ }
+ }
+}