WICG / virtual-scroller

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Searchable invisible DOM implies API changes

bicknellr opened this issue · comments

cc @domenic @graynorton @ojanvafai

The searchable invisible DOM / invisible attribute concept [0] was introduced after the current virtual scroller API was created but, as far as I understand, has become our intended tool for implementing it. It's meant to enable features like find-in-page (FIP) and same-page anchor / accessibility navigation for very large DOM trees by eliminating rendering costs for specially marked elements when they are in the main document. Specifically, content is still considered reachable only if it is in the main document, to avoid introducing entirely new and separate APIs to handle non-rendering content.

However, this new expectation of having all content actually be in the DOM tree has major (simplifying?) implications for the virtual scroller API because most of it is centered around the indirection between items and their representative elements. Further, if we find out that the complete DOM tree of all content intended to be 'virtualized' actually can't exist in the main tree because it isn't performant enough, then I think searchable invisible DOM fails to sufficiently achieve its goals of enabling FIP, same-page navigation, accessibility, etc. for non-rendered content and should not become part of the web platform. [1]

Here are the different pieces of the API that I think need to change and why:

  1. updateElement and recycleElement should be removed.

    These two callbacks are called when the virtual scroller needs to update or tear down the element representing an item because the virtual scroller has decided to remove the element from the document or update it with content representing a different item. However, searchable invisible DOM implies that all content should always be in the tree, so there is never a situation where an element is updated or recycled (at the virtual scroller's discretion). The virtual scroller itself can't decide when to change or remove parts of the tree because, if there are any items that do not map to DOM trees in the main document, then FIP and same-page navigation won't be able to show you / navigate to those items.

  2. createElement should be removed.

    This callback is called when the virtual scroller needs to create a new element to represent an item. However, given that searchable invisible DOM implies that there is no updating or recycling, this always occurs at exactly the same time that an item is added to the virtual scroller: each inserted item should be immediately realized as an element and inserted into the main document. It would be simpler for the user to supply the element itself, at the time they would insert the item, rather than indirecting this behavior through a callback.

  3. ItemSource should be removed.

    ItemSource is used as the virtual scroller's backing data source and the DOM tree managed by the virtual scroller is a direct mapping from these items. This indirection is important if it's necessary not to create elements for all items in the list: the virtual scroller uses ItemSource to get items to pass like keys to the user-supplied callbacks so that they can produce the their representative elements on demand, when the virtual scroller determines that they should be added and removed from the document. However, because elements for all items should be in the main document and the virtual scroller no longer has any control over this process - causing createElement, updateElement, and recycleElement to all be removed in favor of directly passing the DOM tree for each item - ItemSource no longer serves any purpose.

  4. itemsChanged() should be removed.

    This method lets the user to signal to the virtual scroller that the items in an ItemSource have changed, either in their order or in how they map to representative elements. However, with ItemSource removed and the user interacting directly with the virtual scroller in terms of real elements, the virtual scroller no longer needs a separate method to inform it that the mapping from items to elements has changed: there is no mapping and updates should occur exactly when the user interacts with the virtual scroller.

  5. scrollToIndex() should be removed.

    This method is intended to allow the user to scroll the virtual scroller to an item at a particular index in the ItemSource. This indirection was necessary when the virtual scroller was itself in control of the creation of items' representative elements: the user had no other way to ask the virtual scroller to scroll to a particular point other than to tell it about an item because the user can't know the DOM tree representing it until the virtual scroller created it. However, with ItemSource removed, the DOM tree representing the item is created by the user, always in the document, and the new semantics of searchable invisible DOM imply that Element#scrollIntoView should be able to scroll to any of those elements, even if they aren't rendered.

Although I'm less sure about these, the other parts of the virtual scroller API might also have room to be simplified given searchable invisible DOM:

  1. layout

    If searchable invisible DOM / invisible attribute and display locking are merged and display locking keeps the original dimensions of elements as they are inserted into the DOM, then it might be possible to remove the custom layout code and the layout property from virtual scroller. Particularly, the virtual scroller could allow off-screen elements to render and then be locked in chunks, leaving the overall layout of the (some unlocked and many locked) elements up to the browser. Either the browser or virtual scroller itself has to do at least enough layout for the hidden items dimensions to determine the (rough) position of the visible items, so it seems odd that virtual scroller could be faster if the same information about what to ignore is also being communicated to the browser through display locking.

    • Does this depend on display locking taking default dimensions in acquireDisplayLock to handle items that have never been rendered?
  2. rangechange event

    The events dispatched by the browser for showing invisible elements (activateinvisible, or whatever this changes to when merged with display locking) may be sufficient to inform the author that the visible set of elements has changed.

    • Does display locking do this? Maybe virtual scroller should dispatch its own events to individual children when it (un)locks them?

Even with all of this, we still need to supply virtual scroller with some kind of item-managing API. However, this API is now basically pared down to editing an ordered list of DOM nodes that end up being the real children of the virtual scroller... a.k.a. HTMLElement and its ancestor interfaces up to Node. So, I propose that, rather than introducing new API for communicating with the virtual scroller about items, virtual scroller should instead override methods that are normally used to modify its DOM structure (appendChild, removeChild, etc.) to also handle the associated visibility behavior.


[0] Although it sounds like it will be merged into Display Locking.
[1] If you have to build two tiers of 'virtualization' because one isn't good enough (searchable invisible DOM) and one is (removing the content from the tree altogether), why wouldn't you just learn / use the one good one for all cases?

In general I really like the insight here. In particular, it'll be good to erase the items-vs.-DOM distinction, given our focus on using searchable invisible DOM to ensure the DOM always has all the right content.

One concern is that we still want to make it easy to only pay the creation cost of children incrementally, using an idle-until-urgent pattern that creates them in the background---but can also create them immediately if the user scrolls and we still haven't gotten around to it. You can imagine a few APIs for this, e.g.

virtualScroller.createElement = index => { ... };

// ... or ...
virtualScroller.asyncAppend(() => { ... });

// ... or a decoupled library ...
asyncAppend(virtualScroller, () => { ... });

Of these it's key to keep in mind a couple scenarios:

  • On initial load, the virtual scrollbar should have a correctly-estimated scroll thumb size. So, probably you need to supply it with the total number of items, or at least an estimate, ASAP.

  • Even if you have only created 200 DOM elements in the background, if the user scrolls to position 300, you need to immediately create item 300, plus the others adjacent to 300 that would be visible.

One approach here would be to keep createElement(), but move it back to index-based, and then remove all the other things (updateElement, recycleElement, ItemSource, itemsChanged, scrollToIndex).


On specific implementation strategy:

So, I propose that, rather than introducing new API for communicating with the virtual scroller about items, virtual scroller should instead override methods that are normally used to modify its DOM structure (appendChild, removeChild, etc.) to also handle the associated visibility behavior.

This isn't really workable on a technical level; this kind of overriding is not very "webby" and is fragile. There are soo many APIs that add nodes, e.g. not just appendChild but also innerHTML, innerText, prepend, append, insertBefore, ...

The most robust solution would be to use a MutationObserver. I think the timing would probably still work; microtask timing is very quick, and if we quickly toggle something to invisible it should avoid most work. (If not, we should investigate whether we can improve our implementation.)