
One service worker, thirty content scripts, a single storage layer, and a platform that redesigns itself every few weeks. What building a Manifest V3 extension that survives YouTube actually takes.
The battlefield is the DOM
YouTube is a single-page application that mutates its own DOM continuously, serves different markup to different cohorts, and redesigns its selectors without announcement. A content script that hides Shorts today can silently rot next month. This is the central engineering reality of the whole project: the code doesn't just have to work, it has to fail soft and be cheap to repair.
The architecture answer is layers. core/ holds a storage wrapper, logger, DOM helpers and the sanitizer. blocking/ holds one module per threat — site, ads, shorts, keyword, channel, URL, CSS. youtube/ holds the behavioral controllers like autoplay and quality. controllers/ and ui/ wire the pages together. Each module is small, individually reloadable, and registered in manifest.json in dependency order.
One source of truth
Everything — settings, blocks, ring data, 120 days of history — lives in chrome.storage.local. The popup writes it, the background reads it, content scripts subscribe to it. When you block a site, the popup writes to blockedSites, the background updates declarativeNetRequest rules, and every tab's site blocker hears the change and re-evaluates. No state lives in more than one place, because state that lives in two places lives in neither.
MV3's service worker adds a twist: it sleeps. Aggressively. Timers get clamped — Chrome enforces a 30-second minimum on alarms (a full minute before Chrome 120) — which is how v1.7.0's time-tracking undercount by 30–60× happened. The fix, letting each tick accumulate real elapsed time rather than assuming one second, now rehydrates the active tab whenever the worker wakes.
Performance is a feature
An extension that watches YouTube's DOM can easily become the distraction. Early versions ran a whole-document MutationObserver, a 10-second maintenance interval, and ad-skip polling at 100ms — enough to show up in YouTube's own profiler. v1.0.1 replaced the observer with YouTube's own yt-navigate-finish event, killed the maintenance timer, and settled ad-skip polling at 250ms. The rule that fell out of that release: never poll what the platform gives you an event for.
Failing soft
Every DOM write in the codebase is wrapped, every chrome.runtime.lastError is checked, and every message payload is validated before a handler touches it. Storage changes arrive with guards for cleared keys. When the dashboard can't load data, it shows an error state with a Retry button instead of a silently dead page — a pattern learned from v1.8.0, where two writes to elements that didn't exist could crash rendering before tabs ever bound.
None of this is glamorous. It's the difference between an extension that works for a year and one that works for a week.
The maintenance cost, stated honestly
Selector drift is the project's largest ongoing cost, and the README says so in plain words. That honesty shapes contribution guidelines too: modules stay small, docs live beside code, and every new behavior gets the same try/catch-and-validate treatment as the last one. Fighting an infinite scroll is a marathon, not a hackathon.

