How Nvirya AdGuard Works

Understanding what Nvirya AdGuard can and cannot do starts with understanding where a userscript sits in your browser's architecture compared to a browser extension or a VPN/DNS filter.

Comparing Filtering Technologies#

Different content-filtering tools operate at different layers of your device and browser stack:

TechnologyTypical CapabilitiesImportant Limitations
Userscript (Nvirya AdGuard)Runs page-level JavaScript (@run-at document-start), injects cosmetic CSS, observes and modifies the page DOM, and wraps JavaScript APIs (fetch, XMLHttpRequest, sendBeacon, window.open) where supportedDepends on injection timing, userscript manager capabilities, browser APIs, Content Security Policy (CSP), and WebKit sandbox isolation; cannot block browser-level network traffic outside JavaScript
Browser extensionCan access privileged browser APIs (such as declarativeNetRequest or webRequest) to cancel or redirect network requests before they enter the page context, alongside content scripts and CSSDepends on browser vendor support (limited or unavailable on many mobile browsers), extension store policies, manifest version restrictions, and granted host permissions
VPN or DNS filtering (Shadowrocket, Surge, Loon, AdGuard DNS)Blocks or redirects domain resolutions and network packets across the entire operating system or network interface before connections reach the browserCannot inspect or modify the page DOM, collapse empty ad frames, unlock page scrolling, click video skip buttons, or distinguish ads served from the same domain as primary content

Because Nvirya AdGuard is a userscript, it excels at in-page DOM cleanup, interactive element hiding, and JavaScript navigation guards, while facing inherent browser constraints when dealing with low-level network traffic.

Three-Phase Startup Architecture#

To reduce page-load overhead, Nvirya AdGuard v11.7.0 divides its initialization (init()) into three sequential phases:

  1. Phase 1 — Critical Core (document-start):
    • Injects the main-world protection script (injectMainWorldShield) when the site is not allowlisted or paused.
    • Installs JavaScript API wrappers for fetch, XMLHttpRequest, navigator.sendBeacon, window.open, document.write, HTMLScriptElement.prototype.src, and HTMLImageElement.prototype.src.
    • Installs capture-phase event guards for invisible click traps, external link redirects, clipboard writes, and tab-visibility spoofing.
    • Strips known tracking query parameters (such as utm_*, fbclid, gclid, msclkid) from the address bar via history.replaceState (cleanTrackingQuery), excluding YouTube domains.
    • Injects cosmetic CSS (installCosmeticCSS) and saved per-site rules (installSiteRulesCSS) as soon as document.documentElement or document.head exists.
  2. Phase 2 — Deferred DOM (DOMContentLoaded or document.body ready):
    • Mounts the floating Quick Action Button (FAB) inside an open Shadow DOM container in the top frame (window.self === window.top). Note that the full bottom-sheet control panel DOM (ensureSheetBuilt) is deferred for up to 3 seconds or until the browser is idle to keep initial page rendering responsive.
    • Starts the batched MutationObserver (startObserver) and runs initialScan() (or initYouTubeShield() on YouTube domains).
    • Hooks single-page application (SPA) history changes (history.pushState, history.replaceState, and popstate).
  3. Phase 3 — Idle Background Tasks (requestIdleCallback or 2,000 ms fallback):
    • Loads lifetime statistics and 14-day weekly history from storage.
    • Schedules bounded anti-adblock fallback scans (600 ms, 1,800 ms, and 4,200 ms).
    • Registers userscript manager menu commands (GM_registerMenuCommand).
    • Checks for script updates (every 30 minutes) and synchronizes remote domain lists (every 24 hours, if configured).

DOM-Level Filtering vs. Network-Level Filtering#

DOM-Level Filtering#

DOM-level filtering targets HTML elements after they are created or inserted into the document tree:

  • Cosmetic CSS Injection: A <style data-nvirya-cosmetic> sheet hides common ad selectors ([id^="google_ads_"], ins.adsbygoogle, [id^="div-gpt-ad"], [data-ad-slot], known ad iframes, and [data-nvirya-hidden]) along with hostname-scoped rules for YouTube, Google Search, Bing Search, Reddit, X/Twitter, Quora, and Facebook mobile.
  • Batched Mutation Observation: Instead of scanning every element on the page on every change, startObserver() collects added nodes and attribute changes (src, href, style, class, id, data-src, data-srcset) up to a bounded queue (MAX_QUEUE = 800) and processes them in time-sliced chunks (processInChunks, up to 8 ms per slice) via requestIdleCallback or requestAnimationFrame. On Google Search and Bing Search, mutations are debounced by 150 ms (RE_SEARCH_DEBOUNCE_HOST) to avoid layout thrashing during live typing and search result updates.
  • Overlay, Backdrop, and Scroll-Lock Recovery: When the script removes an ad modal (removeTrackedAdNode), it captures the surrounding overlay context (captureOverlayRemovalContext), removes paired full-screen dimmers/backdrops (adjacentBackdropNodes), collapses empty bottom sticky ad bars (cleanupBottomStickyAdWrappers), and restores overflow: auto and position: static on <html> and <body> (unlockPageScroll) unless a legitimate modal dialog is open (hasVisibleLegitimateModal).

JavaScript Network Hooks (and Their Limits)#

A userscript cannot access the browser's privileged network stack. Instead, Nvirya AdGuard intercepts requests at the JavaScript API layer in both the userscript context (installNetworkGuard) and the main page context (mainWorldShield):

  • Window.fetch (Fetch API): When a script calls fetch(url) for a URL matching isAdUrl(u) (or RE_TRACKER when Deep Network Filter is enabled), Nvirya AdGuard does not throw a network error that might trigger anti-adblock scripts. Instead, createFakeFetchResponse resolves with a synthetic 200 OK Response object whose body matches the expected content type ({"status":"ok","success":true,"data":[]} for JSON, /* Nvirya Stub OK */ window.canRunAds = true; for JavaScript, or an empty string for plain text).
  • XMLHttpRequest: When xhr.open(method, url) targets a blocked ad or tracker URL, the script redirects the internal open call to an inert data:text/plain, URI and overrides readyState (4), status (200), statusText ("OK"), responseText, and response during xhr.send(), dispatching synthetic readystatechange, load, and loadend events.
  • Navigator.sendBeacon: Blocked beacon payloads return true immediately without transmitting data over the network.
  • Dynamic <script> and <img> Properties: Setting .src or setAttribute('src', ...) on an HTMLScriptElement pointing to a known ad domain sets type = "javascript/blocked" and dispatches a synthetic load event (triggerFakeScriptLoad). Setting .src on an HTMLImageElement pointing to an ad URL spoofs a loaded 32×32 image (complete = true, naturalWidth = 32, naturalHeight = 32) so layout scripts checking image dimensions do not detect a broken load.

What JavaScript hooks cannot intercept:

  • Declarative HTML subresources parsed by the browser before the userscript's hooks execute (such as static <img src="..."> or <iframe src="..."> tags already fetched by the browser's preload scanner, though DOM filtering still hides or removes the resulting elements).
  • CSS network requests (such as background-image: url(...) or @font-face loads).
  • Server-side HTTP redirects (301, 302, 307, 308 headers) or network traffic inside cross-origin iframes where the userscript manager does not inject scripts.
  • Main-world scripts on pages with strict Content Security Policy (CSP) or WebKit sandbox limits that block inline <script> injection (injectMainWorldShield).

Why Streamed Media Ads Differ from Ordinary DOM Ads#

A traditional banner or pop-up ad is a separate DOM element (<iframe>, <ins>, <div>, or <script>) loaded from a distinct third-party advertising domain. Removing the element or returning an empty response for the ad script usually eliminates the advertisement cleanly.

Video advertisements on platforms such as YouTube work differently:

  1. Shared Media Infrastructure: Video segments and player control payloads are often delivered over the same core API endpoints (/youtubei/v1/player, /youtubei/v1/next) and media CDNs (googlevideo.com, youtube.com/videoplayback) that deliver the actual video you want to watch.
  2. Risk of Playback Breakage: If a script blindly blocks googlevideo.com or drops /youtubei/v1/player responses, the video player will stall, show a black screen, or fail with a playback error.
  3. Coordinated Player State: Because Nvirya AdGuard must allow legitimate media streams through (RE_YOUTUBE_CDN_WHITELIST), it handles video ads through two coordinated layers inside initYouTubeShield():
    • Response JSON Pruning (installYouTubeJsonPruner): Intercepts ytInitialPlayerResponse, Response.prototype.json(), Response.prototype.text(), and XMLHttpRequest player responses to empty out adPlacements, playerAds, adSlots, adBreakHeartbeatParams, and adBreakRenderer before the player processes them.
    • Player-State Acceleration (scheduleVideoAdScan): When an ad still enters the HTML5 <video> element (detected via .ad-showing or .ad-interrupting on #movie_player), the script clicks any active skip button, mutes the video, raises playbackRate to 16, and seeks to duration - 0.05 seconds once media metadata is available, restoring your original volume and playback rate afterward.

For more details on how this affects mobile video playback, see Known Limitations and Troubleshooting.