├── .gitignore
├── async-lifecycle.png
├── sync-lifecycle.png
├── w3c.json
├── LICENSE.md
├── CONTRIBUTING.md
├── index.html
├── README.md
└── EXPLAINER.md
/.gitignore:
--------------------------------------------------------------------------------
1 | **/.DS_Store
2 |
--------------------------------------------------------------------------------
/async-lifecycle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WICG/async-append/HEAD/async-lifecycle.png
--------------------------------------------------------------------------------
/sync-lifecycle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WICG/async-append/HEAD/sync-lifecycle.png
--------------------------------------------------------------------------------
/w3c.json:
--------------------------------------------------------------------------------
1 | {
2 | "group": ["80485"]
3 | , "contacts": ["yoavweiss"]
4 | , "shortName": "async-append"
5 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | All Reports in this Repository are licensed by Contributors under the
2 | [W3C Software and Document
3 | License](http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document). Contributions to
4 | Specifications are made under the [W3C CLA](https://www.w3.org/community/about/agreements/cla/).
5 |
6 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Web Platform Incubator Community Group
2 |
3 | This repository is being used for work in the Web Platform Incubator Community Group, governed by the [W3C Community License
4 | Agreement (CLA)](http://www.w3.org/community/about/agreements/cla/). To contribute, you must join
5 | the CG.
6 |
7 | If you are not the sole contributor to a contribution (pull request), please identify all
8 | contributors in the pull request's body or in subsequent comments.
9 |
10 | To add a contributor (other than yourself, that's automatic), mark them one per line as follows:
11 |
12 | ```
13 | +@github_username
14 | ```
15 |
16 | If you added a contributor by mistake, you can remove them in a comment with:
17 |
18 | ```
19 | -@github_username
20 | ```
21 |
22 | If you are making a pull request on behalf of someone else but you had no part in designing the
23 | feature, you can remove yourself with the above syntax.
24 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Spec proposal
6 |
7 |
17 |
18 |
19 |
20 |
21 | This specification does neat stuff.
22 |
23 |
24 |
25 |
26 | This is an unofficial proposal.
27 |
28 |
29 |
30 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Archive of async-append
2 |
3 | **This incubation is superseded by https://github.com/WICG/display-locking. This repository is archived for historical purposes.**
4 |
5 | A way to create/mutate DOM and add it to the document without blocking the main thread. For proposed solutions, see the [EXPLAINER](EXPLAINER.md).
6 |
7 | # Problem
8 | In order to minimize the number of DOM elements and improve load times, sites often construct elements as needed and add them to the page. _Hopefully_, this happens just in time for the user to see the element with no delay. In reality, the element trees constructed are quite large/complex, leading to jank.
9 |
10 | It is also common for sites to mutate the DOM in response to user actions or data changes. When applying these changes for large sites, it is difficult to avoid jank.
11 |
12 | # Use Cases
13 | Speaking with web developers, a few usage requests have been made for an asynchronous rendering API:
14 |
15 | - Appending DOM or applying changes from [virtual DOM](https://github.com/Matt-Esch/virtual-dom) style objects constructed in a worker.
16 | - Async application of style sheets
17 | - Applying DOM mutations on the main thread normally, but while remaining responsive to input (i.e. race to finish but interrupt for input)
18 |
19 | There are also several UX patterns that lazily instantiate and append DOM:
20 |
21 | - Infinite scrollings lists (news feeds)
22 | - Side drawers
23 | - Chat views
24 | - Image slideshows
25 |
26 | All of these would benefit from an ability to mutate DOM without blocking the main thread.
27 |
28 | # Case Study: YouTube Gaming
29 | [YouTube Gaming](https://gaming.youtube.com/) constructs its comment panel for a video when the user clicks on 'comments', janking the main thread for anywhere from 55-180 ms for rendering (depending on browser). The number gets as high as __500ms__ when including custom element construction.
30 |
31 | ## [gaming.youtube.com](https://gaming.youtube.com/watch?v=i0purbwzs4U)
32 | The following numbers are ms measurements averaged over 5 times.
33 |
34 | __Clicking on 'comments'__
35 | - Chrome
36 | - Style: 65
37 | - Layout: 100
38 | - Paint: 15
39 | - Safari
40 | - Layout: 40
41 | - Paint: 15
42 | - Firefox
43 | - Layout: 40
44 | - Paint: 15
45 |
46 | __Scroll to bottom of 'comments'__
47 | - Chrome
48 | - Style: 30
49 | - Layout: 15
50 | - Paint: 10
51 | - Safari
52 | - Layout (placeholder images): 25
53 | - Paint (placeholder images): 10
54 | - Layout (content): 25
55 | - Paint (content): 5
56 | - Firefox
57 | - Layout: 15
58 | - Paint: 10
59 |
--------------------------------------------------------------------------------
/EXPLAINER.md:
--------------------------------------------------------------------------------
1 | # Make rendering schedulable
2 |
3 | Today's DOM operations are synchronous. This pauses scripts and events such as link navigation or touch scrolls.
4 |
5 | Scripts that mutate *large* subtrees can create noticeable jank. It's not hard to achieve a 150ms+ delay while trying to mutate the DOM in reasonable ways. If web developers attempt to do pieces of this mutation work across multiple timeouts, they will show partially-constructed DOM, which can be even worse for the user experience than jank.
6 |
7 | 
8 |
9 | This spec proposes a way to explicitly declare DOM mutations to be "async", returning immediately with a promise that is fulfilled when the work is done or rejected if the work is invalidated. This API would be a parallel option to the current, synchronous way to mutate DOM.
10 |
11 | A developer could create a subtree to append or build up a set of mutations to be applied to the existing DOM. Once all the DOM mutations were calculated, the developer would pass them to the async API. In the background, a user agent would, at minimum, chunk the resulting work into broad phases yielding back to the event loop between each phase. Based on early investigations, chunking rendering work into broad phases could reduce jank by 30-50% depending on browser.
12 |
13 | 
14 |
15 | Later implementations of the API could further chunk each phase until they fit within a frame's spare time. This would essentially make DOM work jank-free. This level of chunking is expected to be difficult, and shouldn't be required for a V0 of the API.
16 |
17 | # Constraints
18 | We have heard from several developers that DOM mutations can only be async up to a point. After a certain number of frames, it is better to jank while focusing on render work rather than delay the UI update any further. This API should enable developers to specify how long the async operation is allowed to take before it becomes blocking.
19 |
20 | This API should closely match the way that sites are manipulating DOM today. Large sites and frameworks are much more likely to use the feature if adoption means "feature detect and swap sync mutations for async append if available". If the API requires an entire rewrite of DOM mutation logic, adoption will be low.
21 |
22 | Calculating DOM mutations in a worker is a natural thing to pair with async rendering. This API should be relatively unopinionated about input format, so that things like [virtual dom](https://github.com/Matt-Esch/virtual-dom), [`WorkerNode`](github.com/drufball/worker-node), and [`DOMChangeList`](https://github.com/whatwg/dom/issues/270) are all supported.
23 |
24 | # Current (Rough) Planned API
25 |
26 | ```javascript
27 | partial interface Element {
28 | void asyncAppend(DOMBatch, Element);
29 | void asyncAppend(Element);
30 | /* plus pairs for all the other insertion/removal methods,
31 | on Element and ChildNode */
32 | };
33 |
34 | [Constructor] // could add options in the future
35 | interface DOMBatch {
36 | readonly attribute boolean started;
37 | readonly attribute Promise ready;
38 |
39 | void finish();
40 | void cancel(); // and/or a cancel token to the constructor
41 | }
42 | ```
43 |
44 | All the mutation operations come in two forms:
45 |
46 | * a "fire and forget" version with identical signature to the sync method,
47 | that just triggers an async append that finishes "sometime" in the future.
48 | This is ideal for simpler applications that aren't doing complicated time/state management,
49 | but still want the benefit of avoiding jank in the rest of the application
50 | when they're mutating the DOM.
51 | * an explicit tracking/batching version that takes a DOMBatch object,
52 | followed by the normal signature of the method.
53 | This lets you track the process and know when it completes
54 | (by watching the `ready` promise),
55 | cancel the mutation if it turns out you don't need it,
56 | and allows you to explicitly batch several mutations together
57 | so they'll all show up in the DOM at the same time
58 | (by passing the same DOMBatch to multiple calls).
59 |
60 | The DOMBatch object passes through several distinct phases:
61 | * batching
62 | * started
63 | * ready
64 | * finished
65 |
66 | When initially constructed, it's "batching",
67 | and can be passed to mutation methods.
68 | While in this state, the UA does nothing but track the mutations that will be performed.
69 |
70 | At end of microtask, it switches to "started",
71 | and sets its "started" boolean to `true`.
72 | At this point the UA begins the async work:
73 | styling, layout, paint, etc.
74 | The DOMBatch can no longer be passed to mutation methods;
75 | doing so will cause the method to throw an XXXError.
76 |
77 | When the async work is finished,
78 | and the mutation operation is ready to be mapped into the DOM,
79 | it switches to "ready",
80 | and fulfills its `ready` promise.
81 | Calling `finish()` at this point *should* be a very fast sync call.
82 | (But see below for calling `finish()` early or late.)
83 |
84 | After `finish()` is called,
85 | it switches to "finished".
86 | At this state the operation is fully complete,
87 | and the DOMBatch will not change in the future.
88 | In this stage the `cancel()` method has no effect
89 | (and maybe should throw?).
90 |
91 | ## Calling `cancel()`
92 |
93 | As long as the DOMBatch is not in the "finished" state,
94 | the author can call `cancel()` to stop the mutation.
95 | This immediately shifts the DOMBatch into its "finished" state,
96 | and throws away any pending mutation work the UA might have been doing.
97 |
98 | ## Calling `finish()` early or late
99 |
100 | Under normal circumstances,
101 | the author is expected to call `finish()` in the fulfillment callback
102 | of the DOMBatch's `ready` promise.
103 | At that time the async work has just been completed,
104 | and `finish()` should be a very fast bit of sync work.
105 |
106 | Calling `finish()` before the DOMBatch is in the "ready" state is allowed,
107 | and causes the UA to synchronously finish the mutation immediately.
108 | This is useful to give the UA several frames of delay to do async work,
109 | while still guaranteeing that the work is put on the screen ASAP after that deadline.
110 |
111 | Calling `finish()` much later than the "started"=>"ready" transition
112 | *should* be identical to calling it immediately after the transition, as intended
113 | (a quick bit of sync work),
114 | but the UA *may* discard pending async work after a period of time
115 | or when it's under memory pressure.
116 | If this occurs, calling `finish()` just synchronously redoes the work,
117 | same as calling `finish()` before the DOMBatch is "ready".
118 |
119 | The essential guarantee here is that calling `finish()` must always succeed,
120 | and it must always block until the work is complete,
121 | guaranteeing that the DOM is in the desired state when it returns.
122 |
123 | # What Is Visible Before It Finishes?
124 |
125 | Nothing is added to the DOM until `finish()` is called.
126 |
127 | This enables an interesting trivial usage:
128 | one can do a sequence of DOM reads and *async* DOM writes,
129 | and then `finish()` them at the end for a sync insertion.
130 | This duplicates the behavior of the "FastDOM" library,
131 | which explicitly separates DOM reading and writing phases
132 | to prevent accidental interweaving
133 | triggering unwanted sync layouts.
134 | (Allowing the insertion to be fully async is even better,
135 | but not always possible or desired.)
136 |
137 | # Mutating the DOM While An Async Is In-Flight
138 |
139 | Doing the mutation work async assumes that
140 | the UA can accurately determine what selectors will apply to the content
141 | and thus how it will be laid out/painted/etc.
142 | Further mutations between the time the UA starts a batch of async work
143 | (DOMBatch state of "started")
144 | and when it's finished
145 | (DOMBatch state of "finished")
146 | can invalidate this work,
147 | requiring the UA to throw out the work-so-far and start over.
148 |
149 | While the DOMBatch is in the "started" state,
150 | mutations that invalidate the async work
151 | must result in the async work being silently restarted.
152 | The DOMBatch *must not* proceed to the "ready" state
153 | if the completed async work has been invalidated;
154 | it can only transition when its async work is both finished
155 | *and* known to not be invalidated.
156 | The spec does not otherwise place any requirements on *when*
157 | the async work is restarted.
158 |
159 | While the DOMBatch is in the "ready" state,
160 | mutations that invalidate the async work
161 | must silently mark the async work as invalid.
162 | The UA *may* silently restart invalid async work at any time.
163 | If `finish()` is called while the async work is completed but invalid,
164 | the UA *must* silently restart the async work
165 | (as if `finish()` was called early).
166 |
167 | When the DOMBatch is in the "finished" state,
168 | the work has been committed
169 | and no further changes can invalidate it.
170 |
171 | The essential guarantee here is that
172 | an author can naively call `finish()` when the UA tells them the work is done
173 | and have the operation successfully complete,
174 | even if further mutations have invalidated work in the meantime.
175 |
176 | UAs are encouraged to alert the author when this invalidation happens,
177 | such as thru logging a message to the console.
178 |
179 | Authors are encouraged to explicitly batch mutation work with a DOMBatch,
180 | and avoid mutating the DOM otherwise if at all possible.
181 |
182 | Issue: Unclear what to do if the mutation doesn't just invalidate the async work,
183 | but makes it impossible
184 | (such as deleting the parent you're trying to `asyncAppendChild()` to).
185 | Reject ready promise
186 | and throw an async error at `window`?
187 |
188 | # Script Elements
189 |
190 | `