D&D5e, Web-components, & Performance

tl;dr — web-components (edit: with shadowRoots) are kind of slow.

The graph shows the average of five trials (post warmup) in Chrome on my Macbook Pro (it’s a maxed-out 2015 model). I saw nothing to suggest that a larger number of trials would significantly change the results.

Over the long weekend I was hoping to play some D&D with my kids (it never ended up happening). One of the things I find pretty frustrating with the new (well, 2014) Players Handbook is that the spells are all mixed together in strict alphabetical order. (It’s not like earlier editions were better organized. It’s also not clear how they could really fix this, since any arrangement will either require repeating spell descriptions or some kind of tedium.) So to figure out what spells are available to, say, a first level Druid, and what they do, you need to do a lot of page-flipping and cross-referencing.

As an aside, in the late-lamented DragonQuest (SPI, 1980), magic is divided up into “colleges” with a given character having access to the spells of only one college. Each college, along with its spells, is described in one place, with the only page-flipping being required for a relatively small number of spells that are common between colleges. A typical college has access to 20-30 different spells which are thematically unified (e.g. Adepts of the College of Fire have fire-related spells).

This kind of organization is not possible for D&D because the overlap between classes is so great. Sorcerors essentially get a subset of Wizard spells, as do Bards. And so on.

So it struck me that some kind of easily filterable list of spells which let you decide how you wanted them sorted and what you wanted shown would be handy, and this seemed like a nice test for b8r’s web-component system.

Now, b8r is the third rewrite of a library I originally developed with former colleagues for USPTO. Initially we had a highly convoluted “data-table” (not an original name, of course) that essentially let you point a descriptive data-structure at an array and get a table with all kinds of useful bells and whistles. This quickly became unmanageable, and was particularly horrible when people started using it to create quite simple tables (e.g. essentially short lists of key/value pairs) and then add fancy behaviors (such as drag-reordering) to them.

So, I developed a much simpler binding system (“bind-o-matic”) that allowed you to do fine-grained bindings but didn’t try to do bells and whistles, and it quickly proved itself better even for the fancy tables. b8r‘s data-binding is the direct descendant of bind-o-matic.

Anyway, all-purpose tables are hard. So I looked around for something off-the-shelf that would just work. (I’m trying to purge myself of “not invented here” syndrome, and one of the joys of web-components is that they’re supposed to be completely interoperable, right?) Anyway, the closest thing I could find (and damn if I could find a compelling online demo of any of them) had been abandoned, while the next closest thing was “inspired by” the abandoned thing. Not encouraging.

So I decided to write a minimal conceptual set of custom elements — in essence placeholder replacements for <table> <tr> <td> and <th> — which would have zero functionality (they’d just be divs or spans with no behavior) that I could later add CSS and other functionality to in order to get desired results (e.g. the ability to switch a view from a virtual grid of “cards” to a virtual table with a non-scrolling header and resizable columns — i.e. “the dream” of data-table implementations, right?

Now, I have implemented a function named makeWebComponent that will make you a custom element with one line of code, e.g. makeWebComponent('foo-bar', {}). So I made a bunch of components exactly that way and was horrified to see that my test page, which had been loading in ~1000ms was suddenly taking a lot longer. (And, at the time it was loading b8r from github and loading the spell list from dnd5eapi.co and then making an extra call for each of the 300-odd spells in the list.)

So, I ended up implementing my interactive D&D spell list using old school b8r code and got on with my life.

Hunting down the problem

I’m pretty enthusiastic about web-components, so this performance issue was a bit of a shock. So I went to the bindinator benchmark page (which lets me compare the performance of b8r to special-purpose vanilla javascript code doing the same thing with foreknowledge of the task.

In other words, how fast is b8r compared with javascript hand-coded to do the exact same thing in a completely specific way? (An example of how unfair the comparison can be is swapping two items in a large array and then updating the DOM vs. simply moving the two items.)

So the graph at the top of the page compares creating a table of 10,000 rows with a bunch of cells in each row using vanilla js to b8r, and then the other three columns show b8r with two simple <a> tags replaced with <simple-span>, where <simple-span> is defined using makeWebComponent vs. hand-coded (but still with a style node) vs. hand-coded (with nothing except a slot to contain its children).

class SimpleSpan extends HTMLElement {
  constructor() {
    super();

    // const style = document.createElement('style');
    // style.textContent = ':host { font-style: italic; }';
    const shadow = this.attachShadow({mode: 'open'});
    const slot = document.createElement('slot');
    // shadow.appendChild(style);
    shadow.appendChild(slot);
  }
}
window.customElements.define('simple-span', SimpleSpan);

Above is the hand-coded <simple-span> so you can see exactly what I’m doing and second-guess me. The commented-out lines are the difference between the fourth and fifth columns.

I should add that I tried rewriting the above code to use cloneNode(true) instead of creating the new nodes in the constructor, but performance was no different in this case. If the nodes being created were more complex there would likely be an advantage. (I make extensive use of cloneNode(true) in b8r since past experimentation showed a benefit, and makeWebComponent makes use of it.)

I should add that even the simplest component made with makeWebComponent has a style node because Google’s best practices suggest that all custom elements should support the hidden attribute, and doing this with styles is by far the simplest (and I would hope the most performant) way to do this.

It also occurred to me that having the <style> contain :host { font-style: italic; } might be more expensive that :host([hidden]) { display: none; } but, again, that was a wash. Similarly, it occurred to me that initializing the shadowRoot as {mode: 'closed'} might help. It didn’t.

So, this appears to show that the overhead for just two trivial custom elements in each of 10,000 rows is comparable to the overhead for b8r in its entirety for the entire table row. Now, b8r is creating a given table row by cloning, ad-hoc, the hierarchy it’s using as a template, and then querying that hierarchy to find bound elements and then inserting values as appropriate.

When you consider the kinds of uses to which browsers put custom elements internally (e.g. <video> and <input type=”data”>) these are generally not something you’re going to have 20,000 of in the DOM at once. It’s easy, however, to imagine wanting every item in a pretty huge list to include a custom checkbox or popup menu. It’s worth knowing that even simple custom elements have a non-trivial overhead (and we’re talking 1ms for 20 of them on a fast, high-end laptop).

Addendum

I didn’t realize that you could create a web-component with no shadowRoot. (This isn’t exactly something anyone makes obvious — I’ve found literally zero examples of this in tutorials, etc.)

If you do this, the performance issue mostly goes away.

Now, you do lose all the “benefits” of the shadow DOM but you gain performance and still can attach style rules to custom (unique!) tagNames, which makes managing the resulting CSS easier. This is particularly nice given my desire to replace b8r components with custom elements, since there needn’t be any performance overhead (you can “seal” the styling in a container with just one shadowRoot rather than ten thousand instances).

Heterogeneous Lists

b8r's demo site uses a heterogeneous list to display source files with embedded documentation, tests, and examples
b8r’s demo site uses a heterogeneous list to display source files with embedded documentation, tests, and examples

One of the things I wanted to implement in bindinator was heterogeneous lists, i.e. lists of things that aren’t all the same. Typically, this is implemented by creating homogeneous lists and then subclassing the list element, even though the individual list elements may have nothing more in common with one another than the fact that, for the moment, they’re in the same list.

This ended up being pretty easy to implement in two different ways.

The first thing I tried was a effectively an empty (as in no markup) “router” component. Instead of binding the list to a superclass component, you bind to a content-free component (code, no content) which figures out which component you really want programmatically (so it can be arbitrarily complex) and inserts one of those over itself. This is a satisfactory option because it handles both simple cases and complex cases quite nicely, and didn’t actually touch the core of bindinator.

Here’s the file-viewer code in its entirety:

<script>
    switch (data.file_type || data.url.split('.').pop()) {
        case 'md':
        case 'markdown':
            b8r.component('components/markdown-viewer').then(viewer => {
                b8r.insertComponent(viewer, component, data);
            });
            break;

        case 'text':
            b8r.component('components/text-viewer').then(viewer => {
                b8r.insertComponent(viewer, component, data);
            });
            break;

        case 'js':
            b8r.component('components/literate-js-viewer').then(viewer => {
                b8r.insertComponent(viewer, component, data);
            });
            break;
    }
</script>

(I should note that this router is not used for a list in the demo site, since the next approach turned out to meet the specific needs for the demo site.)

The example of this approach in the demo code is the file viewer (used to display markdown, source files, and so on). You pass it a file and it figures out what type of file it is from the file type and then picks the appropriate viewer component to display it with. In turn this means that a PNG viewer, say, need have nothing in common with a markdown viewer, or an SVG viewer. Or, to put it another way, we can use a standalone viewer component directly, rather than creating a special list component and mixing-in or subclassing the necessary stuff in.

You’ll note that this case is pretty trivial — we’re making a selection based directly on the file_type property, and I thought it should be necessary to use a component or write any code for such a simple case.

The second approach was that I added a toTarget called component_map that let you pick a component based on the value of a property. This maps onto a common JSON pattern where you have a heterogeneous array of elements, each of which has a common property (such as “type”). In essence, it’s a toTarget that acts like a simple switch statement, complete with allowing a default option.

The example of this in the demo app is the source code viewer which breaks up a source file into a list of different kinds of things (source code snippets, markdown fragments, tests, and demos). These in turn are rendered with appropriate components.

This is what a component_map looks like in action:

<div
  data-list="_component_.parts"
  data-bind="component_map(
    js:js-viewer|
    markdown:markdown-viewer|
    component:fiddle|
    test:test
  )=.type"
>
  <div data-component="loading"></div>
</div>

From my perspective, both of these seem like pretty clean and simple implementations of a common requirement. The only strike against component_map is obviousness, and in particular the quasi-magical difference between binding to _component_.parts vs. .type, which makes me think that while the latter is convenient to type, forcing the programmer to explicitly bind to _instance_.type might be clearer in the long run.

P.S.

Anyone know of a nice way to embed code in blog posts in WordPress? All I can find are tools for embedding hacks in wordpress.