-
Notifications
You must be signed in to change notification settings - Fork 50
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
Update document-render-blocking.md to include more options #214
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,12 @@ | |
The Web is designed with a model for incremental rendering. When a Document is loading, the browser can render its intermediate states before fetching all the requisite sub-resources, executing all script or fetching/parsing the complete Document. While this is great to reduce the time for first paint, there is a tradeoff between showing a jarring flash of intermediate Document state (which could be unstyled or have more CLS) vs blocking rendering on high priority sub-resources within a reasonable timeout. | ||
|
||
The [render-blocking](https://html.spec.whatwg.org/#render-blocking-mechanism) concept helps browsers in making this tradeoff. It lets authors specify the set of stylesheets and script elements which should block rendering. For example, a stylesheet with the rules necessary to ensure a stable layout. But authors can’t specify which nodes should be added to the DOM before first render. This proposal aims to fill this gap. | ||
|
||
### Disclaimer | ||
|
||
Incremental rendering is a fundamental aspect of the Web. It ensures users wait the minimal time necessary before seeing any content from the new Document. At the same time, the tradeoff between a "good" (stable layout with above the fold content) and "fast" (how soon the first frame shows up) is difficult to get right. For this reason, browsers err towards keeping this tradeoff internal to prevent developers from unintentionally regressing the user experience. | ||
|
||
While we acknowledge that the View Transition use-case necessitates author input into this tradeoff, we want to strive for an API shape which keeps the feature from becoming a footgun for authors. | ||
|
||
# View Transitions Use Case | ||
|
||
|
@@ -20,14 +26,47 @@ The main use-case for this proposal is [cross-document View Transitions](https:/ | |
|
||
Identifying all animations correctly requires the browser to render-block the new Document until all elements which will be assigned a view-transition-name have been added to the DOM. Otherwise morph animations will turn into exit animations and entry animations will get skipped. Since browsers use heuristics to optimally yield and render when fetching a new Document, a consistent transition UX across all browsers is not feasible without an explicit hint from developers to delay rendering until the requisite nodes have been parsed. | ||
|
||
# Proposal | ||
# Other Use Cases | ||
|
||
There are a few other scenarios where a feature to control when the parser yields for rendering can be helpful: | ||
|
||
- Lower CLS: A stable layout of the DOM depends on both parsing the requisite DOM nodes and fetching relevant stylesheets. Without control over parsing, its possible that the browser does multiple renders with layout shifts as more of the DOM is parsed. | ||
Authors will sometimes initially set `display: none` or `opacity: 0` to hide the whole Document to prevent this, only showing it when enough of the Document is parsed. | ||
|
||
- Atomic rendering of semantic elements: A UI widget built using a DOM sub-tree might not make sense to render partially. Consider a menu where only half the buttons show up on first render. Authors could mark sections in the Document which semantically render one widget so the browser doesn't yield midway through parsing one widget. | ||
|
||
- Optimal Yielding: The browser may yield later than was necessary leading to rendering more what's required for above the fold content. A developer hint about a yielding trigger could imply the first frame has less DOM to parse, style, and layout. Browsers can optimize paint to be limited to onscreen content but the prior stages are executed over the entire DOM. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I'm parsing the second and third sentences well. Do you mean to say that a developer can provide a better hint resulting in less of the DOM tree to parse, style, and layout? And also, "prior stages" you mean style & layout? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, less to parse, style and layout for first frame resulting in better FCP.
Yes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Maybe rephrase somehow <_< |
||
However, its difficult for authors to know when the above-the-fold content ends given the variety of device form factors. This situation could also be solved better by `content-visibility: auto` which can optimize out style/layout for offscreen content. | ||
|
||
These use-cases are not the primary problem targeted by this proposal, they are listed to evaluate whether the ability to block parsing should be limited to when the new Document will be displayed with a View Transition or all loads. | ||
|
||
# Dependencies | ||
|
||
The set of elements which need to block rendering for View Transition depends on which Document the user is coming from. A real world example is as follows. | ||
|
||
The user is navigating between Documents of a site which has a header. This header can be scrolled offscreen by the user, so it's not guaranteed to be onscreen when a navigation is initiated. The following cases are possible: | ||
|
||
- The header was onscreen on the old Document and will be onscreen on the new Document. The author wants a morph animation which needs the header to be parsed before first render. | ||
|
||
- The header was not onscreen on the old Document and/or won't be onscreen on the new Document (for instance because of scroll restoration). The author wants just a full page animation, header does not need to be parsed before first render. | ||
|
||
The above requires the new Document to know about the old Document's visual state when the transition started. This can't be done trivially today. The Navigation API provides the list of [entries](https://developer.mozilla.org/en-US/docs/Web/API/Navigation/entries) and the [current entry](https://developer.mozilla.org/en-US/docs/Web/API/Navigation/currentEntry) but there is no notion of a "previous entry" before the current entry was committed. | ||
|
||
[html/256](https://github.com/WICG/navigation-api/issues/256) addresses this. The examples in this proposal rely on the API proposed. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How would that solve the scroll state? Is that somehow accessible on the entry itself? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No, this will have to be manual. Authors can use IntersectionObserver to track whether an element is visible and cache that information on the Document's This proposal is need to look up the previous Document's NavigationHistoryEntry. You can then query the information above which was cached by the old Document using |
||
|
||
# Proposals | ||
|
||
## Blocking Attribute | ||
|
||
Add the [blocking attribute](https://html.spec.whatwg.org/#blocking-attributes) on the [HTMLHtmlElement](https://html.spec.whatwg.org/multipage/semantics.html#the-html-element). This is the [document element](https://dom.spec.whatwg.org/#document-element) for an HTML Document. | ||
|
||
The timing for when this attribute can block rendering for a Document is already well defined in [render-blocking mechanism](https://html.spec.whatwg.org/multipage/dom.html#render-blocking-mechanism). The user agent [unblocks rendering](https://html.spec.whatwg.org/multipage/dom.html#unblock-rendering) on this element when it's done parsing the document as defined [here](https://html.spec.whatwg.org/multipage/parsing.html#the-end). | ||
|
||
## Sample Code | ||
### Block Rendering on Full Document Parsing | ||
This approach neatly fits with the existing `blocking` primitive in html. The con is that while its trivial to block rendering until the full Document is parsed, more not-so-obvious code is needed to optimally block only on the minimal requisite set of elements. That makes it likely that authors will just block on full parsing since that will be an easier fix to the correctness issue. This will degrade the overall user experience by delaying the first frame longer than necessary. | ||
|
||
### Sample Code | ||
|
||
#### Block Rendering on Full Document Parsing | ||
```html | ||
<html blocking="render"> | ||
<body> | ||
|
@@ -36,16 +75,111 @@ The timing for when this attribute can block rendering for a Document is already | |
</html> | ||
``` | ||
|
||
### Block Rendering on Partial Document Parsing | ||
#### Block Rendering on Partial Document Parsing | ||
|
||
```html | ||
<html blocking="render"> | ||
<script> | ||
// The set of element IDs that should block rendering. | ||
let blockingElementIds = new Set(); | ||
|
||
function maybeUnblockRendering() { | ||
if (blockingElementIds.size == 0) { | ||
document.documentElement.blocking=""; | ||
} | ||
} | ||
|
||
// The value returned by getState() is set by the old Document in | ||
// the `navigate` event. It tracks whether the old Document add a | ||
// `view-transition-name` to the header. | ||
if (navigation.initialLoad.from().getState().morphHeader) { | ||
blockingElementIds.add("header-id"); | ||
} | ||
maybeUnblockRendering(); | ||
</script> | ||
<body> | ||
… | ||
<div id="header-id"> | ||
... | ||
</div> | ||
<script> | ||
if (doPartialBlocking) { | ||
document.documentElement.blocking=""; | ||
} | ||
// When an element is parsed, remove it from the blocking set and | ||
// unblock rendering if all blocking elements have been parsed. | ||
blockingElementIds.delete("header-id"); | ||
maybeUnblockRendering(); | ||
</script> | ||
</body> | ||
</html> | ||
``` | ||
|
||
## Meta Tag with Blocking ElementIds | ||
|
||
Add a new meta tag with the name `blocking-elements` and `content` attribute set to the list of [element IDs](https://dom.spec.whatwg.org/#concept-id) which must be parsed before rendering. `*` is a special keyword which implies every element should be blocking. | ||
|
||
Each Document has a render-blocking-until-parsed element ids set (initially empty) and a boolean blocked-until-full-parsing (initially false). A Document is [render-blocked](https://html.spec.whatwg.org/#render-blocked) if render-blocking-until-parsed element ids set is non-empty or blocked-until-full-parsing is true. | ||
|
||
- If the value of the `content` attribute changes, and the Document [allows adding render blocking elements](https://html.spec.whatwg.org/#allows-adding-render-blocking-elements), then: | ||
- If the new attribute value is a comma separated list, the render-blocking-until-parsed element ids is set to the new attribute value and blocked-until-full-parsing is set to false. | ||
- If the new attribute value is `*`, the render-blocking-until-parsed element ids is cleared and blocked-until-full-parsing is set to true. | ||
|
||
This means authors can run script to configure the list until the opening `<body>` tag is parsed (after which no new render blocking resources can be added). | ||
|
||
- If the value of the `content` attribute changes, and the Document **does not** [allow adding render blocking elements](https://html.spec.whatwg.org/#allows-adding-render-blocking-elements), then: | ||
|
||
- If the new attribute value is a comma separated list, the render-blocking-until-parsed element ids is set to be an intersection of the existing value and the new attribute value and blocked-until-full-parsing is set to true. | ||
- If the new attribute value is `*`, no change is made. | ||
|
||
This means authors can run script to remove render-blocking elements after the body tag is parsed but can't add more elements. This allows authors to implement their own timeout if needed. | ||
|
||
- Each time an element's ID value changes, the browser checks if the set of elements which have been completely parsed (i.e. the end tag has been parsed) include all IDs in the render-blocking-until-parsed element ids set. If yes, the render-blocking-until-parsed element ids set is cleared. | ||
|
||
The pro of this approach is that its easier to block on a specific set of elements, which makes it more likely that authors will consider partial blocking. The con is new syntax which requires defining subtle interactions (like script changing element IDs after parsing). Also, if the developer makes errors like a typo in the ID name or removing the element from the Document without updating the list, rendering will be blocked until full parsing is done. These errors can be surfaced on the console. | ||
|
||
### Including Blocking Tokens | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd also note that a typo in this list would cause the parser to block on the whole document (waiting for the non-existent id), but in that case we can throw an error to the console There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
A sub-proposal is for the `content` attribute to include both the list of element IDs and [blocking tokens](https://html.spec.whatwg.org/#possible-blocking-token). This would enable authors to specify which operation needs to be blocked on a set of elements, similar to controlling which operations are blocked on a particular resources. For example, | ||
|
||
```html | ||
<meta name="blocking-elements" content="id1,id2;render"> | ||
``` | ||
|
||
### Sample Code | ||
|
||
#### Block Rendering on Full Document Parsing | ||
|
||
```html | ||
<html> | ||
<meta name="blocking-elements" content="*"> | ||
<body> | ||
… | ||
</body> | ||
</html> | ||
``` | ||
|
||
#### Block Rendering on Partial Document Parsing | ||
|
||
```html | ||
<html> | ||
<meta id="foo" name="blocking-elements" content=""> | ||
<script> | ||
// The value returned by getState() is set by the old Document in | ||
// the `navigate` event. It tracks whether the old Document add a | ||
// `view-transition-name` to the header. | ||
if (navigation.initialLoad.from().getState().morphHeader) { | ||
foo.content="header-id"; | ||
} | ||
</script> | ||
<body> | ||
… | ||
<div id="header-id"> | ||
... | ||
</div> | ||
<!--Rendering is unblocked after this point--> | ||
</body> | ||
</html> | ||
``` | ||
|
||
# Transition Only Blocking | ||
|
||
A completmentory proposal (which works with both options above) is to add `transition` to the list of [possible blocking tokens](https://html.spec.whatwg.org/#possible-blocking-token). This token makes the resource, or parsing, render-blocking only if there is a ViewTransition to this Document. This allows authors to limit render-blocking to when its strictly needed. | ||
|
||
We could also make `transition` to be the only value allowed for `blocking` in the options above. This means authors won't be able to block rendering on parsing unless there is a ViewTransition. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like a parser may have some context here already, like try not yielding between siblings, or something like, but yeah your point that developers may want to control this makes sense