By mid-afternoon, my Chrome usually had 40+ tabs open across three windows. I tried several tab managers, but they either demanded a cloud account, locked basic features behind a subscription, or only solved one half of the problem.
So I spent the last few months building my own: PYLON, an offline-first New Tab that pairs project tab workspaces with dev tools and a deep-focus Pomodoro timer.
This post is about one specific engineering challenge: the site blocker.
The hardest part wasn’t intercepting requests — it was deciding what the source of truth should be when a Manifest V3 background service worker constantly terminates and restarts.
The setup
When a focus session starts, the extension redirects configured distraction sites to a local focus-intercept.html page bundled inside the extension.
I chose Chrome's modern declarativeNetRequest (DNR) API over old blocking webRequests so that the browser itself evaluates rules at native speed without injecting custom JavaScript into the request path.
Three pieces of state have to stay consistent:
| What | Where it lives | Why |
|---|---|---|
| Blocked domains | chrome.storage.local |
Survives restarts, handles crashes, easily exported |
| Active redirect rules | DNR Dynamic Rules | Evaluated directly by the browser network stack |
| Rule IDs | A reserved ID range (1000–1999) |
Ensures we only delete focus rules without touching other extension rules |
My core rule: chrome.storage.local is the sole source of truth. DNR rules are merely a temporary mirror of it.
Step 1: Clean and normalize user input
Users paste whatever happens to be in their address bar:
https://www.youtube.com/watch?v=...reddit.com/r/webdevx.com:443
Because DNR rules match against hostname patterns (typically using ||domain.com^), passing raw URLs will break rule matching. Every entry must be cleaned to a bare domain before touching storage:
const cleaned = Array.from(new Set((domains || [])
.map(d => String(d || '').trim().toLowerCase()
.replace(/^https?:\/\//, '') // drop the protocol
.replace(/\/.*$/, '') // drop any trailing path
.split(':')[0].split('?')[0]) // drop port numbers and query parameters
.filter(Boolean)));
Step 2: Rebuild the rules — don't patch them
My initial instinct was incremental updates: add a rule when a user adds a domain, remove one when deleted.
In Manifest V3, incremental patching is an anti-pattern.
Service workers can terminate at any point of idle time. If an event wakes the worker while a write is resolving, or if two storage events trigger simultaneously, incremental updates can leave stale, orphaned rule IDs in the browser engine.
Instead, I adopted an idempotent reconciliation pattern:
- Read existing dynamic rules.
- Filter for rule IDs belonging to my reserved range.
- In a single atomic
updateDynamicRules()call, delete all stale IDs and re-apply fresh rules generated from storage. - Retry with backoff if Chrome's internal database locks.
async function reconcileDnrWithStorage(expectedDomains, retryCount = 0) {
try {
const existing = await chrome.declarativeNetRequest.getDynamicRules();
const staleIds = existing
.map(r => r.id)
.filter(id => id >= FOCUS_RULE_ID_START && id <= FOCUS_RULE_ID_END);
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: staleIds,
addRules: buildShieldRules(expectedDomains || [])
});
} catch (e) {
if (retryCount < 3) {
await new Promise(r => setTimeout(r, 500));
await reconcileDnrWithStorage(expectedDomains, retryCount + 1);
}
}
}
If the focus session ends, passing an empty array automatically clears all rules in that range.
Step 3: Sweeping already-open tabs
One gotcha with declarativeNetRequest: redirect rules only apply to new network requests.
If a user already has YouTube open in a tab and starts a focus sprint, that tab remains accessible until they click a link or refresh.
To make the block instant, the activation sequence performs a quick tab sweep right after reconciling DNR rules:
const tabs = await chrome.tabs.query({});
for (const tab of tabs) {
if (tab.url && isDomainBlocked(tab.url, expectedDomains)) {
chrome.tabs.update(tab.id, {
url: chrome.runtime.getURL('focus-intercept.html')
});
}
}
Questions for other extension developers:
Building on Manifest V3 has been an interesting exercise in distributed-systems thinking inside a single browser:
- Retry Strategy: Is a static 500ms delay sufficient for DNR rule updates, or have you found exponential backoff necessary under heavy browser load?
-
Reconciliation Frequency: Do you reconcile DNR rules on every service worker startup (
chrome.runtime.onStartup), or strictly when storage change listeners fire? - Race Condition Testing: Has anyone established a reliable local testing setup for simulating abrupt service worker termination during active DNR writes?
If you've tackled DNR sync differently, I’d love to hear your approach in the comments.
Try it out:
If you're looking for an offline-first tab manager with native Chrome Tab Groups sync, developer tools (CORS toggle, localhost port monitor), and this focus shield:
👉 PYLON on the Chrome Web Store (Free, 100% offline, zero accounts/tracking)

Top comments (1)
I'd test a stale retry before tuning the backoff: start focus, force that reconciliation to fail once, then end focus while its 500ms retry is waiting. The retry still carries the old
expectedDomains, so can it reinstall blocking rules after the newer empty-array update? An atomic DNR call doesn't by itself order separate reconciliations. Re-reading current state or rejecting an obsolete generation would make that fixture useful. Also, after the final retry fails, the function currently returns without surfacing failure; does activation still sweep tabs and show the session as protected?