The Safari bug that punishes you for using content blockers

,

Running content blockers in your browser is a good idea–yes, you block ads, but you also block malware, cryptominers, tracking, and more. In general, the experience of using the web with a good content blocker should be noticeably faster, as you skip downloading and executing many megabytes of code, images, and videos that provides you with no benefit. I’ve run content blocking extensions since they were first introduced and I’ve never looked back.

Safari 15, released in September 2021, was the first version to support extensions, including content blocking extensions. This was a huge improvement to mobile browsing on iOS, which doesn’t offer any choice of browser besides Safari. Safari finally had a way to fight back against intrusive ads just like other browsers. I chose to use 1Blocker, which was highly reviewed by Apple aficionados at the time, though I’m sure there are other good alternatives.

How Content Blockers Work

The earlier content blockers used Firefox’s Addons or Chrome’s Extensions spec to intercept web requests. Each request first had to pass through a bit of JavaScript in the extension to see if it would be allowed or not. This enables extremely powerful blocking based not just on lists of “bad” URLs–the extension has the chance to block based on all kinds of complex heuristics.

There two big problems with this approach. First, and most importantly, you have to trust these extensions. Since they get to run whatever JavaScript they want, they can also smuggle your browsing history off to a third party, turning what should be a privacy improvement into a privacy disaster. I don’t know that the reputable extensions ever had problems with this, but I’m sure that many extensions were siphoning off all their user’s browser history. The second issue is that, since the extension runs any JavaScript it wants, in the path of loading any request, it could be a performance (and in turn, battery) problem. Inefficient code could slow down browsing.

As a replacement, Google and Apple both switched to a much more constrained blocking API, which is pretty much just a denylist. Instead of intercepting every request and running arbitrary JavaScript, extensions can only register a list of regular expressions for things to block, and the browser evaluates whether a request matches anything on the list. This is strictly less powerful than the old API, but it does not offer any opportunity for extensions to spy on users.

Unfortunately, the developers of Safari made a mistake in implementing these declarative filters (or made a conscious decision based on tradeoffs that are not immediately clear to me).

How Other Browsers Check Content Blocker Rules

It is extremely common these days to render all or part of a web page using JavaScript in the user’s browser. That means that code on the page is inserting DOM nodes that refer to images, videos, scripts, stylesheets, etc.

In browsers like Chrome and Firefox, the process works a bit like this:

In Chrome and Firefox the blocking task is short, and content blockers are consulted in the background while loading images.

Some JavaScript code inserts a DOM node, say an IMG node, and sets its src attribute. The browser goes to fetch the image itself from the network, and in the background (off the main thread that runs the interactive UI) it does everything needed to load the image–checking the cache, resolving DNS, establishing a connection, downloading the file, and so on. And before it does any of that, it checks the content blocking rules to see if it should be allowed to load the file at all. This check also happens in the background as part of fulfilling the network request. So the DOM node is inserted, it is in a “not yet loaded” state, the browser attempts to fetch the resource in the background, it gets blocked by the content blocking rules. The image fails to load. Same thing for scripts or videos or anything else.

How Safari Checks Rules

Safari, as far as I can tell, does this very differently. When some JavaScript code inserts an IMG node, Safari blocks the main thread to check and see if the image’s source is blocked by the content blocking rules. It performs the content blocking check right then and there when you call insertNode, before anything else can happen. Then, after that check is done, JavaScript can continue running. Like with other browsers, the actual network request (cache, DNS, connection, download) happens on the background.

In Safari the blocking task is long, because it consults content blockers every time an image node is added.

These checks add up when you have very large lists of content blocking rules–and every content blocker extension has a ton of rules. 1Blocker has over 100,000 rules broken up over several different lists. No matter how much you optimize the process for matching against these lists, it takes time to go through them all for every request. That’s true for any browser, but Safari is the only one that blocks the main thread while doing the check. The result is noticeably slower websites, that are not interactive while loading these resources–you can’t scroll, you can’t click, nothing happens until the content blocker rules have been checked. Chrome and Firefox users are certainly loading their images a bit slower because of blocking rules, but they do so in parallel, while the page continues to work. Safari checks each resource one by one, on the same thread that’s supposed to be doing all the other work on the page.

We can demonstrate this easily. I’ve written a small demo app that does so. Open that up, hit the “Do it” button, and it’ll use JavaScript to insert a few hundred images into the DOM. On Chrome or Firefox, even with content blockers enabled, the DOM nodes are inserted in a few milliseconds (on my powerful laptop it takes 2ms). However, open this up in Safari with a content blocker like 1Blocker enabled, and it takes over a second–on my M1 Max CPU it takes 1230ms, or 600 times as long as on Chrome. That’s almost 2.5ms per image.

What does this mean? Not many websites load 500 images in a single loop, so you’re probably not seeing these page-pausing latencies that often. But 2.5ms per image adds up. How many sites load 10 images at once? 100? At that point you’re already up to a quarter second, which is a noticeable hitch. This bug is effectively punishing you for running a content blocker by adding a little pause here, a little pause there. And if a site loads a lot of images at once (say, a bunch of photo thumbnails) you might really feel it.

I’ve filed this bug to track the issue. This bug has existed for over three years, and it’s been over a year since I filed the issue, and as of Safari 18.1 there’s no improvement. It’s worth calling out that my explanation above is an assumption of what’s happening based on what I’m seeing–I’ve tried to read through the WebKit sources to find where it’s checking content blockers, but I don’t know the codebase and my searches haven’t come up with conclusive proof. If you know how these actually work, and can point me to the code, I’d be grateful and will update the post. In the meantime, be aware that your browsing and web apps on Safari run slower because you’re using a content blocker.

P.S.: In DIM, I ended up having to work around this by not inserting images until they’re on-screen (using an IntersectionObserver), but that’s not a workaround I’m happy to have to use everywhere I display a lot of images.

I'm Benjamin Hollis, a software developer in Seattle. Check out my website.