Performance
High-resolution timing from C#: read the monotonic clock, drop named marks and measures on the browser's performance timeline, query recorded entries as typed navigation, resource, long-task and Web Vitals shapes, and stream new ones live through PerformanceObserver.
@inject Bit.Butil.Performance performanceMDN reference
Now returns the milliseconds elapsed since the time origin as a monotonic, sub-millisecond timestamp; TimeOrigin is that origin (typically navigation start) in Unix epoch milliseconds.
var now = await performance.Now(); // e.g. 5321.4 ms since navigation
var origin = await performance.TimeOrigin(); // Unix epoch ms
var startedAt = DateTimeOffset.FromUnixTimeMilliseconds((long)origin);Drops a named marker on the browser's performance timeline - visible in the DevTools Performance panel and retrievable through GetEntries.
await performance.Mark("checkout:start");
// ... do the work you want to time ...
await performance.Mark("checkout:end");Creates a named measure between two marks (or from a mark to now) and reads its duration back from the timeline. Place both marks with the buttons below, then measure.
await performance.Measure("checkout", "checkout:start", "checkout:end");
var entries = await performance.GetEntries("checkout", "measure");
var duration = entries[^1].GetProperty("duration").GetDouble();Returns every PerformanceEntry recorded so far, optionally filtered by name and/or entry type. Entry shapes vary per type, so they are surfaced as JsonElement.
var all = await performance.GetEntries();
var resources = await performance.GetEntries(type: "resource");
var myMarks = await performance.GetEntries("checkout:start", "mark");Every subresource the page fetched, typed as PerformanceResourceTiming - initiator, protocol, phase timings and the three sizes that say whether it was compressed or served from cache. Cross-origin entries report zeros unless the server sends Timing-Allow-Origin.
PerformanceResourceTiming[] resources = await performance.GetResourceEntries();
var slowest = resources.OrderByDescending(r => r.Duration).First();
// slowest.InitiatorType, slowest.NextHopProtocol, slowest.TransferSize
// or narrow to one URL:
var one = await performance.GetResourceEntries("https://example.com/app.js");LCP, CLS and INP already reduced from the entries they are computed over - the last LCP candidate, the worst CLS session window, the near-worst interaction - plus FCP and TTFB. The first call is what starts CLS and INP accumulating, so call it early and read it again later. Click around the page, then read it a second time.
WebVitals? vitals = await performance.GetWebVitals(); // null during prerender/SSR
// vitals.Lcp - milliseconds, good below 2,500
// vitals.Cls - unitless, good below 0.1
// vitals.Inp - milliseconds, good below 200; null until the user interacts
// a metric the engine does not implement is null, which is not a zero scoreTyped entries and typed observers
GetTypedEntries<T> / GetLongTasks / GetLongAnimationFrames / GetLayoutShifts / GetLargestContentfulPaints / GetEventTimings / SubscribeObserver<T>The generic overloads deserialize each entry into the type that describes its kind. Long tasks, layout shifts and LCP candidates are never stored on the timeline - they only reach a subscriber - so the button below subscribes to all three and then blocks the main thread for 120 ms to produce one.
// typed one-shot reads
LargestContentfulPaint[] lcp = await performance.GetLargestContentfulPaints();
PerformanceEventTiming[] slow = await performance.GetEventTimings();
PerformanceEntry[] paints = await performance.GetTypedEntries<PerformanceEntry>(PerformanceEntryTypes.Paint);
// the kinds that are only ever observed
var subscription = await performance.SubscribeObserver<PerformanceLongTaskTiming>(
[PerformanceEntryTypes.LongTask],
tasks =>
{
foreach (var task in tasks)
{
Console.WriteLine($"{task.Duration:F0} ms blocked by {task.Name}");
}
});
await subscription.DisposeAsync();Subscribes a PerformanceObserver for one or more entry types and pushes new entries to your C# handler as they are recorded. This demo observes marks and measures - press the mark buttons above after subscribing.
var subscription = await performance.SubscribeObserver(
["mark", "measure"],
entries =>
{
foreach (var entry in entries)
{
var name = entry.GetProperty("name").GetString();
var duration = entry.GetProperty("duration").GetDouble();
}
},
buffered: true);
// later, when no longer needed:
await subscription.DisposeAsync();Reads the Chrome-only performance.memory snapshot of the JavaScript heap. On other browsers every field is null.
var memory = await performance.GetMemory(); // null during prerender/SSR
// memory.UsedJsHeapSize, memory.TotalJsHeapSize, memory.JsHeapSizeLimit
// all fields are null when the browser does not expose performance.memoryRemoves marks and measures (all of them, or a single name), and empties the resource-timing buffer - useful when the buffer fills up on long-lived pages.
await performance.ClearMarks(); // all marks
await performance.ClearMarks("checkout:start"); // a single mark
await performance.ClearMeasures();
await performance.ClearResourceTimings();API reference
ValueTask<double> Now()ValueTask<double> TimeOrigin()ValueTask Mark(string name)ValueTask Measure(string name, string? startMark = null, string? endMark = null)ValueTask ClearMarks(string? name = null)ValueTask ClearMeasures(string? name = null)ValueTask ClearResourceTimings()ValueTask<JsonElement[]> GetEntries(string? name = null, string? type = null)ValueTask<T[]> GetTypedEntries<T>(string entryType, string? name = null)ValueTask<PerformanceNavigationTiming[]> GetNavigationEntries()ValueTask<PerformanceResourceTiming[]> GetResourceEntries(string? name = null)ValueTask<PerformanceLongTaskTiming[]> GetLongTasks()ValueTask<PerformanceLongAnimationFrameTiming[]> GetLongAnimationFrames()ValueTask<LargestContentfulPaint[]> GetLargestContentfulPaints()ValueTask<LayoutShift[]> GetLayoutShifts()ValueTask<PerformanceEventTiming[]> GetEventTimings(bool firstInputOnly = false)ValueTask<WebVitals?> GetWebVitals()ValueTask<PerformanceMemory?> GetMemory()Task<ButilSubscription> SubscribeObserver(string[] entryTypes, Action<JsonElement[]> handler, bool buffered = true)Task<ButilSubscription> SubscribeObserver<T>(string[] entryTypes, Action<T[]> handler, bool buffered = true)ValueTask DisposeAsync()