-
-
Notifications
You must be signed in to change notification settings - Fork 264
Coalesce component renders during Blazor WebAssembly startup #12753 #12754
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| namespace Boilerplate.Client.Core.Components; | ||
|
|
||
| public partial class AppComponentBase | ||
| { | ||
| /// <summary> | ||
| /// Opt-in flag (disabled by default). When enabled, the renders triggered during the app's | ||
| /// startup window are coalesced: a burst of <see cref="ComponentBase.StateHasChanged"/> calls | ||
| /// collapses into a single render once the burst settles, instead of re-rendering this component | ||
| /// (and its whole subtree) once per call. | ||
| /// <para> | ||
| /// This targets Blazor WebAssembly startup cost only, so it is active exclusively in the browser. | ||
| /// The window is measured from app start (<see cref="startupTimestamp"/>), components created after <see cref="CoalesceRendersDuration"/> has elapsed | ||
| /// never coalesce and render normally, keeping steady-state interactivity immediately responsive. | ||
| /// </para> | ||
| /// </summary> | ||
| protected virtual bool CoalesceRenders => false; | ||
|
|
||
| /// <summary>How long after app start the coalescing stays active.</summary> | ||
| protected virtual TimeSpan CoalesceRendersDuration => TimeSpan.FromSeconds(3); | ||
|
|
||
| /// <summary>The quiet period a render burst must settle for before it is flushed (extended on each new change).</summary> | ||
| protected virtual TimeSpan CoalesceRendersWindow => TimeSpan.FromMilliseconds(300); | ||
|
|
||
|
|
||
| private static readonly long startupTimestamp = Stopwatch.GetTimestamp(); | ||
| private bool passCoalescedRender; | ||
| private bool coalesceDisposed; | ||
| private ITimer? coalesceTimer; | ||
|
|
||
|
|
||
| protected override bool ShouldRender() | ||
| { | ||
| // Only Blazor WebAssembly (where startup rendering is the bottleneck); everywhere else render normally. | ||
| if (CoalesceRenders is false || AppPlatform.IsBrowser is false) | ||
| return base.ShouldRender(); | ||
|
|
||
| // Past the startup window (measured from app start, not this component) → behave normally from here on. | ||
| if (Stopwatch.GetElapsedTime(startupTimestamp) >= CoalesceRendersDuration) | ||
| { | ||
| DisposeCoalesceTimer(); | ||
| return base.ShouldRender(); | ||
| } | ||
|
|
||
| // A render we scheduled ourselves (the trailing edge), let it through. | ||
| if (passCoalescedRender) | ||
| { | ||
| passCoalescedRender = false; | ||
| return true; | ||
| } | ||
|
|
||
| // Inside a burst: suppress this render, but (re)arm a single trailing render so the final | ||
| // startup state is guaranteed to paint. Created once (stopped), then each new change just | ||
| // resets the due time via Change, no re-allocation per suppressed render (debounce). | ||
| coalesceTimer ??= TimeProvider.CreateTimer(static state => | ||
| { | ||
| var self = (AppComponentBase)state!; | ||
| // The timer fires on a threadpool thread, hop back onto the renderer's sync context. | ||
| _ = self.InvokeAsync(() => | ||
| { | ||
| if (self.coalesceDisposed) return; // do not render after disposal | ||
| self.passCoalescedRender = true; | ||
| self.StateHasChanged(); | ||
| }); | ||
| }, this, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); | ||
|
|
||
| coalesceTimer.Change(CoalesceRendersWindow, Timeout.InfiniteTimeSpan); | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| private void DisposeCoalesceTimer() | ||
| { | ||
| coalesceTimer?.Dispose(); | ||
| coalesceTimer = null; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Implementation of the partial declared in <c>AppComponentBase.cs</c>, invoked from | ||
| /// <see cref="DisposeAsync()"/> so the coalescing timer is always released while its whole | ||
| /// logic stays isolated in this file. Compiles away entirely when this partial is absent. | ||
| /// </summary> | ||
| partial void DisposeRenderCoalescing() | ||
| { | ||
| coalesceDisposed = true; | ||
| DisposeCoalesceTimer(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,44 +8,46 @@ public partial class AppComponentBase : OwningComponentBase, IAsyncDisposable | |
| [CascadingParameter(Name = Parameters.IsOnline)] protected bool? IsOnline { get; set; } | ||
| [CascadingParameter] protected Task<AuthenticationState> AuthenticationStateTask { get; set; } = default!; | ||
|
|
||
| [AutoInject] protected IJSRuntime JSRuntime = default!; | ||
| [AutoInject] private IServiceProvider serviceProvider = default!; | ||
|
|
||
| [AutoInject] protected IStorageService StorageService = default!; | ||
| protected IJSRuntime JSRuntime => field ??= serviceProvider.GetRequiredService<IJSRuntime>(); | ||
|
|
||
| [AutoInject] protected JsonSerializerOptions JsonSerializerOptions = default!; | ||
| protected IStorageService StorageService => field ??= serviceProvider.GetRequiredService<IStorageService>(); | ||
|
|
||
| [AutoInject] protected TimeProvider TimeProvider = default!; | ||
| protected JsonSerializerOptions JsonSerializerOptions => field ??= serviceProvider.GetRequiredService<JsonSerializerOptions>(); | ||
|
|
||
| protected TimeProvider TimeProvider => field ??= serviceProvider.GetRequiredService<TimeProvider>(); | ||
|
Comment on lines
+13
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Converting these dependencies from Prompt To Fix With AIThis is a comment left during a code review.
Path: src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs
Line: 13-19
Comment:
**Synchronize component injection documentation**
Converting these dependencies from `[AutoInject]` fields to lazy getter-only properties leaves the Boilerplate component documentation showing the removed declarations as the `AppComponentBase` pattern. Update the associated documentation so template consumers are not directed to an implementation that no longer matches the generated source.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
|
|
||
| /// <summary> | ||
| /// <inheritdoc cref="Infrastructure.Services.PubSubService"/> | ||
| /// </summary> | ||
| [AutoInject] protected PubSubService PubSubService = default!; | ||
| protected PubSubService PubSubService => field ??= serviceProvider.GetRequiredService<PubSubService>(); | ||
|
|
||
| [AutoInject] protected IConfiguration Configuration = default!; | ||
| protected IConfiguration Configuration => field ??= serviceProvider.GetRequiredService<IConfiguration>(); | ||
|
|
||
| [AutoInject] protected NavigationManager NavigationManager = default!; | ||
| protected NavigationManager NavigationManager => field ??= serviceProvider.GetRequiredService<NavigationManager>(); | ||
|
|
||
| [AutoInject] protected IAuthTokenProvider AuthTokenProvider = default!; | ||
| protected IAuthTokenProvider AuthTokenProvider => field ??= serviceProvider.GetRequiredService<IAuthTokenProvider>(); | ||
|
|
||
| [AutoInject] protected IStringLocalizer<AppStrings> Localizer = default!; | ||
| protected IStringLocalizer<AppStrings> Localizer => field ??= serviceProvider.GetRequiredService<IStringLocalizer<AppStrings>>(); | ||
|
|
||
| [AutoInject] protected ClientExceptionHandlerBase ExceptionHandler = default!; | ||
| protected ClientExceptionHandlerBase ExceptionHandler => field ??= serviceProvider.GetRequiredService<ClientExceptionHandlerBase>(); | ||
|
|
||
| [AutoInject] protected AuthManager AuthManager = default!; | ||
| protected AuthManager AuthManager => field ??= serviceProvider.GetRequiredService<AuthManager>(); | ||
|
|
||
| [AutoInject] protected SnackBarService SnackBarService = default!; | ||
| protected SnackBarService SnackBarService => field ??= serviceProvider.GetRequiredService<SnackBarService>(); | ||
|
|
||
| [AutoInject] protected ITelemetryContext TelemetryContext = default!; | ||
| protected ITelemetryContext TelemetryContext => field ??= serviceProvider.GetRequiredService<ITelemetryContext>(); | ||
|
|
||
| /// <summary> | ||
| /// <inheritdoc cref="ISharedServiceCollectionExtensions.ConfigureAuthorizationCore"/> | ||
| /// </summary> | ||
| [AutoInject] protected IAuthorizationService AuthorizationService = default!; | ||
| protected IAuthorizationService AuthorizationService => field ??= serviceProvider.GetRequiredService<IAuthorizationService>(); | ||
|
|
||
| /// <summary> | ||
| /// <inheritdoc cref="AbsoluteServerAddressProvider" /> | ||
| /// </summary> | ||
| [AutoInject] protected AbsoluteServerAddressProvider AbsoluteServerAddress { get; set; } = default!; | ||
| protected AbsoluteServerAddressProvider AbsoluteServerAddress => field ??= serviceProvider.GetRequiredService<AbsoluteServerAddressProvider>(); | ||
|
|
||
|
|
||
| private CancellationTokenSource? cts = new(); | ||
|
|
@@ -263,8 +265,16 @@ protected async Task Abort() | |
| await currentCts.TryCancel(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Implemented in the rendering partial (<c>AppComponentBase.Rendering.cs</c>) to release the | ||
| /// render-coalescing timer on disposal. Keeps that feature's logic fully isolated in its own file. | ||
| /// </summary> | ||
| partial void DisposeRenderCoalescing(); | ||
|
|
||
| public async ValueTask DisposeAsync() | ||
| { | ||
| DisposeRenderCoalescing(); | ||
|
|
||
| try | ||
| { | ||
| if (cts != null) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new lazy dependency properties replace direct
[AutoInject]declarations with repeatedserviceProvider.GetRequiredService<T>()calls. This hides the component's dependencies behind a service locator and conflicts with the repository convention requiring component dependencies to use[AutoInject].Context Used: src/Templates/Boilerplate/Bit.Boilerplate/AGENTS.m... (source)
Prompt To Fix With AI
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!