Skip rerendering child components if their params are definitely unchanged
This commit is contained in:
parent
08d7b77d38
commit
25b76bc6dc
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using Microsoft.AspNetCore.Blazor.RenderTree;
|
using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Blazor.Components
|
namespace Microsoft.AspNetCore.Blazor.Components
|
||||||
|
|
@ -39,5 +40,99 @@ namespace Microsoft.AspNetCore.Blazor.Components
|
||||||
/// <returns>The enumerator.</returns>
|
/// <returns>The enumerator.</returns>
|
||||||
public ParameterEnumerator GetEnumerator()
|
public ParameterEnumerator GetEnumerator()
|
||||||
=> new ParameterEnumerator(_frames, _ownerIndex);
|
=> new ParameterEnumerator(_frames, _ownerIndex);
|
||||||
|
|
||||||
|
// It's internal because there isn't a known use case for user code comparing
|
||||||
|
// ParameterCollection instances, and even if there was, it's unlikely it should
|
||||||
|
// use these equality rules which are designed for their effect on rendering.
|
||||||
|
internal bool DefinitelyEquals(ParameterCollection oldParameters)
|
||||||
|
{
|
||||||
|
// In general we can't detect mutations on arbitrary objects. We can't trust
|
||||||
|
// things like .Equals or .GetHashCode because they usually only tell us about
|
||||||
|
// shallow changes, not deep mutations. So we return false if both:
|
||||||
|
// [1] All the parameters are known to be immutable (i.e., Type.IsPrimitive
|
||||||
|
// or is in a known set of common immutable types)
|
||||||
|
// [2] And all the parameter values are equal to their previous values
|
||||||
|
// Otherwise be conservative and return false.
|
||||||
|
// To make this check cheaper, since parameters are virtually always generated in
|
||||||
|
// a deterministic order, we don't bother to account for reordering, so if any
|
||||||
|
// of the names don't match sequentially we just return false too.
|
||||||
|
//
|
||||||
|
// The logic here may look kind of epic, and would certainly be simpler if we
|
||||||
|
// used ParameterEnumerator.GetEnumerator(), but it's perf-critical and this
|
||||||
|
// implementation requires a lot fewer instructions than a GetEnumerator-based one.
|
||||||
|
|
||||||
|
var oldIndex = oldParameters._ownerIndex;
|
||||||
|
var newIndex = _ownerIndex;
|
||||||
|
var oldEndIndexExcl = oldIndex + oldParameters._frames[oldIndex].ComponentSubtreeLength;
|
||||||
|
var newEndIndexExcl = newIndex + _frames[newIndex].ComponentSubtreeLength;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
// First, stop if we've reached the end of either subtree
|
||||||
|
oldIndex++;
|
||||||
|
newIndex++;
|
||||||
|
var oldFinished = oldIndex == oldEndIndexExcl;
|
||||||
|
var newFinished = newIndex == newEndIndexExcl;
|
||||||
|
if (oldFinished || newFinished)
|
||||||
|
{
|
||||||
|
return oldFinished == newFinished; // Same only if we have same number of parameters
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Since neither subtree has finished, it's safe to read the next frame from both
|
||||||
|
ref var oldFrame = ref oldParameters._frames[oldIndex];
|
||||||
|
ref var newFrame = ref _frames[newIndex];
|
||||||
|
|
||||||
|
// Stop if we've reached the end of either subtree's sequence of attributes
|
||||||
|
oldFinished = oldFrame.FrameType != RenderTreeFrameType.Attribute;
|
||||||
|
newFinished = newFrame.FrameType != RenderTreeFrameType.Attribute;
|
||||||
|
if (oldFinished || newFinished)
|
||||||
|
{
|
||||||
|
return oldFinished == newFinished; // Same only if we have same number of parameters
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!string.Equals(oldFrame.AttributeName, newFrame.AttributeName, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false; // Different names
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldValue = oldFrame.AttributeValue;
|
||||||
|
var newValue = newFrame.AttributeValue;
|
||||||
|
var oldIsNotNull = oldValue != null;
|
||||||
|
var newIsNotNull = newValue != null;
|
||||||
|
if (oldIsNotNull != newIsNotNull)
|
||||||
|
{
|
||||||
|
return false; // One's null and the other isn't, so different
|
||||||
|
}
|
||||||
|
else if (oldIsNotNull) // i.e., both are not null (considering previous check)
|
||||||
|
{
|
||||||
|
var oldValueType = oldValue.GetType();
|
||||||
|
var newValueType = newValue.GetType();
|
||||||
|
if (oldValueType != newValueType // Definitely different
|
||||||
|
|| !IsKnownImmutableType(oldValueType) // Maybe different
|
||||||
|
|| !oldValue.Equals(newValue)) // Somebody says they are different
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Both null, hence equal, so continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The contents of this list need to trade off false negatives against computation
|
||||||
|
// time. So we don't want a huge list of types to check (or would have to move to
|
||||||
|
// a hashtable lookup, which is differently expensive). It's better not to include
|
||||||
|
// uncommon types here even if they are known to be immutable.
|
||||||
|
private bool IsKnownImmutableType(Type type)
|
||||||
|
=> type.IsPrimitive
|
||||||
|
|| type == typeof(string)
|
||||||
|
|| type == typeof(DateTime)
|
||||||
|
|| type == typeof(RenderFragment)
|
||||||
|
|| type == typeof(decimal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -153,13 +153,22 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
||||||
// Preserve the actual componentInstance
|
// Preserve the actual componentInstance
|
||||||
newComponentFrame = newComponentFrame.WithComponentInstance(componentId, componentInstance);
|
newComponentFrame = newComponentFrame.WithComponentInstance(componentId, componentInstance);
|
||||||
|
|
||||||
// Supply latest parameters. They might not have changed, but it's up to the
|
// As an important rendering optimization, we want to skip parameter update
|
||||||
// recipient to decide what "changed" means. Currently we only supply the new
|
// notifications if we know for sure they haven't changed/mutated. The
|
||||||
|
// "MayHaveChangedSince" logic is conservative, in that it returns true if
|
||||||
|
// any parameter is of a type we don't know is immutable. In this case
|
||||||
|
// we call SetParameters and it's up to the recipient to implement
|
||||||
|
// whatever change-detection logic they want. Currently we only supply the new
|
||||||
// set of parameters and assume the recipient has enough info to do whatever
|
// set of parameters and assume the recipient has enough info to do whatever
|
||||||
// comparisons it wants with the old values. Later we could choose to pass the
|
// comparisons it wants with the old values. Later we could choose to pass the
|
||||||
// old parameter values if we wanted.
|
// old parameter values if we wanted. By default, components always rerender
|
||||||
|
// after any SetParameters call, which is safe but now always optimal for perf.
|
||||||
|
var oldParameters = new ParameterCollection(oldTree, oldComponentIndex);
|
||||||
var newParameters = new ParameterCollection(newTree, newComponentIndex);
|
var newParameters = new ParameterCollection(newTree, newComponentIndex);
|
||||||
componentInstance.SetParameters(newParameters);
|
if (!newParameters.DefinitelyEquals(oldParameters))
|
||||||
|
{
|
||||||
|
componentInstance.SetParameters(newParameters);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int NextSiblingIndex(RenderTreeFrame frame, int frameIndex)
|
private static int NextSiblingIndex(RenderTreeFrame frame, int frameIndex)
|
||||||
|
|
|
||||||
|
|
@ -832,6 +832,44 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
||||||
Assert.Same(objectWillNotChange, newComponentInstance.ObjectProperty);
|
Assert.Same(objectWillNotChange, newComponentInstance.ObjectProperty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SkipsUpdatingParametersOnChildComponentsIfAllAreDefinitelyImmutableAndUnchanged()
|
||||||
|
{
|
||||||
|
// We only know that types are immutable if either Type.IsPrimitive, or it's one of
|
||||||
|
// a known set of common immutable types.
|
||||||
|
|
||||||
|
// Arrange: Populate old and new with equivalent content
|
||||||
|
RenderFragment fragmentWillNotChange = builder => throw new NotImplementedException();
|
||||||
|
var dateTimeWillNotChange = DateTime.Now;
|
||||||
|
foreach (var tree in new[] { oldTree, newTree })
|
||||||
|
{
|
||||||
|
tree.OpenComponent<CaptureSetParametersComponent>(0);
|
||||||
|
tree.AddAttribute(1, "MyString", "Some fixed string");
|
||||||
|
tree.AddAttribute(1, "MyByte", (byte)123);
|
||||||
|
tree.AddAttribute(1, "MyInt", int.MaxValue);
|
||||||
|
tree.AddAttribute(1, "MyLong", long.MaxValue);
|
||||||
|
tree.AddAttribute(1, "MyBool", true);
|
||||||
|
tree.AddAttribute(1, "MyFloat", float.MaxValue);
|
||||||
|
tree.AddAttribute(1, "MyDouble", double.MaxValue);
|
||||||
|
tree.AddAttribute(1, "MyDecimal", decimal.MinusOne);
|
||||||
|
tree.AddAttribute(1, "MyDate", dateTimeWillNotChange);
|
||||||
|
tree.AddAttribute(1, "MyFragment", fragmentWillNotChange); // Treat fragments as primitive
|
||||||
|
tree.CloseComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderTreeDiffBuilder.ComputeDiff(renderer, new RenderBatchBuilder(), 0, new RenderTreeBuilder(renderer).GetFrames(), oldTree.GetFrames());
|
||||||
|
var originalComponentInstance = (CaptureSetParametersComponent)oldTree.GetFrames().Array[0].Component;
|
||||||
|
Assert.Equal(1, originalComponentInstance.SetParametersCallCount);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var renderBatch = GetRenderedBatch();
|
||||||
|
var newComponentInstance = (CaptureSetParametersComponent)oldTree.GetFrames().Array[0].Component;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Same(originalComponentInstance, newComponentInstance);
|
||||||
|
Assert.Equal(1, originalComponentInstance.SetParametersCallCount); // Received no further parameter change notification
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void QueuesRemovedChildComponentsForDisposal()
|
public void QueuesRemovedChildComponentsForDisposal()
|
||||||
{
|
{
|
||||||
|
|
@ -915,6 +953,20 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class CaptureSetParametersComponent : IComponent
|
||||||
|
{
|
||||||
|
public int SetParametersCallCount { get; private set; }
|
||||||
|
|
||||||
|
public void Init(RenderHandle renderHandle)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetParameters(ParameterCollection parameters)
|
||||||
|
{
|
||||||
|
SetParametersCallCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class DisposableComponent : IComponent, IDisposable
|
private class DisposableComponent : IComponent, IDisposable
|
||||||
{
|
{
|
||||||
public int DisposalCount { get; private set; }
|
public int DisposalCount { get; private set; }
|
||||||
|
|
|
||||||
|
|
@ -341,7 +341,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
||||||
Assert.Equal(0, edit.ReferenceFrameIndex);
|
Assert.Equal(0, edit.ReferenceFrameIndex);
|
||||||
});
|
});
|
||||||
AssertFrame.Text(batch.ReferenceFrames[0], "Modified message");
|
AssertFrame.Text(batch.ReferenceFrames[0], "Modified message");
|
||||||
Assert.Empty(batch.DiffsByComponentId[nestedComponentFrame.ComponentId].Single().Edits);
|
Assert.False(batch.DiffsByComponentId.ContainsKey(nestedComponentFrame.ComponentId));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue