-
Notifications
You must be signed in to change notification settings - Fork 312
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Allow addEventListener/removeEventListener for 'fetch' to called at anytime #1195
Comments
Once you remove the fetch event listener, how do you get it back? Are you seeing a delay on all fetches, or mainly navigations? |
I'm suggesting that addEventListener and removeEventListener for Right now, at least as implemented in Chrome, fetch requests block on the completion of the Facebook is seeing that requests for non-navigation resources that are served from the HTTP cache are significantly slower when there's an activate |
Taking a step back from the proposed solution for a moment, when is Facebook looking to enable/disable the fetch? What are the conditions? Is it always before |
I've mentioned some of this in the 3rd paragraph but let me elaborate. The key point is that there's valid use cases to disable the fetch listener significantly after the In general I think this situation is similar to Having finer control over the |
That isn't something SW caches can do. I guess you mean you just delete them manually? We have a couple of options. We could provide an API to enable/disable fetch interception, eg |
If we are going to go down this path I'd like to see us come up with a system that will work for all event types that might wake up the service worker. FetchEvent is a bit of a unique problem because it has special treatment other events don't get. It would be nice to unify things so we don't have to keep adding exceptions in the future. |
I'd say the "message" event is currently the same as "fetch". You don't have to opt into either. |
Well, I just filed issue #1200 about message event. Is the browser allowed to disable it if there is no event handler at install time? |
I meant the stored resources are now all stale since the server revision updated and thus any stored resources will never be used. If the FB SW does a load a week later then the SW knows everything in the SW cache is stale and nothing will be used. Ideally for this a SW would want to start negotiating an update with the server which can take time to run in the background. During that period the Before we discuss adding a new API I'd really like to have an explicit public discussion ruling out this proposal. I haven't seen a good argument so far for why we can't just allow add/remove at anytime and let the browser optimize based on the current state? How is |
I'd rather we sorted out the requirements before we started bikeshedding an API, but ok. Consider: function fetchHandler() {
// …
}
addEventListener('fetch', fetchHandler); How are you going to remove that event listener? I guess it's not too hard if you want to remove it from the activate event: addEventListener('activate', async event => {
event.waitUntil(async function() {
if (await shouldRemoveFetchListener()) {
removeEventListener('fetch', fetchHandler);
}
}());
}); But what if you want to remove it from the page? I guess you'd have to use // In the page:
navigator.serviceWorker.controller.postMessage('remove-fetch-event'); // In the service worker:
addEventListener('message', ({ data }) => {
if (data == 'add-fetch-event') {
addEventListener('fetch', fetchHandler);
}
else if (data == 'remove-fetch-event') {
removeEventListener('fetch', fetchHandler);
}
}); If you want to know when that action is complete, I guess you'd have to post a message back. What if you wanted to find out the current state of the event listening? I guess you'll have to store that data in IDB and hope you keep it in sync. But what if you have one of these? addEventListener('push', event => …); Or what if you do this? // In the page:
navigator.serviceWorker.controller.postMessage('do-something'); If the service worker isn't running, either of the above will cause it to run. And as it runs it'll come to this line again: addEventListener('fetch', fetchHandler); And you've added your fetch listener again. Although if the service worker was already running, it wouldn't re-execute right now, so the outcome is time dependent. Something like Putting it on Changing navigation preload into a general preload may solve this particular problem, but it also may solve some other issues, such as speeding up stale-while-revalidate type of behaviours. |
I want to stress that the above isn't a dismissal of the feature you're proposing. Like I said, I'd rather we concentrated on figuring out the requirements before we blocked discussion on a particular API. |
I didn't mean to dismiss discussion on requirements, I was just afraid the proposal could get side tracked. I'm happy to discuss requirements more. I wanted to think about this a little bit so sorry for the delayed response.
A general preload is interesting and potentially useful to us but I think it's add a lot of complexity and doesn't address this particularly problem well:
General preload is useful and something I'd like to explore but I don't think it solves this particular problem well. I think the root of the problem with my proposal, like you mention, is how to deal with the SW restarting and correctly deciding if a // At global scope
if (self.fastSyncStorage.get('does_client_want_fetch')) {
addEventListener('fetch', fetchHandler);
} Now the problem is there's no sync storage API AFAIK and even if there was then this pattern would add a sync dependent disk read which would slow down SW startup. Another option is to have the client always register the fetch handler when the SW is restarted and as soon as async storage returns a read that indicates that the fetch handler is not required then it can be unregistered. This means that any network fetch around the time a SW is restarted will be slower. That's not a good design in my opinion.
How about if we, in addition to my proposal, added an API that returns // At global scope
if (!serviceWorker.fetch.wasUnregistered) {
addEventListener('fetch', fetchHandler);
} Implementors would be explicitly allowed to not restart the SW on a This would solve the following:
|
So you're saying the overhead of the fetch is greater than the network request itself? What kind of delay are you seeing? I'm asking this because I'd like to try and recreate is issues you're seeing, so we can figure out if this is a Chrome problem, or a fundamental spec issue that needs addressing. We have https://github.com/jakearchibald/service-worker-benchmark – does this show the kind of issue you're seeing?
I don't see how this is different in your proposal.
I'm not really sure what this means. Can you provide some more details?
In terms of discussing the merits of APIs, can we do better than "it is bad"? I think in #1195 (comment) I used code examples to show disadvantages in the API you proposed rather than just declaring it "bad". Could you extend the same courtesy to me? In fact, I held off on pointing out flaws in your API at all until you demanded it – instead I wanted to bypass that and discuss the problem. In terms of whatwg/dom#371 should be solved by limiting the error throwing to events on the service worker global. This rule is in place to alert the developer to the footgun of adding listeners async, which will result them missing events. Your proposal doesn't change this, so it's a bit of a red herring. |
Hey all, I'm Vlad, I work on Facebook's WebSpeed team with Benoit. The fundamental motivation for this request for new APIs is that a do-nothing Fetch handler is pretty slow today. It's slow because:
So when a ServiceWorker's Fetch handler is no longer needed, we end up with non-negligible overheads on every Fetch request without getting any benefit in return. Additionally, I am assuming that just optimizing the browser's implementation can't significantly reduce these overheads (e.g. make them consistently < 10ms) because spawning processes and threads and JS execution contexts just seems like a non-trivial amount of work for a CPU so it would be hard to make it consistently fast (especially with lots of CPU contention during Facebook.com load). Benoit wrote a pretty exhaustive list of situations when a ServiceWorker's Fetch handler would no longer be needed, but I think the main use-case (for us) is when main-thread Fetch requests can no longer be responded to from the ServiceWorker's cache because the cache contents are out of date, e.g. JS code files were added to the SW cache on Oct 1st, but it's currently Oct 4th and we don't want the ServiceWorker serving cached JS code files from anytime before October 2nd because of severe bugs in the old cached client code or because of incompatibilities between the old cached client code and newer server code. So the requirement really is to make "pass-through" Fetches very cheap somehow. |
Just to clarify some answers to your questions:
We're seeing slow fetches of subresources (dozens of .js and .css files) through the do-nothing Fetcher during the initial main document load. I can look into whether do-nothing Fetches for resources loaded after initial page load are slow as well if you like.
We would disable fetches after client-side code notices the SW cache has become too old or when getting a signal from the server-side to invalidate the cache. |
Would #1026 be a solution for the problems here? For example, if you could do the following:
I guess I'm wondering if we expose the bypass primitive, perhaps then the page can manage the state about whether requests should get a FetchEvent or not. |
No it's orthogonal. But the problem in #1026 is a lot more urgent and impactful to fix. This issue mentioned here can be worked around by forcing a new service worker to install with skipWaiting which allows us to re-validate the cache or avoid registering the fetch handler to use the optimization from #718. I had assumed this issue here would be a quick fix but devoting resources to #1026 is much more important. The reason they are orthogonal is imagine that the SW is being restarted and a preload request goes out. The server is generating markup but doesn't know the state that the SW is in so it cannot annotate tags with serviceworker="none" since the SW has not been started yet and hasn't communicated with the server yet by the time the page is being generated (assuming a slow SW startup and a high latency network with is typical). The server doesn't know the SW needs to be bypassed or not when generating the markup since the SW isn't running and hasn't established the state of the its cache yet. I don't think I did the best job at explaining the motivation for this issue. But this issue is sufficiently complex that I'm not sure what is miscommunicated here. A live discussion might be more effective so I'm happy to get on a call if anyone would like to discuss and get clarification on the issue. |
In some ways implementing #1026 is much easier to implement. At least in gecko we have a flag we can add to network requests to bypass service worker logic completely. I imagine other browsers must have something similar given current features. Implementing #1026 would simply be exposing an API to add that existing flag.
Honestly this doesn't seem like a bad way to solve the problem to me. I like that it uses our stateful versioning based on SW script identity.
TPAC is next week. |
F2F: This is lower priority than #1026, but we could do this with some kind of async toggle on the registration. |
AI: @jakearchibald to spec this and find out if this breaks the web. |
So I'm not particularly happy with
as that makes those methods even more magical. Or is a different approach the plan now? |
Yes I think #1195 (comment) was meant for another issue as we didn't discuss this particular proposal at the F2F (though we did talk about #1026 and static routes, etc). @jakearchibald I think you meant your comment for #913 ? I think we can go ahead and close this issue as #1026 or static routes are more likely solutions, and given bgirard's comment. |
As both Google and Facebook has found, there’s a high measurable overhead to routing fetches through the service worker.
#718 implemented a great partial solution and I ran an A/B control experiment at Facebook and confirmed it showed a large startup benefit for our push-only service worker. This change requires the Service Worker to decide on first evaluation to register a fetch handler or not. This is saved on the SW registration and this value is retained. After this it’s impossible to change your mind so the work around is to force installing a new service worker to get this new first run.
However, this leaves out a lot of practical use case where a service worker may change its mind. For instance, a service worker may choose to disable ‘fetch’ caching if a user is logged out if the caches contain sensitive information. Or the caches may simply expire and be incompatible with the current server version. I don’t think it’s reasonable to lock-in a service worker to listen to fetch event for its full lifetime and pay the performance overhead.
Right now, the work around is to force install a new service worker, bypassing HTTP caches, use skipWaiting to force the new service worker to activate. This is very complicated to get right compared to simply maintaining addEventListener and removeEventListener for your fetch event. This current approach passes on a lot of complexity to the service worker developer and establishes a bad precedent to addEventListener. See whatwg/dom#371 for discussion.
As I see it, the service worker shutting down is, for the most part, done as an optimization. If the service worker is shutdown while there’s no ‘fetch’ event then there’s no point in waking up the service worker since it wouldn’t be able to observe the fetch had it not been shutdown. If there’s a current ‘fetch’ listener or there was one at the time of the shutdown then the fetch should be handled by the service worker. I see no point in disallowing that to change after first run.
The service worker should continue to block on the ‘activate’ event completion before deciding if the navigate event should be handled or not. That gives an appropriate chance for a service worker to activate and decide if it wishes to handle all requests without missing the first navigate. It also lets the service worker a chance to change its mind and removeEventListener(‘fetch’, handler) because it decides that caching is no longer viable and is unlikely to overcome the performance overhead of having the fetch handler.
From Facebook’s experience the ‘fetch’ handler adds significant overhead. When caches and the server are in the right state then the ‘fetch’ handler is able to overcome the overhead of listening to all web request and provide an overall win. However, in some cases we may deactivate the cache in which case it would be preferable to remove the ‘fetch’ listener and only listen to push events. Updating addEventListener and removeEventListener would allow this naturally and it would resolve whatwg/dom#371 by not requiring any change there.
The text was updated successfully, but these errors were encountered: