Learn how you can extend Safari's functionality with Safari Web Extensions. We'll introduce you to the latest WebExtension APIs, explore non-persistent background page support — a particularly relevant topic if you're developing for iOS — and discover how you can use the Declarative Net Request WebExtensions API to block content on the web. Finally, we'll show you how to customize tabs in Safari 15.
♪ ♪ Hi. My name is Ellie Epskamp-Hunt. I work as a Safari engineer. Today, I'm giving an overview of some new Web Extensions API avilable in Safari.
Last year, Safari added support for the Web Extensions API on macOS. It's been amazing to see all of the new Safari extensions that have shipped over the past year that use this new API support. And with this release, we're really excited to bring web extensions to iOS and iPadOS.
A persistent background page never closes. So, if I have two extensions turned on in my browser, there'll be two background pages constantly running. And if I use eight extensions, that's eight extension processes running in the background at all times. We can see there's a problem here. Persistent background pages are like these invisible tabs that a user can never close, and they eat up memory and increase CPU usage. Users shouldn't have to make a compromise between using their extensions and getting great performance out of their browser. So instead, extensions can adopt a non-persistent background page. These types of pages can come and go as needed, making your extension more performant and giving your users a better browsing experience overall.
If you're developing for iOS, your extension must have a non-persistent background page because of the resource constraints on iOS devices. So now that we have an understanding of the reasons to use a non-persistent background page, let's take a look at how they work. The lifetime of a non-persistent background page is structured around events. A background page registers event listeners in order to react to things that happen in the browser like a tab closing or a message from another part of the extension. And those events help the browser to determine if your background page should be loaded or unloaded. Let's take a look at an example. When your extension is turned on or updated, your background page will be loaded, and it will register event listeners. For the sake of this example, let's say that this background page has exactly one listener for a message from a content script. If time passes and our content script doesn't send any messages, the background page will be unloaded by the browser because of that inactivity.
But if our content script sends a message, the background page will be woken up so it can receive and react to that message.
And after the event happens, the background page will stay loaded.
But if time passes again and no more events fire, the background page will unload. So with that mental model in mind, we can talk about how to actually implement a non-persistent background page. First, you'll add the "persistent" key to the background section of your manifest. And then you might have to make a couple more changes to your background script. Because your background page can be unloaded, you'll need to use the storage API to write information to disk as needed. Use browser.storage to maintain information across the lifetime of your background page. Next, you'll need to register your event listeners at the top level of your script. Do not register listeners in the completion handler of another event listener. And you'll want to use the browser.alarms API instead of timers. A timer won't be invoked if the background page has unloaded. Now let's talk about some code you want to avoid. Remove calls to browser.extension. getBackgroundPage. It won't wake up the background page if it's already been unloaded. And finally, you'll need to remove any webRequest listeners. webRequest is an API that lets you analyze web traffic, and the frequency at which webRequest events fire make this API incompatible with non-persistent background pages. So to see how this all works together, let's try it out in Safari. I'm using a modified version of the sample code from last year's session about Safari extensions. This extension can replace words in web pages with emoji and reports how many total replacements have happened. First, let's see what this extension does without making any changes. Because we have left out the "persistent" key in the manifest, the background page is persistent by default. I'll build and run the app containing the extension. And then I'll turn it on in Safari's preferences. Now I'll use the extension on a web page. Let's go to this Wikipedia article about fish, and I'll use the popover to interact with the extension.
When I click the "replace words" button, every instance of the word "fish" was replaced with a fish emoji. If I click the popover again, I can see my total number of words that've been replaced. The background page for this extension is in charge of keeping track of that replacement count. Let's head to Activity Monitor to take a look at our extension process. Here we can see the web process where all our extension code is running. Because our extension uses a persistent background page, this process will always be running when Safari is running, even when I stop using this extension hours later. So let's make this extension a little better and make its background page non-persistent. The first thing I'll do is add the "persistent" key to the background section of my manifest. And let's stop here and see if our extension still works. I'll build the app containing my extension. I'll come back to Safari and reload the page. Then I'll replace some words. After that, I'll briefly wait, giving the background page some time to go idle. For the purpose of this demo, I've modified Safari to unload background pages much faster than normal. We can verify that the background page has, in fact, unloaded in the develop menu, under Web Extension Background Pages. This is also where you can inspect your background page. Note that if you choose to inspect the page when it's unloaded, it will immediately load. Now that the background page is unloaded, let's open the popover again. Instead of our expected count of 56, we see zero words replaced. So we've got a bug in our extension. We need to go back and make some more changes so that our extension works correctly with a non-persistent background page. Here we are in the code for the background page of the extension. This background page does two things. It either adds one to the word replacement count, or it reports the current count. The global variable is what's causing our bug. When the background page is reloaded, the count is reset to 0. So instead of maintaining that state that 560 words were replaced, we lose it. So to get around this, let's use the browser.storage API to save and load our word count as needed. First, we'll add some code to load that count from storage.
I'll parse the result from the storage API to get the value that I want.
And I'll save that value back to storage whenever it's updated. And then I'll bring that onMessage listener into the body of the storage callback.
But wait. We've got a problem. We know that event listeners must be registered at the top level of our script, so this isn't going to work. So let's restructure things here and bring the storage call into the body of the listener.
And because we are using the storage API, we need to add the storage permission to the manifest.
Now I'll rebuild the app and test my extension again.
I'll do the exact same thing as before. I'll view that Wikipedia page about fish and reload the page. Then I'll replace some words and wait for a moment, giving our background some time to unload.
Great. Our popover now reports the correct number of words replaced. We took an extension with a persistent background page and successfully converted it to use a non-persistent background page. And if we go back to Activity Monitor, the extension process is no longer present after the background page has unloaded because we did this work to adopt a non-persistent background page. That was an overview of non-persistent background page support in Safari. Remember, if you are developing an extension for iOS, you'll have to adopt a non-persistent background page.
Next, let's take a look at declarative net request, a new content-blocking API. Safari has supported Content Blocker Extensions, built using WebKit Content Rule List, since 2015. There are a couple improvements to them this year, which you can check out in Apple's updated documentation.
However, web extensions haven't had that kind of fast, privacy-preserving, content-blocking capability until now. The declarative net request API, which was recently introduced by Chrome, checks all of those boxes. Let's go over the basics.
There's a unique ID along with a priority, which determines the order in which the rules are applied.
The action piece of the rule allows you to block, allow, or upgrade the scheme of a resource. And the condition is where you tell Safari where and under what conditions to run this rule. In the condition dictionary of this rule, there are two keys. "regexFilter" is matched against the resource URL, and the "resourceTypes" array specifies the types of resources that will be blocked. Let's go into more detail about what's supported in this condition dictionary.
Here are all the resource types you can target using a declarative net request rule.
The "excludedResourceTypes" key lets you specify the types that you don't wanna match against.
The "domainType key" allows you to block a resource based on the relation of the domain of the resource being loaded and the domain of the document. A "first-party" load is any load where the URL has the same security origin as the document. Every other case is "thirdParty." And finally, the "Case-Sensitive" key allows you to control whether the regexFilter is case sensitive or not. By default, it's true.
So now, let's build a web extension that blocks content using the declarative net request API. The first thing I'll do is add a declarative net request section to the manifest. Inside that declarative net request section, I'll add a ruleset by writing an ID, a bool to indicate that it's on, and a path to the JSON file containing my rules. And while we're in the manifest, I'll also add the declarative net request permission. From here, let's go into the ruleset JSON file.
I'll write a rule to block images on all web pages.
I'll build the app containing the extension and open Safari.
Notice how this extension doesn't have the ability to see any browsing history or web page contents, even though it will be able to block content across all web pages. Before I turn on the extension, I'm going to open a WebKit blog post with some images in it. We can see that there are two images on this web page. If I come back to preferences, and turn on the extension, and then reload the page, the images will be blocked.
Now let's go to another web page like this Wikipedia page about fish. Images are also blocked here, but I'd actually prefer if I could see images on this particular page. So let's modify our extension so that images are blocked everywhere except here.
I'll come back to Xcode and write a rule to allow images on this page. The action type of this rule will be "allow," and it will be a higher-priority rule than our first blocking rule. I'll rebuild my app, and then I'll come back to Safari. I'll reload the page.
But this new rule didn't work because I'm still not seeing any images.
I'll look in extension preferences for any error messages. Okay, it looks like I used an empty array for the resource types key instead of an array with the string "image." I'll come back to Xcode to fix my mistake.
I'll rebuild and come back to Safari's preferences to verify that the error message is gone.
Then, I'll reload the page. And great, images are no longer being blocked on this Wikipedia page. So that was an overview of how to build a web extension that can block content on the web. You can consult Apple's documentation for more information on how to use declarative net request. Finally, let's take a look at how extensions can customize new tabs in Safari. We know that users love to personalize their browser, and extensions are a great way to do that. The new tab override API allows extensions to take over the new tab page in Safari and customize it completely. This API is already publicly available in Safari 14.1. New tab overrides are declared in the manifest. And when the user turns on an extension with a new tab override, they make a choice on whether or not to let that extension take over new tabs in Safari.
Here's how you'd point out your new tab override page in the manifest. Let's build an extension that uses this new API together. I'm going to add a new tab override to the Sea Creator extension. Our goal is to have a fun web page appear every time we open a new tab in Safari. I'll start by declaring that my HTML page is a new tab override in the manifest.
I have some existing HTML and CSS files that I'd like to use. They are in my extension's resources folder. I just need to add them to the Xcode project. If you've never added a file to an Xcode project, don't worry. It's pretty easy. I'll click File, Add Files to Sea Creator, and then select the files I want to add, making sure they're a part of the extension target and not the app target.
This HTML creates a colorful page with a fun fact. So let's run the app, and in Safari, I'll turn on the extension.
I get this prompt, asking me if I want this extension to be able to take over my new tabs and windows. I'll allow it to do so.
If I wanted to make changes to this later, I can come into General settings.
But now, when I create new tabs in Safari, my new tab page appears. It looks pretty good! But I wanna make a couple of tweaks. My new tab override page doesn't have a very nice title.
So back in Xcode, I'll add a title so my pages look good in Safari's tab bar.
I can also pick a different theme color if I want something distinct from the one Safari inferred from the page. This meta tag I'm using isn't specific to new tab overrides. It will work on any web page. If you wanna learn more about the changes to Safari's UI, be sure to checkout the session called "Design for Safari 15." Let's see how that looks now. I'll build again.
And back in Safari, I'll create a new tab. Great. We successfully added a new tab override to the Sea Creator extension.
And that was a look at how extensions can customize new tabs in Safari. Today, we discussed three new Web Extension APIs available in Safari on macOS and iOS. I encourage you to download the sample projects associated with this session and play around with the new APIs. I showed you how these extensions work on macOS, but they work on iOS as well.
We'd also love to know what you think. You can use Feedback Assistant to file bugs, or you can come chat with us on the Safari Developer Forums. And finally, check out the other sessions I mentioned today if you haven't already. Thank you and have a great WWDC. [ethereal percussion music]
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.