#53204: perf: keep requests off the main thread for type-filtered and observer-only webRequest listeners
Description of Change
Renderer requests no longer wait for the main process when the only webRequest listeners that could match them are observers (onCompleted, onErrorOccurred, onSendHeaders, onBeforeRedirect, onResponseStarted) or are filtered to other resource types. Follow-up to #53115, whose IO-thread gate sent every request through the UI-thread ProxyingURLLoaderFactory as soon as a session had any listener, because a listener may modify the request and re-runs on each redirect leg, so its URL filter cannot be applied up front. Two things can be decided up front:
- A request's resource type never changes across redirects, so the listeners' existing
typesfilter is honored before the request reaches the UI thread, in the gate and in the proxy's pass-through path. A blocking listener registered withtypes: ['mainFrame', 'subFrame']no longer pulls images, scripts andfetch()calls through the main process. - Observer listeners cannot change anything. When only observers match a request's type, it goes straight to the network service and a pass-through
URLLoaderClienton the IO thread reports the redirect, response and completion toapi::WebRequestafterwards. Listeners receive the samedetails(including oneonSendHeadersper redirect leg); the renderer never waits for them.
InterceptState keeps two resource-type masks (blocking / observing) instead of a boolean, recomputed whenever a listener is added or removed.
Linux x64 release, page with 150 small subresources from a local server in a separate process, median of 3 warm loads; "busy" = the main process runs 100 ms of JavaScript right after the navigation commits. Before -> after:
| listeners on the session | page, idle | page, busy | fetch() round trip |
main-process CPU per request |
|---|---|---|---|---|
| none | 53 -> 50 ms | 152 -> 54 ms | 0.46 -> 0.40 ms | 146 -> 129 µs |
onCompleted + onErrorOccurred + onSendHeaders |
56 -> 46 ms | 160 -> 55 ms | 0.63 -> 0.46 ms | 342 -> 244 µs |
onBeforeRequest + onHeadersReceived, types: ['mainFrame', 'subFrame'] |
57 -> 49 ms | 161 -> 108 ms | 0.60 -> 0.40 ms | 309 -> 129 µs |
| both of the above | 57 -> 48 ms | 155 -> 108 ms | 0.61 -> 0.47 ms | 325 -> 236 µs |
onBeforeSendHeaders, no filter |
58 -> 54 ms | 159 -> 163 ms | 0.61 -> 0.65 ms | 320 -> 342 µs |
In the 108 ms rows the subresources are on the direct path but the document request itself matches the frame-typed blocking listeners, so its completion still goes through the main process; an unfiltered blocking listener is unchanged by design.
Checklist
- PR description included and stakeholders cc'd
-
npm testpasses (new cases in api-web-request-spec; webRequest, protocol, session, net, webContents, subframe, webview, service worker and extensions specs locally on Linux) - PR release notes describe the change in a way relevant to app developers, and are capitalized, punctuated, and past tense.
- I have reviewed and verified the changes
Release Notes
Notes: Renderer network requests no longer waited on the main process when the session's webRequest listeners only observe requests (onCompleted, onErrorOccurred, onSendHeaders, onBeforeRedirect, onResponseStarted) or are filtered to other resource types.
Backports
Semver Impact
Semantic Versioning helps users understand the impact of updates:
- Major (X.y.z): Breaking changes that may require code modifications
- Minor (x.Y.z): New features that maintain backward compatibility
- Patch (x.y.Z): Bug fixes that don't change the API
- None: Changes that don't affect using facing parts of Electron