loading
Note:
Secure context required Service workers only run over HTTPS (localhost is exempt). This site ships a minimal demo worker at /sw.js that does no caching - it exists purely so the registration-based APIs on this page (and on the BackgroundSync and Push pages) have something real to register and inspect.

Check support

IsSupported

Returns true when the runtime exposes navigator.serviceWorker. Service workers are available in every modern browser, but not in insecure contexts.

C#
var supported = await serviceWorker.IsSupported();
Live sample
support output
Results will appear here when you interact with the samples.

Register a worker

Register

Registers a same-origin worker script and resolves with a snapshot of the created registration. Optional parameters control the scope, the update-via-cache strategy (imports, all or none) and whether the script is loaded as an ES module.

@inject Bit.Butil.ServiceWorker serviceWorker

<button @onclick="Register">Register /sw.js</button>

@code {
    private async Task Register()
    {
        var info = await serviceWorker.Register("/sw.js");

        // info.IsRegistered   -> true
        // info.Scope          -> e.g. "https://example.com/"
        // info.ActiveState    -> "activated" once the worker is running
        // info.InstallingState / info.WaitingState / info.UpdateViaCache
    }
}
Live sample
register output
Results will appear here when you interact with the samples.

Inspect and update the registration

GetRegistration / Update

GetRegistration returns the registration matching a scope, or the most specific one for the current URL when the scope is omitted; IsRegistered is false when none exists. Update forces the browser to re-fetch the worker script and check for a newer version.

C#
var info = await serviceWorker.GetRegistration();

if (info.IsRegistered)
{
    await serviceWorker.Update();
}
Live sample
registration output
Results will appear here when you interact with the samples.

Messaging and controller changes

PostMessage / SubscribeMessage / SubscribeControllerChange

PostMessage sends a payload to the worker currently controlling this page and returns false when there is no controller yet. SubscribeMessage receives payloads the worker posts back as JsonElement values, and SubscribeControllerChange fires whenever the controlling worker changes - for example right after the demo worker activates and claims this page. Both return a ButilSubscription; dispose it to detach.

@implements IAsyncDisposable
@inject Bit.Butil.ServiceWorker serviceWorker

@code {
    private string? _lastEcho;
    private ButilSubscription? _messageSub;
    private ButilSubscription? _controllerSub;

    private async Task Subscribe()
    {
        _messageSub = await serviceWorker.SubscribeMessage(data =>
        {
            // data is a JsonElement carrying whatever the worker posted
            _lastEcho = data.GetProperty("echo").GetString();
            InvokeAsync(StateHasChanged);
        });

        _controllerSub = await serviceWorker.SubscribeControllerChange(() =>
        {
            // the page is now controlled by a (new) worker
            InvokeAsync(StateHasChanged);
        });
    }

    // False when nothing controls this page yet - a first registration does not control the page
    // that registered it until the worker claims it.
    private async Task Post()
    {
        var delivered = await serviceWorker.PostMessage("hi worker");
    }

    public async ValueTask DisposeAsync()
    {
        if (_messageSub is not null) await _messageSub.DisposeAsync();
        if (_controllerSub is not null) await _controllerSub.DisposeAsync();
    }
}
Live sample
messaging output
Results will appear here when you interact with the samples.

skipWaiting and clients.claim

SkipWaiting / Claim

Both of these can only be done by the worker to itself, so these post a message and the worker acts on it. SkipWaiting promotes a worker that has installed and is waiting for the old one's pages to go away - the 'reload to update' button, without the reload. Claim takes control of pages that loaded before the worker activated, which is why the first visit after an install is the one where offline support quietly does not work.

@inject Bit.Butil.ServiceWorker serviceWorker

@code {
    // False when there was no worker waiting - a first registration activates on its own.
    private async Task Promote()
    {
        var hadWaitingWorker = await serviceWorker.SkipWaiting();
    }

    // Claim answers over a MessageChannel, so it is false when no worker answered in time as well
    // as when the call failed inside the worker.
    private async Task Take()
    {
        var claimed = await serviceWorker.Claim();
    }
}
Live sample
lifecycle output
Results will appear here when you interact with the samples.

The Clients API

MatchAllClients

Which tabs, iframes and workers the service worker controls. The Clients API exists only on the worker's global scope, so this is a question asked over a MessageChannel - and an empty result is ambiguous: the worker may have answered with no clients (this page is not in the list while it is uncontrolled and includeUncontrolled is false), or there was no active worker, or none answered.

@inject Bit.Butil.ServiceWorker serviceWorker

@code {
    private async Task ListClients()
    {
        ServiceWorkerClientInfo[] clients = await serviceWorker.MatchAllClients(
            includeUncontrolled: true, type: "window");

        // An empty array is ambiguous: no clients, no active worker, or none answered in time.
        foreach (var client in clients)
        {
            // client.Id, client.Url, client.Type, client.FrameType,
            // client.Focused, client.VisibilityState
        }
    }
}
Live sample
clients output
Results will appear here when you interact with the samples.

Unregister

Unregister

Removes the registration matching the given scope, or the current one when omitted, and returns true when a registration was actually removed. Butil never unregisters workers automatically on disposal - the consuming app decides when a worker goes away.

C#
var removed = await serviceWorker.Unregister();
Live sample
unregister output
Results will appear here when you interact with the samples.
Warning:
Registrations outlive the page A service worker registration persists across reloads and browser restarts until it is unregistered or its script fails to fetch. If you registered the demo worker here, it stays registered for this origin until you press Unregister (or remove it in DevTools, under Application, Service workers).

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes navigator.serviceWorker.
Register
ValueTask<ServiceWorkerRegistrationInfo> Register(string scriptUrl, string? scope = null, string? updateViaCache = null, bool moduleType = false)
Registers a same-origin service worker script and resolves once the registration is created.
GetRegistration
ValueTask<ServiceWorkerRegistrationInfo> GetRegistration(string? scope = null)
Returns the registration matching the scope (or the most specific one for the document URL when null).
Update
ValueTask Update(string? scope = null)
Forces an update check for a registration.
Unregister
ValueTask<bool> Unregister(string? scope = null)
Unregisters the worker matching the scope. Returns true when something was removed.
PostMessage
ValueTask<bool> PostMessage<T>(T message)
Sends the message to the active worker controlling this page. Returns false when no controller exists.
SubscribeMessage
Task<ButilSubscription> SubscribeMessage(Action<JsonElement> handler)
Subscribes to messages broadcast from the service worker; each payload arrives as a JsonElement.
SubscribeControllerChange
Task<ButilSubscription> SubscribeControllerChange(Action handler)
Fires when navigator.serviceWorker.controller changes.
DisposeAsync
ValueTask DisposeAsync()
Detaches every subscription created by this instance. Registrations themselves are left untouched.
InvokeServiceWorkerMessage
void InvokeServiceWorkerMessage(Guid id, JsonElement data)
Interop plumbing invoked from JavaScript ([JSInvokable]); not intended for direct use.
InvokeServiceWorkerControllerChange
void InvokeServiceWorkerControllerChange(Guid id)
Interop plumbing invoked from JavaScript ([JSInvokable]); not intended for direct use.
ServiceWorkerRegistrationInfo.IsRegistered
bool IsRegistered { get; set; }
True when a registration was found or created.
ServiceWorkerRegistrationInfo.Scope
string Scope { get; set; }
Scope URL the registration applies to.
ServiceWorkerRegistrationInfo.ActiveState
string? ActiveState { get; set; }
The active worker's state: installing, installed, activating, activated, redundant, or null when none.
ServiceWorkerRegistrationInfo.InstallingState
string? InstallingState { get; set; }
The installing worker's state, when one is being installed.
ServiceWorkerRegistrationInfo.WaitingState
string? WaitingState { get; set; }
The waiting worker's state, when an update is queued.
ServiceWorkerRegistrationInfo.UpdateViaCache
string? UpdateViaCache { get; set; }
Update via cache strategy: imports, all, or none.
GetRegistrations
ValueTask<ServiceWorkerRegistrationInfo[]> GetRegistrations()
Every registration this origin has, not just the one matching a scope. Useful for cleaning up workers left by an earlier version of an app.
Ready
ValueTask<ServiceWorkerRegistrationInfo> Ready(int timeoutMs = 10000)
Waits until a worker is active and returns its registration - the point at which PostMessage will actually reach it. Register returns while the worker is still installing. Times out rather than hanging when nothing is registered.
EnableNavigationPreload
ValueTask<bool> EnableNavigationPreload(string? scope = null)
Makes the browser issue the navigation request in parallel with booting the worker. False when there is no active worker yet.
DisableNavigationPreload
ValueTask<bool> DisableNavigationPreload(string? scope = null)
Turns navigation preload back off.
SetNavigationPreloadHeader
ValueTask<bool> SetNavigationPreloadHeader(string value, string? scope = null)
Sets the Service-Worker-Navigation-Preload header value the browser sends on preload requests.
GetNavigationPreloadState
ValueTask<NavigationPreloadState> GetNavigationPreloadState(string? scope = null)
Whether navigation preload is enabled, and with what header value.
SkipWaiting
ValueTask<bool> SkipWaiting(string? scope = null)
Asks a waiting worker to call skipWaiting(). False when no worker is waiting. Needs the worker-side message handler.
Claim
ValueTask<bool> Claim(string? scope = null, int timeoutMs = 5000)
Asks the active worker to call clients.claim(), taking control of pages that loaded before it activated. Needs the worker-side message handler.
MatchAllClients
ValueTask<ServiceWorkerClientInfo[]> MatchAllClients(bool includeUncontrolled = false, string type = &quot;window&quot;, string? scope = null, int timeoutMs = 5000)
The clients the worker controls, asked over a MessageChannel. Empty also means the worker does not answer.
NavigationPreloadState
class NavigationPreloadState { bool IsSupported; bool Enabled; string HeaderValue; }
Navigation preload's state. IsSupported tells 'not implemented' apart from 'implemented and off'.
ServiceWorkerClientInfo
class ServiceWorkerClientInfo { string Id; string Url; string Type; string FrameType; bool Focused; string VisibilityState; }
One client of the worker - a page, a worker or a shared worker.
An unhandled error has occurred. Reload 🗙