├── .gitignore ├── CONTRIBUTING.md ├── EXPLAINER.md ├── LICENSE.md ├── README.md ├── async-lifecycle.png ├── index.html ├── sync-lifecycle.png └── w3c.json /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ![synchronous rendering lifecycle](sync-lifecycle.png) 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 | ![asynchronous rendering lifecycle](async-lifecycle.png) 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 | ` 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 |
31 |

Introduction

32 |

33 | See ReSpec's user guide 34 | for how toget started! 35 |

36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /sync-lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WICG/async-append/ef678c84ef6a32e26390aa67011ca1d6f897913e/sync-lifecycle.png -------------------------------------------------------------------------------- /w3c.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": ["80485"] 3 | , "contacts": ["yoavweiss"] 4 | , "shortName": "async-append" 5 | } --------------------------------------------------------------------------------