Re: [whatwg/dom] Proposal: DOMChangeList (#270)

Also, I'm not a huge fan of the API proposed - it's a bit too verbose and too much of a shotgun for something that's really just performing transactions on nodes. I'd personally prefer there was a way to just force browsers to batch something *without* having changes exposed to the main thread until the next animation frame, and a way to run changes in the layout pass to reduce the impact of style recalcs (which account for 90% of the perf issues with DOM frameworks).

Here's my thought on such an API, one that's a bit less ambitious:

```webidl
// `execute` runs after animation frames, after all previous transactions have been
// executed, but before intersection observers have been run. Any transactions started
// here are deferred to the *next* animation frame.
//
// When `execute` called, if any properties of a node all of these are true, then an
// `InvalidStateError` should be thrown:
// 1. The node being mutated is in the calling document.
// 2. The node is live, or would have been made live in the transaction.
// 3. UI-visible attributes of that node are read/modified, including ones like
//    `parentNode` and even if indirectly (like in a transaction).
// 4. The node *is not* in `read` (if read) or `write` (if modified).
dictionary LayoutOptions {
    AbortSignal signal;
    sequence<Node> read = [];
    sequence<Node> write = [];
    // I know this is invalid syntax, but `this` is supposed to be the layout options object
    Promise<any>? execute();
}

// Transactions assume the state right after the animation frames are run *are* the
// initial state - they don't actually save anything first, and they act as if the
// operations work on the state after animation frame callbacks run.
[Exposed=Window, Constructor]
interface DOMTransaction {
    void insertBefore(ParentNode node, ChildNode? node, ChildNode next);
    void insertAfter(ParentNode node, ChildNode? node, ChildNode prev);
    void replace(ChildNode node, ChildNode? other);
    void setAttribute(Element elem, DOMString key, DOMString? value, DOMString? ns);
    void toggleClass(Element elem, DOMString classes, boolean force);
    void setStyle(HTMLElement elem, DOMString key, DOMString? value);
    void setNodeValue(Node node, DOMString value);
    // The tree is locked between when animation frames and intersection observers
    // would be run. The fulfillment value is that of `callback.execute()` or
    // `undefined` if it wasn't passed.
    Promise<any> end(optional LayoutOptions? options);
}
```

There's a few specific omissions and design points with my proposal here that I thought I'd note:

1. Most DOM interactions can be reduced to these.
    - Append: `transaction.insertAfter(parent, null, node)`
    - Prepend: `transaction.insertBefore(parent, null, node)`
    - Insert before: `transaction.insertBefore(parent, next, node)`
    - Insert after: `transaction.insertAfter(parent, prev, node)`
    - Replace: `transaction.replace(node, other)`
    - Remove: `transaction.replace(node, null)`
    - Set attribute: `transaction.setAttribute(node, key, value, ns)`
    - Remove attribute: `transaction.setAttribute((node, key, null, ns)`
    - Set class: `transaction.toggleClass(node, classes, true)`
    - Remove class: `transaction.toggleClass(node, classes, false)`
    - Set node value: `transaction.setNodeValue(text, value)`
    - Set style: `transaction.setStyle(elem, key, value)`
    - Remove style: `transaction.setStyle(node, key, null)`
    - Set `id`/`className`/etc.: set that attribute

1. The few that can't could be handled in the `LayoutCallback` without having to spend an extra animation frame. (It also exists for the sake of refs, like what most vdom frameworks have some variant of.)

1. I chose to include both, and only, `insertBefore` and `insertAfter` as it's much simpler than the half dozen existing methods, and it works better with single-pass updates.

1. I chose to *exclude* properties that aren't available via normal DOM attributes because there's generally nothing left to do.

1. This is *technically* polyfillable without an explosion of code short of the throwing errors part. It would require wrapping `requestAnimationFrame` and `cancelAnimationFrame` to call the callbacks at the right time, but that's about it.

1. The reason I require explicit lists is so browsers can lock the others in that document to update them in parallel and be able to compute their layout without having to assume the prospect of interference with other code. (In practice, they only need *one* read/write lock to rule them all per document, and after that, a quick flag check whose value is initially calculated after animation frame callbacks are run to assert that the node is destined to become live.)

1. The choice of whether to fire an extra callback is deferred until the end because you might find while patching that you might not need to fire that layout callback (which would be potentially pretty expensive). If no layout callbacks are scheduled, the browser can compute layout immediately and exercise its normal magic, all off-thread.

1. The `DOMTreeConstruction` API is about 90% solved by `innerHTML` with `<template>`s, and in general, it looks to be solving a problem orthogonal to transactions (so I'm more neutral on it). You could still get very nice gains from using them in place of `document.createElement(elem)` with large trees, while still adding them via `elem.appendChild(tree)`. (I've witnessed this in action already with `template.content.cloneNode(true)` + a few `elem.querySelector` calls being *faster* than the equivalent `document.createElement` + `elem.appendChild` sequence, and in some cases, even when dynamically created with `innerHTML`.)

-- 
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/whatwg/dom/issues/270#issuecomment-366687117

Received on Monday, 19 February 2018 13:02:54 UTC