Speculation
Speculation rules that make the next navigation instant - and, more importantly for most apps, the prerendering state that says whether anyone is actually looking at this page yet.
@inject Bit.Butil.Speculation speculationMDN reference
IsPrerendering is true the page is running in a hidden tab nobody has opened:
analytics events, a play(), a POST, anything that counts a visit is happening for a
user who may never arrive. Hold those back and run them from OnActivated.
GetActivationStart is worth more than it looks: every other timestamp on the page is measured from the moment the prerender began, so a load time computed without subtracting it counts time the user never spent waiting.
@inject Bit.Butil.Speculation speculation
if (await speculation.IsPrerendering())
{
// running in a hidden tab - hold the analytics call back
}
var activationStart = await speculation.GetActivationStart();
var realLoadTime = loadEnd - activationStart;Fires once, on a page that was prerendered, at the moment the user actually navigates to it - and never on a page that wasn't. So it is a place to put work, not the only place: pair it with an IsPrerendering check that is false.
IAsyncDisposable
Bit.Butil.Speculation speculation
AnalyticsService analytics
{
private ButilSubscription? _subscription;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender is false) return;
// A prerendered page runs in a hidden tab that may never be shown, so anything that counts
// a visit has to wait for the activation that makes it real.
if (await speculation.IsPrerendering())
{
_subscription = await speculation.OnActivated(activationStart =>
{
analytics.PageView(); // now there is really someone here
});
}
else
{
analytics.PageView();
}
}
public async ValueTask DisposeAsync()
{
if (_subscription is not null) await _subscription.DisposeAsync();
}
}Prefetch fetches the response and stops there - cheap, no side effects, safe to be generous with. Prerender loads and runs the whole page in a hidden tab, which is what makes the navigation instant and also what makes it expensive: bandwidth, CPU and battery for a page the user may never open. Keep it to a handful of destinations you are confident about, and remember those pages will see IsPrerendering true.
IAsyncDisposable
Bit.Butil.Speculation speculation
{
private ButilSubscription? _prefetch;
private ButilSubscription? _prerender;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender is false) return;
_prefetch = await speculation.Prefetch(["/url", "/selection"]);
_prerender = await speculation.Prerender(["/getting-started"], SpeculationEagerness.Conservative);
// anything the two convenience methods don't cover - a rule set of your own:
var documentRules = @"{ ""prerender"": [ { ""where"": { ""selector_matches"": "".product-link"" } } ] }";
await speculation.AddRules(documentRules);
}
// Removes the rules, cancelling any speculation the user never went on to use.
public async ValueTask DisposeAsync()
{
if (_prefetch is not null) await _prefetch.DisposeAsync();
if (_prerender is not null) await _prerender.DisposeAsync();
}
}API reference
ValueTask<bool> IsSupported()ValueTask<bool> IsPrerendering()ValueTask<bool> WasPrerendered()ValueTask<double> GetActivationStart()ValueTask<ButilSubscription?> Prerender(string[] urls, SpeculationEagerness eagerness = Moderate)ValueTask<ButilSubscription?> Prefetch(string[] urls, SpeculationEagerness eagerness = Moderate)ValueTask<ButilSubscription?> AddRules(string rulesJson)ValueTask<ButilSubscription> OnActivated(Action<double> handler)ValueTask DisposeAsync()Immediate | Eager | Moderate | Conservative