Skip to content

Latest commit

 

History

History
288 lines (180 loc) · 28.4 KB

browsing-context.md

File metadata and controls

288 lines (180 loc) · 28.4 KB

Prerendering browsing contexts

We envision modernized prerendering to work by loading content into a prerendering browsing context, which is a new type of top-level browsing context. A prerendering browsing context can be thought of as a tab that is not yet shown to the user, and which the user has not yet affirmatively indicated an intention to visit. As such, it has additional restrictions placed on it to ensure the user's privacy and prevent disruptions.

Prerendering browsing contexts can be activated, which causes them to transition to being full top-level browsing contexts (i.e. tabs). From a user experience perspective, activation acts like an instantaneous navigation, since unlike normal navigation it does not require a network round-trip, creation of a Document, or running of the web-developer-provided initialization JavaScript. All of that has already been done in the prerendering browsing context. (Or at least, the majority of it; the site might delay some of its initialization until activation, or some of the initialization might not have finished, especially if the browser deprioritizes unactivated browsing contexts.)

Activation might replace an existing top-level browsing context, for example if the user clicks a normal link whose target has been prerendered. Or it might cause the prerendered context to be shown in a new tab/window, for example if the user clicks on a target="_blank" link. Activation lifts the restrictions on the prerendered content, as by that point a user-visible navigation has occurred.

In general, activation of a prerendering browsing context is done by the user agent, when it notices a navigation that could use the prerendered contents. However, some forms of prerendering, such as portals, can provide explicit entry points for activation.

Documents rendered within a prerendering browsing context have the ability to react to activation, which they can use to upgrade themselves once free of the restrictions. For example, they could start using permission-requiring APIs, or get access to unpartitioned storage.

Note: a browsing context is the right primitive here, as opposed to a Window or Document, as we need these restrictions to apply even across navigations. For example, if you prerender https://a.example/ which contains <meta http-equiv="refresh" content="0; URL=https://a.example/home"> then we need to continue applying these restrictions while loading the /home page.

Table of contents

Example

Consider https://a.example/, which contains the following HTML:

<link rel="prerender2" href="https://b.example/">

<a href="https://b.example/">Click me!</a>

The "prerender2" rel here is illustrative only. See the triggers document for more serious discussion of potential APIs for triggering prerendering.

Upon loading https://a.example/, the browser notices the request to prerender https://b.example/. It does so by creating a prerendering browsing context, which it navigates to https://b.example/. This navigation takes place using special fetch modes, which ensure that https://b.example/ has opted in to being prerendered, and ensures that the request for https://b.example/ and any of its subresources is performed without any credentials that might identify the user.

Within this prerendering browsing context, assuming the opt-in check passes, loading of https://b.example/ proceeds mostly as normal. This includes any expensive web-developer-provided JavaScript necessary to initialize the web app found there. It could even include server- or client-side redirects to other pages, perhaps even other domains.

However, if https://b.example/ is one of those sites that requests notification permissions on first load, such a permission prompt will be denied, as if the user had declined. Similarly, if https://b.example/ performs an alert() call, the call will instantly return, without the user seeing anything. Another key difference is that https://b.example/ will not have any storage access, including to cookies. Thus, the content it initially renders will be a logged-out view of the web app, or perhaps a specially-tailored "prerendering" view which leaves things like logged-in state indeterminate.

(The above describes a conservative plan for the behavior restrictions of prerendered content. See also #7 and #8 for discussion of alternate strategies.)

Now, the user clicks on the "Click me!" link. At this point the user agent notices that it has a prerendering browsing context originally created for https://b.example/, so it activates it, replacing the one displaying https://a.example/. The user observes their browser navigating to https://b.example/, e.g., via changes in the URL bar contents and the back/forward UI. And since https://b.example/ was already loaded in the prerendering browsing context, this navigation occurs seamlessly and instantly, providing a great user experience.

Upon activation, https://b.example/ gets notified via the API. At this point, it now has access to storage and cookies, so it can upgrade itself to a logged-in view if appropriate:

document.storageAccessAvailable.then(() => {
  document.getElementById('user').textContent = localStorage.getItem('current-user');
});

This completes the journey to a fully-rendered view of https://b.example/, in a user-visible top-level browsing context.

Restrictions

For an API-by-API analysis of the restrictions in prerendering browsing contexts, including an overview table and alternatives considered, see this document. The following section outlines the reasoning and threat model behind the proposed restrictions.

Privacy-based restrictions

Prerendering is intended to comply with the W3C Target Privacy Threat Model. This section discusses the aspects of that threat model that are particularly relevant to the browsing context part of the story, and how the design satisfies them.

A prerendering browsing context can contain either a same-site or cross-site resource. Same-site prerendered content don't present any privacy risks, but cross-site resources risk enabling cross-site recognition by creating a messaging channel across otherwise-partitioned domains. For simplicity, when a cross-site channel needs to be blocked, we also block it for same-site cross-origin content. In some cases we even block it for same-origin content.

Because prerendered browsing contexts can be activated, they (eventually) live in the first-party storage shelf of their origin. This means that the usual plan of storage partitioning does not suffice for prerendering browsing contexts as it does for nested browsing contexts (i.e. iframes). Instead, we take the following measures to restrict cross-origin prerendered content:

  • Prevent communication with the referring document, to the same extent we prevent it with a cross-site link opened in a new tab.
  • Block all storage access while content is prerendered.

If we allowed communication, then the prerendered content could be given the user ID from the host site. Then, after activation gives the prerendered page access to first-party storage, it would join that user ID with information from its own first-party storage to perform cross-site tracking.

If we allowed access to (unpartitioned) storage, then side channels available pre-activation (e.g., server-side timing correlation) could potentially be used to join two separate user identifiers, one from the referring site and one from the prerendered site's unpartitioned storage.

The below subsections explore the implementation of these restrictions in more detail.

Storage access blocking

Prerendered pages that are cross-origin to their referring site will have no access to storage.

We could attempt to address the threat by providing partitioned or ephemeral storage access, but then it is unclear how to transition to unpartitioned storage upon activation. It would likely require some kind of web-developer-written merging logic. Completely blocking storage access is thus deemed simpler; prerendered pages should not be doing anything which requires persistent storage before activation.

This means that most existing content will appear "broken" when prerendered by a cross-origin referrer. This necessitates an explicit opt-in to allow cross-origin content to be prerendered, discussed elsewhere. Such content might optionally "upgrade" itself to a credentialed view upon activation, as shown in the example above.

For a more concrete example, consider https://aggregator.example/ which wants to prerender this GitHub repository. To make this work, GitHub would need to add the opt-in to allow the page to be prerendered. Additionally, GitHub should add code to adapt their UI to show the logged-in view upon activation, by removing the "Join GitHub today" banner, and retrieving the user's credentials from storage and using them to replace the signed-out header with the signed-in header. Without such adapter code, activating the prerendering browsing context would show the user a logged-out view of GitHub in the top-level tab that the prerendering browsing context has been activated into. This would be a bad and confusing user experience, since the user is logged in to GitHub in all of their other top-level tabs.

As for the exact mechanism of this blocking:

  • Asynchronous storage access APIs, such as IndexedDB, the Cache API, and File System Access's origin-private file system, will perform no work and have their corresponding promises/events delayed until activation.

  • For synchronous storage APIs like localStorage and docuemnt.cookie, we are currently still discussing the best option, in #7. The simplest idea would be to have them throw exceptions, but there may be more friendly alternatives that would allow prerendering to work on more pages without code changes.

Communications channels that are blocked

  • Prerendering browsing contexts have no reference to the Window, or other objects, of their referrer. Thus, they cannot communicate using postMessage() or other APIs.
  • BroadcastChannel behavior is modified in prerendering browsing contexts. Any messages sent to such a channel are queued up and only delivered after activation. Any messages received on the channel pre-activation are dropped.
  • SharedWorker construction is delayed in prerendering browsing contexts. In particular, while the new SharedWorker() constructor returns immediately, no worker is started or connected to until after activation. Any messages sent to the SharedWorker pre-activation are buffered up and delivered upon activation. (And, since the SharedWorker is not connected to an actual shared worker pre-activation, no message can be received on it pre-activation.)
  • Web locks APIs will return promises which wait to settle (and wait to do any lock-related work) until activation.
  • TODO ServiceWorker?
  • Fetches within cross-origin prerendering browsing contexts, including the initial request for the page, do not use credentials. Credentialed fetches could be used for cross-site recognition, for example by:
    • Using the sequence of loads. The referring page could encode a user ID into the order in which a sequence of URLs are prerendered. To prevent the target from correlating this ID with its own user ID without a navigation, a document loaded into a cross-origin prerendering browsing context is fetched without credentials and doesn't have access to storage, as described above.
    • The host creates a prerendering browsing context, and the prerendered site decides between a 204 and a real response based on the user's ID. Or the prerendered site delays the response by an amount of time that depends on the user's ID. Because the prerendering load is done without credentials, the prerendered site can't get its user ID in order to make this sort of decision.
  • Sizing side channels: prerendering browsing contexts always perform layout based on the initial size of their referring browsing context, as its most likely that upon activation, they'll end up with that same size. However, further resizes to the referring browsing context are not used to update the size of the prerendering browsing context, as this could be used to communicate a user ID. For simplicity, we apply this sizing model to same-origin prerendered content as well.

Communications channels that match navigation

As mentioned above, we prevent communications to the same extent we prevent it with a cross-site link opened in a new tab. In particular:

  • The prerendered content's own URL and the referring URL are available to prerendered content to the same extent they're available to normal navigations. Solutions to link decoration will apply to both.

Note that since a non-activated prerendering browsing context has no storage access, it cannot join any information stored in the URL with any of the prerendered site's data. So it's only activation, which gives full first-party storage access, which creates a navigation-equivalent communications channel. This equivalence makes sense, as activating a prerendering browsing context is much like clicking a link.

Restrictions on the basis of being non-user-visible

Apart from the privacy-related restrictions to communications and storage, while prerendered, pages are additionally restricted in various ways due to the fact that the user has not yet expressed any intent to interact. All of these restrictions apply regardless of the same- or cross-origin status of the prerendered content.

  • APIs with a clear async boundary will have their work delayed until activation. Thus, their corresponding promises would simply remain pending, or their associated events would not fire. This includes features that are controlled by the Permissions API (list), some features that are controlled by Permissions Policy, pointer lock, and orientation lock (the latter two of which are controlled by <iframe sandbox="">).

  • Any feature which requires user activation will not be available, since user activation is not possible in prerendering browsing contexts. This includes APIs like PresentationRequest and PaymentRequest, as well as the beforeunload prompt and window.open().

  • The gamepad API will return "no gamepads" pre-activation, and fire gamepadconnected as part of activation (after which it will return the usual set of gamepads).

  • Autoplaying content will not be treated as eligible for autoplay pre-activation, but activation will revisit such content and potentially start it autoplaying if it is eligible.

  • Downloads will be delayed until after activation.

  • window.alert() and window.print() will silently do nothing pre-activation.

  • window.confirm() and window.prompt() will silently return their default values (false and null) pre-activation.

Note: specifying this will likely be messy, as these different APIs are controlled through different mechanisms, and many specs will need to be patched to include a "pause until activation" step.

Restrictions on loaded content

To simplify implementation, specification, and the web-developer facing consequences, prerendering browsing contexts cannot host non-HTTP(S) top-level Documents. In particular, they cannot host:

  • javascript: URLs
  • data: URLs
  • blob: URLs
  • about: URLs, including about:blank and about:srcdoc

In some cases, supporting these would create a novel situation for a top-level browsing context: for example, right now, a top-level browsing context cannot navigate to a data: or blob: URL, so allowing those to be prerendered and then activated (which is equivalent to a navigation) would require new implementation and specification infrastructure.

In other cases, like javascript: URLs or about:blank, the problem is that those URLs generally inherit properties from their creator, and we don't want to allow this cross-Document influence for prerendered content. Overall, restricting to HTTP(S) URLs ensures that prerendered content always has a well-defined origin, that is not contingent on the referring page.

The removal of the script-visible about:blank in prerendering browsing contexts also greatly simplifies them; its existence in other browsing contexts causes Windows and Documents to lose their normally one-to-one relationship.

If a prerendering browsing context navigates itself to a non-HTTP(S) URL, e.g. via window.location = "data:text/plain,foo", then the prerendering browsing context will be immediately discarded, and no longer be used by the user agent for anything.

Note that iframes (nested browsing contexts) inside of a prerendered browsing context have no such restrictions.

JavaScript APIs

Purpose-specific APIs

To react to changes in prerendering state, script can use APIs particular to the behavior they are interested in. For example, the storage access API API can be used in supporting browsers to observe whether unpartitioned storage is available. Especially with a proposed extension, this can be quite ergonomic:

document.storageAccessAvailable.then(() => {
  // grab user data from cookies/IndexedDB
  // update the UI
});

Another similar case is asking for permissions. Since permission-granting is automatically delayed until activation, the normal permission-requesting code could be used. For example, to prompt for notifications, you'd just write:

Notification.requestPermission().then(state => {
  // This will be called only after the user grants or denies the permission.
  // - If the page is rendered normally, that will probably be soon.
  // - If the page is rendered in a prerendering browsing context, then the prompt will be delayed until activation.
});

Finally, for cases related to rendering and visibility, we propose a dedicated prerendering state API:

function afterPrerendering() {
  // start a video/animation
  // fetch large resources
  // connect to a chat server
  // etc.
}

if (document.prerendering) {
  document.addEventListener('prerenderingchange', () => {
    afterPrerendering();
  }, { once: true });
} else {
  afterPrerendering();
}

Please read that sibling explainer for more details on the design choices and motivations there.

Potential primitive API

We are also considering exposing the core primitive which the browser uses, i.e. a browsing context's loading mode. This could look something like a document.loadingMode object, with:

  • A property, type, which is either "default", "prerender", or "uncredentialed-prerender". It could be extended in the future to other types.
  • An event, "change", which fires when type changes.

In the future, document.loadingMode might have additional properties; for example, it might expose the notion that the page was loaded via some proxy, as mentioned in the fetch integration.

However, we currently believe the purpose-specific APIs described above suffice for all known use cases. So although we like the idea of exposing the spec-level primitives directly, we're currently putting that idea on hold. See some discussion in #2.

Page lifecycle and freezing

User agents need to strike a delicate balance with prerendered content. Such content needs enough resources to do its initial setup work, so that loading it is as instant as possible. But it shouldn't consume resources in a way that would detract from a user's experience on the content they're actively viewing on the referring site.

One mechanism user agents will probably use for this is to freeze prerendered pages, in the sense defined by the Page Lifecycle specification. The most important impact of freezing, for our purposes, is that tasks queued by the page will not be run by the event loop. In particular, we envision user agents freezing prerendered pages after some initial setup time, to avoid recurring timers or data transfers.

Using the freezing mechanism is a natural fit for prerendered content, since freezing is already performed by user agents for backgrounded content. In particular, content which uses the page lifecycle API (such as the freeze and resume events) will likely react correctly if it becomes frozen in a prerendering browsing context, just like if it were frozen in any other browsing context.

Session history

From the user's perspective, activating a prerendering browsing context behaves like a conventional navigation. The current Document displayed in the prerendering browsing context is appended to session history, with any existing forward history entries pruned. Any navigations which took place within the prerendering browsing context, before activation, do not affect session history.

From the developer's perspective, a prerendering browsing context can be thought of as having a trivial session history where only one entry, the current entry, exists. All navigations within the prerendering browsing context are effectively done with replacement. While APIs that operate on session history, such as window.history, can be called within prerendering browsing contexts, they only operate on the context's trivial session history. Consequently, prerendering browsing contexts do not take part in their referring page's joint session history; that is, they cannot navigate their referrer by calling history.back() enough times, like iframes can navigate their embedders.

This model ensures that users get the expected experience when using the back button, i.e., that they are taken back to the last thing they saw. Once a prerendering browsing context is activated, only a single session history entry gets appended to the joint session history, ignoring any previous navigations that happened within the prerendering browsing context. Then, stepping back one step in the joint session history, e.g. by pressing the back button, takes the user back to the referrer page.

Navigation

Each prerendering browsing context has an original URL, which is the URL it was originally instantiated with. For example, given

<link rel="prerender2" href="https://a.example/">

the original URL is https://a.example/. Once instantiated, the prerendering browsing context might navigate elsewhere, e.g. via server-side redirects, <meta http-equiv="refresh">, or calling .click() on an <a> element. This will perform further with-replacement navigations within the prerendering browsing context, all offscreen. But the original URL stays the same.

Later, the prerendering browsing context can be used to satisfy a navigation, based on the original URL, not the prerendering browsing context's current URL. That is, if https://a.example/ redirects to https://b.example/, then given

<a href="https://a.example/">Click me!</a>
<a href="https://b.example/">Click me!</a>

only the navigation initiated by clicking on the first of these links could be satisfied by activating the prerendering browsing context.

Another interesting situation to consider is what happens if the user right-clicks on the first link, and chooses "Open in New Tab". The first time they do this, the new tab can be created instantly, by activating the prerendering browsing context. However, if they do it a second time, the prerendering browsing context has been used up; the second navigation will perform a normal, non-instant navigation.

Rendering-related behavior

Prerendered content needs to strike a delicate balance, of doing enough rendering to be useful, but not actually displaying any pixels on the user's screen. As such, we want developers to avoid performing expensive work which is not beneficial while being prerendered. And ideally, doing this should require minimal additional coding by the developer of the page being prerendered.

Generally speaking, our plan is to treat content as if it were in a "background tab": it will still perform layout, using (for privacy and simplicity reasons) the creation-time size of the referring page as the viewport. Rendering APIs which communicate visibility information, such as Intersection Observer or the loading attribute, will indicate visibility based on the creation-time viewport.

CSP integration

A prerendered Document can apply CSP to itself as normal. Being in a prerendering browsing context vs. a normal top-level browsing context does not change any of the impacts of CSP. Note that since prerendered documents are always loaded from HTTP(S) URLs, there is no need to worry about complex CSP inheritance semantics.

Prerendered content will be affected by prefetch-src on the referring page, which provides a way of preventing prefetching in addition to the triggers.

The navigate-to directive prevents navigations, which means that if prerendered content is prevented from being navigated to via this mechanism, then the corresponding prerendering browsing context will never be activated. This mostly falls out automatically from the CSP spec preventing navigations, but any prerendering APIs that explicitly expose the activation operation (such as portals) will need to account for it in their specification.

Note that navigate-to will prohibit navigations based on the URL of the link clicked (or similar), which corresponds to a prerendering browsing context's original URL (discussed above). This means that given something like

Content-Security-Policy: navigate-to https://a.example

and markup such as

<link rel="prerender2" href="https://a.example/redirects-to-another-origin">

<script>
location.href = 'https://a.example/redirects-to-another-origin';
</script>

then the navigation will be allowed, even though the prerendering browsing context could be pointing to an origin besides https://a.example. In other words, prerendering does not change the behavior of navigate-to, despite allowing the browser to know more information about the eventual navigation destination.