workmanager_web 0.2.0
workmanager_web: ^0.2.0 copied to clipboard
Web (experimental) implementation of workmanager using a Service Worker and Web Worker for background task execution.
workmanager_web #
Experimental web implementation of workmanager for Flutter. It approximates
"execute Dart code in the background, even when the app is closed" on the web
using a Service Worker (for page-closed execution) and a Web Worker
(for real parallel execution while the page is open).
⚠️ EXPERIMENTAL. Read Honest limitations before using this package. Web browsers cannot run Dart at an exact wall-clock time after the page is closed — the web contract is a best-effort approximation.
How it works #
| Situation | Mechanism |
|---|---|
| Page open | Tasks execute in a dedicated Web Worker running the compiled callback dispatcher (dart2js, Flutter-free) — real parallel execution off the main thread. Falls back to in-page execution if the bundle is missing. |
| Page closed — Periodic Background Sync | Chromium fires the Service Worker roughly every minInterval (browser minimum 12 hours) once the PWA is installed and the user has engagement. The Service Worker then runs the compiled Dart dispatcher itself via importScripts and records the result in IndexedDB. |
| Page closed — Web Push | A push message wakes the Service Worker and triggers the same Dart execution path. Requires a push server. |
| Page closed — fetch interception | Opportunistic: a same-origin request can wake the Service Worker and run due periodic tasks / overdue one-off tasks. Not reliable — browsers throttle Service Worker wake-ups. |
Recorded results are replayed into the app's event log on the next page load, so background executions while the page was closed are observable.
Setup #
1. Add the dependency #
dependencies:
workmanager_web: ^0.1.0
The main workmanager package also delegates to this package on web, so
Workmanager() works on web too. For web-specific options use
WorkmanagerWeb directly.
2. Copy the Service Worker into your app #
The Service Worker script is JavaScript and lives in this package's web/
folder. Copy it into your app's web/ folder so it is served at
/workmanager_service_worker.js:
cp ../workmanager_web/web/workmanager_service_worker.js web/
3. Write a Flutter-free dispatcher and compile it #
A Service Worker cannot run the Flutter engine, so your callback dispatcher
must live in a Flutter-free Dart file (no package:flutter/... imports)
and be compiled with plain dart2js. Use WorkmanagerExecution.executeTask
instead of Workmanager().executeTask(...):
// lib/background_tasks.dart — pure Dart, no Flutter imports
import 'package:workmanager_web/execution.dart';
@pragma('vm:entry-point')
void callbackDispatcher() {
WorkmanagerExecution.instance.executeTask((taskName, inputData) async {
// Flutter-free background work only.
return true;
});
}
// web/background.dart — standalone dart2js entrypoint
import 'package:workmanager_web/worker.dart';
import 'package:my_app/background_tasks.dart';
void main() {
WorkmanagerWebWorker.run(callbackDispatcher);
}
Compile it and commit the output next to your app's web/ folder:
dart compile js --no-source-maps -O2 web/background.dart -o web/background.dart.js
Why
-Ddart.library.js_interop=falsewas evaluated and rejected: that define selects the legacy inline JS interop for dart2wasm. dart2js supportsdart:js_interopnatively and the define would actually break this package's conditional imports. Plaindart compile jsis what you want.
4. Initialize and register tasks #
import 'package:workmanager_web/workmanager_web.dart';
await WorkmanagerWeb().initialize(
callbackDispatcher,
dispatcherUrl: WorkmanagerWeb.defaultDispatcherUrl, // /background.dart.js
);
await WorkmanagerWeb().registerPeriodicTask(
'uniqueName',
'taskName',
frequency: const Duration(minutes: 15),
);
The example app in this repository wires all of this up — see
example/ and its web/ folder for a runnable reference,
including the PWA manifest.
5. Communicating with the background worker #
Task execution is request/response — the page asks the worker to run a task and
gets the result back. For anything more interactive (progress updates, live
data, "worker, do X now"), the worker also supports free-form two-way
messaging over postMessage:
// Page side — send a message, listen for replies.
WorkmanagerWeb().workerMessages.listen((payload) {
print('worker says: $payload');
});
WorkmanagerWeb().sendMessageToWorker({'op': 'watch', 'city': 'cardiff', 'threshold': 5.0});
// Dispatcher side (Flutter-free bundle) — receive and reply.
void callbackDispatcher() {
WorkmanagerExecution.instance.executeTask((taskName, inputData) async {
return true;
});
WorkmanagerExecution.instance.messageHandler = (payload) {
WorkmanagerExecution.instance.sendToPage?.call({'kind': 'ack'});
};
}
How it maps to the browser:
- Page open — messages travel between the page and the dedicated Web
Worker over
postMessage, so the handler runs off the main thread. - No Web Worker available — the message is delivered directly to the
in-page dispatcher; replies surface on the same
workerMessagesstream. - Service Worker execution (page closed) — a
sendToPagecall is delivered to every open page viaclients.postMessage; when no page is open it is dropped (persistent task results still arrive throughbackgroundEventson the next load).
The example app demonstrates this with a live "worker chat" panel, a simulated weather-watch use case, and browser notifications when a background task finishes (including while the tab is closed, via the Service Worker).
Testing it (what the maintainers verified) #
flutter run -d chrome(orflutter build web+ serve over HTTPS orlocalhost).- Install the PWA (Chrome address bar icon, or the in-app "Install PWA" button) and interact with the app for a bit — Periodic Background Sync only runs for installed, engaged PWAs.
- Register a periodic task, then open DevTools → Application → Periodic Background Sync and press periodicsync for the task's tag. This works even with a 15-minute frequency because Chrome's 12-hour minimum only applies to the real scheduler, not the DevTools trigger.
- Close the page, trigger periodicsync again, reopen the app: the event log shows the task executed inside the Service Worker (compiled Dart dispatcher), replayed from IndexedDB.
- Web Push: DevTools → Application → Service Workers → Push, or send a
real push from your server. The push payload may contain
{"taskName": "...", "inputData": {...}, "title": "..."}.
Honest limitations #
- No exact scheduling. There is no web API for "run this at 15:00".
registerPeriodicTask(frequency)maps to Periodic Background Sync, which Chrome runs roughly everymax(frequency, 12h)only for installed, engaged PWAs.registerOneOffTask(initialDelay)runs on a page timer while the page is open, and best-effort on the next Service Worker wake after the deadline when it is closed. - Chromium only for Periodic Background Sync (Safari/Firefox do not implement it). In-page Web Worker execution works everywhere.
- Secure context required: HTTPS or
localhost(service workers, periodic sync and push are all gated on this). - The callback dispatcher must be Flutter-free to run inside the Service Worker / Web Worker. Flutter plugins and UI code cannot run there; only the code reachable from your Flutter-free dispatcher file is compiled into the bundle.
- No notification on missed work. If the browser never wakes the Service Worker, a task is simply recorded as missed on the next open.
- One-off tasks are best-effort when the page is closed: they run on the next wake event (periodic sync, push, fetch) after their deadline, which may be much later — or never if no wake happens.
- Experimental: the API and file layout may change. Do not rely on it for production workloads yet.
API #
WorkmanagerWeb mirrors the main package's ergonomics:
initialize(callbackDispatcher, {serviceWorkerUrl, dispatcherUrl, useWebWorker})registerOneOffTask(...)/registerPeriodicTask(...)— same signatures asWorkmanager()executeTask(handler)— used inside the dispatcher (also available asWorkmanagerExecution.instance.executeTask(...)in the compiled bundle)cancelByUniqueName(...)/cancelByTag(...)/cancelAll()isScheduledByUniqueName(...)/printScheduledTasks()triggerTask(...)— run a task now (demos/tests)backgroundEvents— live stream of execution events, including events replayed from the Service WorkersendMessageToWorker(payload)/workerMessages— free-form two-way messaging with the background worker (see "Communicating with the background worker")
iOS-only task types (registerProcessingTask, registerHealthResearchTask,
registerContinuedProcessingTask) throw UnsupportedError on web.
Structure #
lib/
workmanager_web.dart # WorkmanagerWeb (WorkmanagerPlatform impl)
execution.dart # Flutter-free handler registry (used by the bundle)
worker.dart # WorkmanagerWebWorker — bundle entrypoint
src/
worker_protocol.dart # page <-> worker message contract incl. chat messages (unit-tested)
browser_glue_*.dart # js_interop bindings (web) + VM-safe stubs
web/
workmanager_service_worker.js # the Service Worker (copy into your app)
See DESIGN.md for the design decisions and alternatives considered.