Back to articles

declarativeNetRequest in browser extensions: what a peer-reviewed study says about MV3 ad blocking

A peer-reviewed study found Manifest V3 ad blocking matches MV2 effectiveness. Here's what declarativeNetRequest really changes if you build content filters.

Maxim Kosterin
10 min read

If you build browser extensions that touch network requests — ad blockers, privacy tools, content filters, parental controls — you've probably spent the last two years worrying about Manifest V3 killing your product. The webRequest blocking API was your Swiss army knife, and Google replaced it with declarativeNetRequest, a declarative system with hard rule limits and no dynamic inspection.

The internet consensus? MV3 destroys ad blockers. Google did this on purpose. Privacy tools are dead.

Except a peer-reviewed study just proved that's not what's happening.

The study that changes the conversation

Researchers Karlo Lukic and Lazaros Papadopoulos at Goethe University Frankfurt published a paper in Proceedings on Privacy Enhancing Technologies (PoPETs) 2026 that directly tested whether MV3 ad blockers are worse than their MV2 predecessors. Their conclusion: no statistically significant reduction in effectiveness.

Let that sink in. This isn't a blog post or a Twitter thread. It's an independently funded, peer-reviewed study from a respected privacy research venue — unaffiliated with Google or any ad blocker vendor.

They tested four widely used ad blockers: Adblock Plus, AdGuard, Stands, and uBlock Origin. The MV2 versions used the synchronous chrome.webRequest API. The MV3 versions used the asynchronous chrome.declarativeNetRequest API. Both ran with default filter sets to replicate what a typical user would experience.

The result? MV3 versions actually blocked slightly more trackers — an average of 1.8 additional tracking scripts per website.

Why this matters if you're building extensions

If you're an extension developer who's been holding off on migrating to MV3, this study should recalibrate your priorities. The data suggests your users won't notice a downgrade in filtering quality — if you've done the migration work properly.

But "properly" is the key word here. The declarativeNetRequest API is fundamentally different from webRequest, and understanding those differences is what separates a clean migration from a broken one.

The old way: webRequest

With MV2, you could intercept any network request in real time, inspect its headers, modify or block it on the fly, and make decisions based on dynamic state. Here's a simplified pattern:

chrome.webRequest.onBeforeRequest.addListener(
  (details) => {
    if (shouldBlock(details.url, details.type)) {
      return { cancel: true };
    }
  },
  { urls: ["<all_urls>"] },
  ["blocking"]
);

This was powerful. It was also a security and performance concern — any extension with webRequest blocking could silently intercept and modify every single HTTP request your browser made.

The new way: declarativeNetRequest

MV3's declarativeNetRequest (DNR) flips the model. Instead of running your code on every request, you declare rules upfront and the browser engine evaluates them natively. You never see the actual request data:

// In your rules.json
[
  {
    "id": 1,
    "priority": 1,
    "action": { "type": "block" },
    "condition": {
      "urlFilter": "||tracker.example.com",
      "resourceTypes": ["script", "image", "xmlhttprequest"]
    }
  }
]

And in your manifest:

{
  "declarative_net_request": {
    "rule_resources": [{
      "id": "blocklist",
      "enabled": true,
      "path": "rules.json"
    }]
  }
}

The browser handles the matching. Your extension never touches request data. That's better for user privacy and browser performance — but it means you lose the ability to make context-aware decisions at runtime.

The rule limit problem (and why it's smaller than you think)

The biggest complaint about DNR has been the rule limit. When declarativeNetRequest first launched, the static rule cap was 30,000 rules. For context, uBlock Origin's default filter lists contain well over 300,000 rules. That's a ten-to-one gap.

Google heard the feedback. The guaranteed minimum for static rules is now 330,000, with browsers often allowing more depending on available memory. Dynamic and session rules have separate limits (check chrome.declarativeNetRequest.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES for the current value in your target browser).

Here's the thing: most extensions aren't uBlock Origin. If you're building a corporate content filter, a parental control tool, or even a moderate ad blocker, you're probably working with thousands of rules, not hundreds of thousands. The 330,000 limit is generous for the vast majority of use cases.

And even for massive filter lists, the Goethe study shows that well-optimized rule sets under DNR perform just as well as the old approach. The trick is in the optimization, not in the API surface area.

Practical migration patterns for DNR

If you're ready to move from webRequest to declarativeNetRequest, here are the patterns I've seen work best.

Static rules for known filter lists

If your blocking logic is based on domain lists or URL patterns that don't change at runtime, static rulesets are your primary tool. Define them in JSON, reference them in your manifest, and let the browser handle matching:

// manifest.json
{
  "permissions": ["declarativeNetRequest"],
  "declarative_net_request": {
    "rule_resources": [
      {
        "id": "ads",
        "enabled": true,
        "path": "rules/ads.json"
      },
      {
        "id": "trackers",
        "enabled": true,
        "path": "rules/trackers.json"
      }
    ]
  }
}

Split your rules into logical rulesets. Users can toggle categories on and off, and you stay under per-ruleset limits more easily.

Dynamic rules for user customization

Users want to add custom block rules or allowlist specific sites. That's where dynamic rules come in:

async function addUserBlockRule(domain) {
  const currentRules = await chrome.declarativeNetRequest.getDynamicRules();
  const nextId = currentRules.length > 0
    ? Math.max(...currentRules.map(r => r.id)) + 1
    : 1;
 
  await chrome.declarativeNetRequest.updateDynamicRules({
    addRules: [{
      id: nextId,
      priority: 2,
      action: { type: "block" },
      condition: {
        urlFilter: `||${domain}`,
        resourceTypes: [
          "script", "image", "stylesheet",
          "xmlhttprequest", "sub_frame"
        ]
      }
    }]
  });
}

Dynamic rules persist across browser sessions but count against a separate, smaller limit. Use them for user-specific customizations, not your core filter list.

Session rules for temporary state

Need to block something temporarily — during a browsing session, for a specific tab, or based on a toggle? Session rules disappear when the browser closes:

await chrome.declarativeNetRequest.updateSessionRules({
  addRules: [{
    id: 9999,
    priority: 3,
    action: { type: "block" },
    condition: {
      urlFilter: "||ads.tempsite.com",
      resourceTypes: ["script"],
      tabIds: [activeTabId]
    }
  }]
});

This is useful for "block this element" features or per-tab configurations. No cleanup needed — the browser handles it.

The caveats the study didn't cover

The Goethe study is solid, but it has acknowledged limitations that matter for developers.

First, they used default filter sets. If your extension relies on advanced, regularly updated filter lists with hundreds of thousands of rules, the DNR rule limits might still bite. The study didn't test whether less popular or long-tail websites get worse filtering under DNR constraints.

Second, this is a snapshot. As the researchers note, Chrome could tighten or loosen MV3 constraints in future releases. Building your extension architecture to handle rule limit changes — with graceful degradation for less critical rules — is worth the engineering effort now.

Third, uBlock Origin's full version is no longer available on Chrome. Raymond Hill maintains uBlock Origin Lite (MV3) for Chromium browsers, but the full-featured version lives on Firefox. If your extension has similar complexity, plan for Firefox as a first-class target, not an afterthought.

Targeting both engines is also getting easier in a way that's easy to miss: Chrome is standardising on the browser.* namespace, which I covered in the Chrome 148 browser namespace change. Writing against browser.* today means one less shim in your build when you ship the Firefox target.

What to do right now

If you're an extension developer working with network request filtering, here's my recommended sequence:

  1. Audit your current rule count. Export your filter lists and count the rules. If you're under 100,000, the DNR migration is straightforward. If you're above 300,000, you'll need to prioritize and segment your rules.

  2. Split rules into logical rulesets. Categories like "ads," "trackers," "social widgets," and "custom" map cleanly to separate ruleset files. This gives users control and keeps you under per-ruleset limits.

  3. Use dynamic rules sparingly. Reserve them for user-generated content — custom blocks, allowlists, temporary overrides. Don't use dynamic rules for your core filter list.

  4. Test on real websites, not synthetic benchmarks. The Goethe study's strength was testing on actual ad-supported websites. Your QA should mirror that approach.

  5. Track your extension's performance on Extenshi's catalog. The security and privacy scoring can help you understand how your permissions profile compares to competitors — and what your users see before they install.

  6. Validate the manifest and scan the build before you upload. My free Manifest V3 generator checks your declarative_net_request block against store policy, and @extenshi/cli (npx @extenshi/cli) runs a pre-publish security scan straight from your terminal or CI pipeline. You get 3 scans and 10 reads free, one-time; prepaid credit packs cover you past that.

FAQ: Manifest V3 ad blocking

Which is the best Manifest V3 ad blocker right now? For end users on Chromium, uBlock Origin Lite and AdGuard are the two that consistently show up as the serious MV3-native options, and the Goethe study's numbers put the MV3 builds of the tested blockers at parity with their MV2 predecessors. As a developer the more useful reading is why they hold up: both invested in rule-set optimisation rather than trying to reproduce webRequest semantics with declarativeNetRequest.

Why isn't my ad blocker blocking everything anymore? Nine times out of ten it isn't DNR's fault. First-party ad serving — where the ad is delivered from the same domain as the content — defeats URL-pattern filtering regardless of which API does the matching. Cosmetic filtering (hiding an element that already loaded) is also unaffected by MV3 and is still handled by content scripts. If your users report gaps, check whether the missing block was ever a network rule to begin with.

Is declarativeNetRequest slower than webRequest? The opposite, generally. Matching happens in the browser's native code instead of round-tripping into your JavaScript for every request, so the per-request cost drops and your service worker doesn't have to stay alive to service traffic. What you lose is not speed, it's runtime context.

Can I still inspect request bodies or headers? Not for blocking decisions. You can observe requests with the non-blocking webRequest listeners, and you can modify headers declaratively with modifyHeaders rules, but you cannot read a request, run arbitrary logic, and then decide. If your product genuinely needs that — a debugging proxy, some enterprise DLP tooling — DNR is the wrong tool and you should plan around a native host or Firefox instead.

Do the rule limits apply per extension or per ruleset? Both, and they're separate budgets. Static rules have a global guaranteed minimum (now 330,000) plus per-ruleset caps, while dynamic and session rules draw on their own smaller pool. Read the current value from chrome.declarativeNetRequest.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES at runtime rather than hardcoding it — it has moved before and it will move again.

The MV3 migration isn't optional anymore

With Manifest V2 fully disabled in Chrome 138 and complete removal coming in Chrome 139 by June 2026, this isn't a theoretical exercise. If your extension still uses webRequest blocking on Chrome, you're on borrowed time — and I walked through what the final removal milestone actually breaks in the Chrome 150 MV2 removal piece.

The good news from the Goethe study is that the migration doesn't have to mean a worse product. Your MV3 extension can match — and according to the data, occasionally beat — your MV2 version's filtering effectiveness. The declarativeNetRequest API is different, not worse. It just requires a different way of thinking about network filtering.

Stop treating MV3 as a loss and start treating it as a different architecture. The research backs you up.

Want to see how your extension stacks up? Search the Extenshi catalog to see your extension's security score, permission breakdown, and how it compares to alternatives in your category.


This article is based on publicly available security research and news reporting. Extenshi does not independently verify all claims made by third-party researchers. References to specific companies or products reflect the findings of cited sources and do not constitute accusations of intentional wrongdoing. If you believe any information is inaccurate, please contact us at [email protected].

Related Articles