Prompt and applicable scenarios
A same-origin analytics app has three independent requirements:
- Parse a user-selected 200 MiB CSV locally while input and rendering remain responsive.
- Share one WebSocket and in-memory subscription state across tabs while at least one tab remains open.
- Open the app shell and last successful report offline, queue one save, and retry after connectivity returns.
Choose among Dedicated Worker, SharedWorker, and Service Worker for each requirement. Explain ownership, lifetime, communication, persistence, cancellation and failure handling, browser fallbacks, security boundaries, and validation.
The 200 MiB file, single WebSocket, and offline save are interview assumptions, not universal product targets. This question fits senior frontend, Web platform, and frontend architecture roles. Its core skill is browser execution and Web platform selection, so the category is frontend.
What the interviewer evaluates
First, can the candidate choose by who owns the work, how long it must live, and which pages it affects instead of calling every background thread a Web Worker? A strong answer assigns page-owned computation to a Dedicated Worker, temporary same-origin multi-page state to a SharedWorker, and scoped network proxying, caching, and event-driven background work to a Service Worker.
Second, can the candidate separate threading from persistence? Moving code off the main thread does not automatically reduce CPU, memory, or I/O. SharedWorker memory is not durable state. A browser may stop a Service Worker between events, so pending work belongs in durable storage such as IndexedDB.
Third, can the candidate quantify messaging cost? Structured cloning normally copies data. An ArrayBuffer can be transferred so its ownership moves without another byte-for-byte copy, but the sender's buffer becomes detached. Large CSV handling also needs chunking, progress, backpressure, and cancellation.
Fourth, can the candidate provide a correct fallback? SharedWorker reached Baseline 2026 in current browser releases, but older devices and some implementations may not support it. Background Sync is still not Baseline. Correctness cannot depend on one optional API.
Fifth, can the candidate test lifecycle failures? One happy-path click-through is insufficient. Refreshes, closing the final tab, Service Worker updates, offline restarts, duplicate sync events, cache contamination, and older-browser fallbacks all matter.
Questions to clarify first
- Is CSV parsing occasional or continuous? One-off work can create one Dedicated Worker on demand. Continuous concurrent work needs a bounded worker pool and queue instead of one new thread per file.
- Must the entire input be in memory? Prefer streaming or chunked input when the parser supports it. If the whole
ArrayBuffermust move at once, budget separately for raw bytes, decoded strings, parsed values, and indexes. - Are all tabs exactly same-origin? SharedWorker requires the same scheme, host, and port. Subdomains, third-party iframes, or different development ports change the communication design.
- Is one WebSocket an optimization or a correctness constraint? If it only saves connections, a per-tab connection is a valid fallback. If the server permits only one session, the fallback needs BroadcastChannel, leader election, leases, and split-brain recovery.
- May an offline save retry automatically? Sensitive or conflicting operations may require user confirmation after the page returns. Automatic retries need an idempotency key, durable state, and bounded backoff.
- Which older browsers and privacy modes are in scope? That answer sets the feature-detection and fallback boundary for SharedWorker, Background Sync, storage quota, and Service Worker behavior.
30-second answer framework
“I would split the requirements by ownership and lifetime. The 200 MiB CSV is CPU-heavy work owned by the current page, so I would use a Dedicated Worker, parse in chunks, report progress, transfer the ArrayBuffer when ownership can move, and cancel cooperatively or terminate the worker. I would put the one same-origin cross-tab WebSocket in a SharedWorker and let each page subscribe through a MessagePort, with feature detection and a fallback to BroadcastChannel plus leader election or, when acceptable, one connection per tab. I would use a Service Worker for the offline shell, report cache, and later retry because it can intercept scoped requests and handle background events. The queue goes in IndexedDB with idempotency keys. Without Background Sync, the page flushes it on startup, foregrounding, or reconnection. I would test final-tab closure, offline restart, duplicate sync, and Service Worker updates.”
Step-by-step deep dive
Step 1: Choose by ownership, scope, and lifetime
| Requirement | Choice | Owner and communication | Main failure boundary | |---|---|---|---| | Parse a large CSV for the current page | Dedicated Worker | Creating page; postMessage | Page closure, cancellation, memory peak | | Share a connection across same-origin tabs | SharedWorker | Same-origin pages; one MessagePort per page | Final reference closes, old browser lacks support | | Offline shell, network proxy, later sync | Service Worker | Registered origin and path scope; events and client messages | Update waits, termination between events, optional API unavailable |
None of the three can manipulate the DOM directly. Dedicated Worker and SharedWorker run JavaScript work needed by pages. A Service Worker is an event-driven network proxy registered against an origin and path. It can handle fetch, caching, Push, and background sync in supporting browsers, but it is a poor home for a CSV parse that must run continuously for minutes.
Step 2: Isolate the 200 MiB CSV parse in a Dedicated Worker
Create the Dedicated Worker on demand because the task serves only the current page. The main thread owns file selection, progress UI, and result rendering. The worker owns decoding, tokenization, type inference, and aggregation. It has no DOM access, so it returns results through messages.
This interface sketch omits the parser and error types. chunkBytes tells the worker to advance internally in 4 MiB batches; it does not mean the main thread makes another copy:
const parser = new Worker(
new URL("./csv-parser.worker.js", import.meta.url),
{ type: "module" },
);
const jobId = crypto.randomUUID();
const buffer = await file.arrayBuffer();
parser.postMessage(
{ type: "parse", jobId, buffer, chunkBytes: 4 * 1024 * 1024 },
[buffer],
);
parser.onmessage = ({ data }) => {
if (data.jobId !== jobId) return;
if (data.type === "progress") renderProgress(data.rows);
if (data.type === "done") renderReport(data.summary);
};
cancelButton.onclick = () => {
parser.postMessage({ type: "cancel", jobId });
};Cancellation can be cooperative or forceful. The parse loop checks a cancellation flag at chunk boundaries, releases temporary objects, and reports a final state. If the worker hangs or the parser cannot yield, terminate() stops it, but every job inside that worker is lost. A long-lived multi-file processor therefore needs job isolation rather than one undifferentiated shared worker.
Step 3: Design for copying and peak memory
Ordinary postMessage uses structured cloning. Under a simplified assumption where a 200 MiB ArrayBuffer exists in both sender and receiver, raw bytes alone occupy about 400 MiB. That excludes the File backing store, decoded strings, row objects, indexes, and garbage-collection headroom. This is an interview derivation, not a browser memory guarantee.
Placing the ArrayBuffer in the transfer list moves its memory resource to the worker and detaches the sender's buffer, avoiding that byte copy. Do not transfer it if the page still needs the original and ownership has not been designed. A safer large-input path reads Blob.stream() or slices, sends small chunks, and receives incremental statistics. The main thread limits chunks in flight and sends the next only after an acknowledgment, which creates backpressure.
Shared memory is not the default shortcut. SharedArrayBuffer requires cross-origin isolation and introduces atomics, data races, and stricter deployment security. Do not add that complexity before measurements show ordinary messages and transferable objects are the bottleneck.
Step 4: Keep the same-origin multi-tab connection in a SharedWorker
SharedWorker matches “keep one shared instance while at least one same-origin page remains connected.” Each tab opens the same named worker at the same URL and registers subscriptions over its own MessagePort. The worker owns the WebSocket, a set of ports, and an in-memory subscription map, then routes server messages to interested ports.
const socketHub = new SharedWorker(
new URL("./socket-hub.shared-worker.js", import.meta.url),
{ type: "module", name: "analytics-socket-hub" },
);
socketHub.port.start();
socketHub.port.postMessage({ type: "subscribe", reportId });
socketHub.port.onmessage = ({ data }) => applyLiveUpdate(data);
window.addEventListener("pagehide", () => {
socketHub.port.postMessage({ type: "disconnect", reportId });
socketHub.port.close();
});Only exact same-origin contexts can access the SharedWorker. It may stay alive while an open page holds a reference; after the last reference closes, it is not a permanent background service. Connection loss, browser exit, or a worker crash may erase the subscription map. Persist server cursors that must survive, or let every page redeclare its subscriptions.
Step 5: Give SharedWorker a real fallback
Feature-detect SharedWorker. Without it, tabs can exchange heartbeats and events over BroadcastChannel, while Web Locks or an expiring storage lease elects one leader to own the WebSocket. Other tabs elect a replacement after the leader closes. The server deduplicates by session and event ID, while clients reject messages from an old leader using a monotonic epoch to limit split-brain damage.
That fallback is substantially more complex than SharedWorker. If one connection is only a cost optimization, the simplest correct fallback is one WebSocket per tab with server-side idempotency and connection limits. Build election, lease renewal, failure detection, and fault injection only when a hard server constraint requires one connection.
Step 6: Use a Service Worker for offline behavior and retries
A Service Worker is registered against an origin and path scope and can intercept requests from controlled pages. The app shell suits versioned precaching or cache-first. The latest report often suits network-first: update Cache Storage on a successful network response and return the last successful cached response on failure. Put the user's save queue in IndexedDB, not Cache Storage or a Service Worker global variable.
This is still an interface sketch. Production code must allowlist cacheable URLs, validate responses, version caches, and write the save to IndexedDB before showing “queued”:
navigator.serviceWorker.register("/sw.js", { scope: "/" });
// sw.js
self.addEventListener("install", (event) => {
event.waitUntil(cacheAppShell());
});
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
if (url.pathname.startsWith("/reports/")) {
event.respondWith(networkFirstReport(event.request));
}
});
self.addEventListener("sync", (event) => {
if (event.tag === "retry-report-saves") {
event.waitUntil(flushIndexedDbQueue());
}
});Background Sync is available only in some browsers. If registration fails or the API is absent, call the same queue flusher when the page starts, returns to the foreground, or receives an online event. Connectivity events are retry triggers, not proof of success; only the server response confirms completion. Each save stores an idempotency key, attempt count, and next retry time. Conflicts enter a review state instead of overwriting forever.
Step 7: Handle Service Worker lifetime and updates
A Service Worker controls pages only after download, installation, waiting, and activation. After first registration, the current document commonly needs another navigation before it becomes controlled. A new version may install and wait for pages using the old version to close. skipWaiting() and clients.claim() can take control faster, but an old page paired with a new worker can break when protocols differ. Version message and cache schemas accordingly.
The browser may terminate a worker between events and restart it for the next event. event.waitUntil() extends the lifetime associated with the current event's Promise; it does not create a resident process. Persist the queue, attempt count, and idempotency key. Sync processing must resume from any record and safely run the same request twice.
Service Workers require a secure context. HTTPS is the production requirement, while localhost is a development exception. The default scope is constrained by the script path. Also verify CSP worker-src, user-sensitive cache contents, logout isolation, and CORS and credential behavior for cross-origin requests.
Step 8: Validate with a failure matrix
Test the Dedicated Worker with 1 MiB and 200 MiB files, malformed input, and cancellation. Record main-thread long tasks, input delay, parse throughput, peak memory, and release time after cancellation. Compare structured clone, transferable, and chunked variants, then choose the measured bottleneck.
Test SharedWorker with two simultaneous subscriptions, closing the leader page, closing the final page, network flaps, worker errors, and a browser without SharedWorker. Acceptance checks include the actual server connection count, no unexpected message gaps or duplicates, cursor continuity after reconnect, and a fallback that preserves correctness.
Test Service Worker on first visit, controlled revisit, offline refresh, cache expiry, a waiting update, forced browser shutdown, duplicate sync, exhausted storage quota, and missing Background Sync. Use idempotency keys at the save endpoint to prove that at-least-once attempts do not create duplicate writes. Verify that cached private data cannot cross users.
High-quality sample answer
“I would begin with who owns the task, how long it must live, and whether it proxies the network. The 200 MiB CSV serves only the current page and is CPU-heavy, so I would use a Dedicated Worker. The main thread handles file selection and UI; the worker parses chunks, reports progress, and checks cancellation between chunks. Ordinary messaging uses structured clone. If a complete 200 MiB buffer exists on both sides, the interview assumption puts raw bytes alone near 400 MiB, so I would benchmark a transferable ArrayBuffer that moves ownership or stream chunks with a bounded number in flight.
I would keep the one cross-tab WebSocket in a SharedWorker. Every page must be exactly same-origin and redeclares subscriptions through its MessagePort. The worker stores only reconstructable connection and routing state. It is not persistence; state may disappear after the final page closes, the browser exits, or the worker crashes. I would feature-detect it. If one connection is only an optimization, the fallback is one connection per tab. If it is a hard constraint, I would use BroadcastChannel plus lease-based election, with epochs and event IDs for split brain and duplicates.
I would use a Service Worker for the offline shell, latest report, and later save. It intercepts requests within scope: versioned precaching for the shell and network-first with cache fallback for the report. The save is written to IndexedDB with an idempotency key before background sync is registered. Without Background Sync, startup, foregrounding, or reconnect invokes the same flusher. Because the Service Worker may stop between events, the queue never lives only in a global; each run recovers durable state and associates current work with waitUntil().
Finally, I would validate main-thread long tasks and memory, the true WebSocket connection count and reconnect behavior, offline restart and duplicate sync, plus Service Worker update waiting, old-browser fallbacks, per-user cache isolation, and HTTPS and CSP boundaries.”
Common mistakes
- Using a Service Worker as a long-running compute thread → the browser may terminate it between events or during long work → use a Dedicated Worker for page computation and reserve Service Worker for event-driven network work.
- Sending a 200 MiB object with
postMessagewithout measuring memory → structured clone and parsed results may coexist at a large peak → compare transferable, chunked, and streaming paths and measure the peak. - Assuming any worker guarantees a responsive app → CPU, garbage collection, and serialization still cost time → measure main-thread long tasks, throughput, and message batch size.
- Treating SharedWorker memory as reliable state → it may vanish after the final reference closes or the browser exits → keep only reconstructable runtime state and persist critical cursors and queues.
- Ignoring exact same-origin requirements → a different scheme, host, or port cannot share it → map origin boundaries before choosing client-side coordination.
- Copying a complex election system whenever SharedWorker is unavailable → the business may need correctness, not exactly one connection → decide whether the connection count is a hard constraint and otherwise fall back per tab.
- Keeping an offline queue in a Service Worker global → a worker restart loses the jobs → write IndexedDB first and invoke a replay-safe flusher.
- Depending only on Background Sync → it is still not a Baseline feature supported by every major browser → also retry on startup, foregrounding, and reconnection.
- Forcing every update to take control immediately → an old page and new worker may have incompatible protocols → version messages, caches, and migrations before choosing to skip waiting.
- Caching every successful response → private data may persist across accounts or be reused incorrectly → use a URL allowlist, per-user isolation, logout cleanup, and response validation.
Follow-up questions and responses
Follow-up 1: What changes when the CSV grows to 2 GiB and mobile memory is insufficient?
Stop calling file.arrayBuffer() for the full input. Read Blob.stream() or slice() chunks, retain a partial row across decode boundaries, and let the worker return aggregates or columnar batches. Bound chunks in flight and require acknowledgments for backpressure. Expose cancellation points. If the product must retain every row, spill to IndexedDB or segmented files and page the results instead of keeping the complete object graph on the JavaScript heap. Add a device-specific limit and an explicit rejection path.
Follow-up 2: What if pages on different subdomains must share exactly one WebSocket?
SharedWorker cannot cross exact origin boundaries. One option is a controlled origin that owns the live connection and exposes an audited iframe message bridge, with strict targetOrigin, sender, and schema validation. Another is moving single-connection coordination to the server while each page keeps an independent client connection. The first couples security and availability; the second costs more server connections. Choose from the actual hard constraint rather than trying to bypass same-origin policy.
Follow-up 3: SharedWorker is Baseline 2026, so why keep a fallback?
Use the product's browser matrix. Baseline 2026 describes a newly common capability across current browser releases; it does not cover every old device, frozen enterprise version, privacy mode, or embedded environment. Feature detection is cheap. Start with a per-tab connection as the fallback and build election only if a hard connection constraint justifies it.
Follow-up 4: How do you prevent duplicate writes when Background Sync repeats a save?
The client creates one stable idempotency key for the business operation. The queue stores at least its payload, key, attempt count, and next retry time. The server enforces uniqueness or stores a result by user and key, returning the original result for a duplicate. The client deletes the queue item only after confirmation. A timeout means unknown outcome and a retry with the same key, never a new key. Business-version conflicts move to review instead of silent overwrite.
Follow-up 5: A new Service Worker is installed, but users keep an old page open indefinitely. What do you do?
Notify controlled pages that an update is ready and let the user refresh at a safe point. A security fix can use skipWaiting() and clients.claim() only when old and new pages remain compatible with the worker's message protocol and cache and IndexedDB migrations are reentrant. Release tests must exercise an old page with the new worker. When compatibility cannot be guaranteed, keep the waiting phase instead of optimizing only for immediate activation.
Follow-up 6: Why not use a Service Worker to share the WebSocket across tabs too?
Service Worker lifetime is event-driven, and the browser may stop it when idle. A WebSocket requires a continuous connection and in-memory session, which conflicts with that model. A SharedWorker is a better owner while page references remain. If the target browser lacks SharedWorker and one connection is mandatory, use cross-tab election or change the server architecture rather than assuming the Service Worker stays resident.