loading

High-resolution clocks

Now / TimeOrigin

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.

C#
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);
Live sample
clocks output
Results will appear here when you interact with the samples.

Marks

Mark

Drops a named marker on the browser's performance timeline - visible in the DevTools Performance panel and retrievable through GetEntries.

C#
await performance.Mark("checkout:start");

// ... do the work you want to time ...

await performance.Mark("checkout:end");
Live sample
marks output
Results will appear here when you interact with the samples.

Measures

Measure / GetEntries

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.

C#
await performance.Measure("checkout", "checkout:start", "checkout:end");

var entries = await performance.GetEntries("checkout", "measure");
var duration = entries[^1].GetProperty("duration").GetDouble();
Live sample
measures output
Results will appear here when you interact with the samples.

Querying the timeline

GetEntries

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.

C#
var all = await performance.GetEntries();
var resources = await performance.GetEntries(type: "resource");
var myMarks = await performance.GetEntries("checkout:start", "mark");
Live sample
Entry type filter
timeline query output
Results will appear here when you interact with the samples.

Resource timing

GetResourceEntries

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.

C#
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");
Live sample
resource timing output
Results will appear here when you interact with the samples.

Web Vitals

GetWebVitals

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.

C#
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 score
Live sample
web vitals output
Results will appear here when you interact with the samples.

Typed 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.

C#
// 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();
Live sample
typed entries output
Results will appear here when you interact with the samples.

Live observer

SubscribeObserver

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.

C#
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();
Live sample
observer output
Results will appear here when you interact with the samples.

Memory snapshot

GetMemory

Reads the Chrome-only performance.memory snapshot of the JavaScript heap. On other browsers every field is null.

C#
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.memory
Live sample
memory output
Results will appear here when you interact with the samples.

Clearing buffers

ClearMarks / ClearMeasures / ClearResourceTimings

Removes 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.

C#
await performance.ClearMarks();            // all marks
await performance.ClearMarks("checkout:start"); // a single mark
await performance.ClearMeasures();
await performance.ClearResourceTimings();
Live sample
clearing output
Results will appear here when you interact with the samples.
Note:
Prefer marks over stopwatches Marks and measures land on the same timeline the DevTools Performance panel records, so your application timings show up right next to the browser's own paint, layout and network entries.
Warning:
GetMemory is Chrome-only performance.memory was never standardized; Chromium exposes it, Firefox and Safari do not. Always handle the all-null case.

API reference

Member
Signature
Description
Now
ValueTask<double> Now()
High-resolution timestamp since the time origin, in milliseconds.
TimeOrigin
ValueTask<double> TimeOrigin()
The time origin of the document - typically the navigation start, in Unix epoch milliseconds.
Mark
ValueTask Mark(string name)
Adds a named mark to the browser's performance timeline.
Measure
ValueTask Measure(string name, string? startMark = null, string? endMark = null)
Creates a named measure between two marks (or between a mark and now).
ClearMarks
ValueTask ClearMarks(string? name = null)
Removes performance marks; null clears all of them.
ClearMeasures
ValueTask ClearMeasures(string? name = null)
Removes performance measures; null clears all of them.
ClearResourceTimings
ValueTask ClearResourceTimings()
Empties the resource-timing buffer.
GetEntries
ValueTask<JsonElement[]> GetEntries(string? name = null, string? type = null)
Returns all recorded entries, optionally filtered by name and/or type, as JsonElement values.
GetTypedEntries<T>
ValueTask<T[]> GetTypedEntries<T>(string entryType, string? name = null)
Returns the entries of one entryType deserialized into the type describing that kind.
GetNavigationEntries
ValueTask<PerformanceNavigationTiming[]> GetNavigationEntries()
The document's own load timing - one entry, or none during prerender.
GetResourceEntries
ValueTask<PerformanceResourceTiming[]> GetResourceEntries(string? name = null)
Every subresource fetch with its full phase-by-phase timing breakdown.
GetLongTasks
ValueTask<PerformanceLongTaskTiming[]> GetLongTasks()
Tasks that blocked the main thread for over 50 ms. Never on the timeline, so the first call starts collecting them - returning what the engine had already buffered - and a later call reads what came in between.
GetLongAnimationFrames
ValueTask<PerformanceLongAnimationFrameTiming[]> GetLongAnimationFrames()
Frames over 50 ms with the scripts that caused them, named down to the function. Chromium-only.
GetLargestContentfulPaints
ValueTask<LargestContentfulPaint[]> GetLargestContentfulPaints()
The LCP candidates recorded so far; the last one is the current LCP.
GetLayoutShifts
ValueTask<LayoutShift[]> GetLayoutShifts()
The layout shifts recorded so far, with the rectangles each moved element occupied.
GetEventTimings
ValueTask<PerformanceEventTiming[]> GetEventTimings(bool firstInputOnly = false)
The slow interactions INP is computed from, or the page's single first-input entry.
GetWebVitals
ValueTask<WebVitals?> GetWebVitals()
LCP, CLS and INP as they stand, plus FCP and TTFB. The first call starts CLS and INP accumulating.
GetMemory
ValueTask<PerformanceMemory?> GetMemory()
Chrome-only JS heap snapshot; all fields are null on browsers without performance.memory.
SubscribeObserver
Task<ButilSubscription> SubscribeObserver(string[] entryTypes, Action<JsonElement[]> handler, bool buffered = true)
Subscribes a PerformanceObserver for the given entry types and returns a disposable subscription handle.
SubscribeObserver<T>
Task<ButilSubscription> SubscribeObserver<T>(string[] entryTypes, Action<T[]> handler, bool buffered = true)
The same subscription with each batch deserialized into T - the way to read long tasks, layout shifts and LCP candidates.
DisposeAsync
ValueTask DisposeAsync()
Disconnects all observers and releases the interop reference.
An unhandled error has occurred. Reload 🗙