Download
♪ ♪
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.
You can learn more about extensions
on these new platforms in its own dedicated session,
"Meet Safari Web Extensions on iOS."
And if you wanna learn more
about Safari Web Extensions in general,
you can check out last year's session.
Today, we're covering three new extension APIs.
First, we'll talk about non-persistent background pages,
which are a way to structure your extension
for better performance.
Then, I'll introduce a content-blocking API
for web extensions called declarative net request.
And at the end, we'll look at how extensions
can customize new tabs in Safari.
Before we learn more about this new API,
let's talk about persistent background pages.
Web extensions are made
using JavaScript, HTML, and CSS.
Some extensions have a script that run in the background
of the browser called a background page.
It doesn't have any visible UI, but it can react to events
like a tab opening or a message
from another part of the extension.
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 564,
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 564 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.
The content-blocking rules
are written in a JSON format.
Those JSON rules are logically grouped
into files called rulesets,
and there's JavaScript API
that lets you individually toggle these rulesets on or off.
And because Chrome supports this API as well,
you can write one content blocker that can run
in multiple browsers across multiple platforms.
Let's go over how to write content-blocking rules
using declarative net request.
The first step is to specify a ruleset
in the extension's manifest.
Here, I've declared one ruleset.
You'll also need to add
the declarative net request permission.
Here's an example of a declarative net request rule
that would go inside the file we specified in the ruleset.
It has four pieces.
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 "third-party."
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]