Performance and Custom Elements

November 26, 2013 3:43 pm | 6 Comments

This past week I dug into the performance of custom elements and found some surprises.

In my previous blog post, Async Ads with HTML Imports, I complained that HTML imports block the page from rendering when a SCRIPT tag is found and lamented the fact that the spec doesn’t provide a mechanism to make this coupling asynchronous. Custom elements are just the opposite: they’re asynchronous by default and the spec doesn’t provide a way to make them synchronous.

Cake and eat it too

It’s important to understand why we need both synchronous AND asynchronous mechanisms for loading content.

  • Sometimes content is so critical to the page it should be rendered before anything else. In these situations it’s necessary to load the content synchronously in order to achieve the user experience desired by the website owner and designers. Synchronous loading is also necessary in other situations such as when there are dependencies between resources (e.g., interdependent scripts) and to avoid jarring changes in page layout (also known as Flash of Unstyled Content or FOUC).
  • Other times, the content coming from sub-resources in the page is secondary to the main page’s content and developers & designers prefer to load it asynchronously. This is a newer pattern in web development. (I like to think I had something to do with it becoming popular.) Loading these less critical resources asynchronously produces a better user experience in terms of faster loading and rendering.

The bottomline is there are situations that call for both behaviors and developers need a way to achieve the user experience they deem appropriate. The main role for specs and browsers is to provide both mechanisms and choose a good default. We didn’t do that in the previous versions of HTML and are trying to fill that gap now with the Resource Priorities spec which adds the lazyload attribute to various tags including IMG, SCRIPT, and LINK. We don’t want to repeat this gap-filling-after-the-fact process in the future, so we need to provide sync and async capabilities to the HTML5 features being spec’ed now – and that includes Web Components.

Custom Elements howto

Note that right now the only browser I found that supports Web Components is Chrome Canary – so you’ll need to install that to play with the examples. I turned on the following flags in chrome://flags/: Experimental Web Platform features, Experimental JavaScript, and HTML Imports.

The way to define a custom element is in JavaScript. Here’s the custom element used in my examples. It creates a new custom element called x-foo:

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
    this.innerHTML = '<div id="imported-content" style="background: #E99; border: 2px; font-size: 2em; text-align: center; padding: 8px; height: 100px;">CUSTOM ELEMENT</div>';
};

var XFoo = document.register('x-foo', {prototype: XFooProto});

To make custom elements more reusable they’re wrapped inside an HTML import:

<link rel="import" href="import-custom-element.php">

In the HTML document we can use the custom element just like any other HTML tag:

<x-foo></x-foo>

Experienced developers recognize that this creates a race condition: what happens if the x-foo tag gets parsed before import-custom-element.php is done downloading?

(async) Custom Elements = FOUC

The first example, custom-element.php, demonstrates the typical custom element implementation described above. If you load it (in Chrome Canary) you’ll see that there’s a Flash of Unstyled Content (FOUC). This reveals that browsers handle custom elements asynchronously: the HTML import starts downloading but the browser continues to parse the page. When it reaches the x-foo tag it skips over it as an unrecognized element and renders the rest of the page. When the HTML import finishes loading the browser backfills x-foo which causes the page’s content to jump down ~100 pixels – a jarring FOUC experience.

This is great for faster rendering! I love that the default is async. And there are certainly scenarios when this wouldn’t created FOUC (custom elements that aren’t visible or will be used later) or the FOUC isn’t so jarring (below-the-fold, changes style but not layout). But in cases like this one where the FOUC is undesirable, there needs to be a way to avoid this disruptive change in layout. Sadly, the spec doesn’t provide a way of doing this. Let’s look at two possible workarounds.

Sized Custom Elements

The jarring change in layout can be avoided if the main page reserves space for the custom element. This is done in the custom-element-sized.php example like this:

<div style="height: 120px;">
<x-foo></x-foo>
</div>

The custom element is inside a fixed size container. As shown by this example, the existing page content is rendered immediately and when the HTML import finally finishes downloading the custom element is backfilled without a change in layout. We’ve achieved the best of both worlds!

The drawback to this approach is it only works for custom elements that have a fixed, predefined size. That condition might hold for some custom elements, but certainly not for all of them.

Sync Custom Elements

The custom-element-sync.php example shows a workaround to avoid FOUC for custom elements that have an unknown size. Unfortunately, this technique blocks rendering for everything in the page that occurs below the custom element. The workaround is to add a SCRIPT tag right above the custom element, for example:

<script>
var foo=128;
</script>
<x-foo></x-foo>

As shown in my previous post, HTML imports cause the parser to stop at the first SCRIPT tag that is encountered. There is a slight benefit here of making sure the only SCRIPT tag after the <link rel="import"...> is right before the custom element – this allows the content above the custom element to render without being blocked. You can see this in action in the example – only the content below the custom element is blocked from rendering until the HTML import finishes loading.

By blocking everything below the custom element we’ve avoided the FOUC issue, but the cost is high. Blocking this much content can be a bad user experience depending on the main page’s content. Certainly if the custom element occupied the entire above-the-fold area (e.g., on a mobile device) then this would be a viable alternative.

It would be better if the spec for custom elements included a way to make them synchronous. One solution proposed by Daniel Buchner and me to W3 Public Webapps is to add an attribute called “elements” to HTML imports:

<link rel="import" href="elements.html" elements="x-carousel, x-button">

The “elements” attribute is a list of the custom elements that should be loaded synchronously. (In other words, it’s NOT the list of all custom elements in the HTML import – only the ones that should cause rendering to be blocked.) As the browser parses the page it would skip over all custom elements just as it does now, unless it encounters a custom element that is listed in the “elements” attribute value (e.g., “x-carousel” and “x-button”). If one of the listed custom elements is reached, the parser would block until either the custom element becomes defined or all outstanding HTML import requests are done loading.

Tired of hacks

I love finding ways to make things work the way I want them to, but it’s wrong to resort to hacks for these new HTML5 features to achieve basic behavior like avoiding FOUC and asynchronously loading. Luckily, the specs and implementations are in early stages. Perhaps there’s still time to get them changed. An important part of that is hearing from the web development community. If you have preferences and use cases for HTML imports and custom elements, please weigh in. A little effort today will result in a better Web tomorrow.

Many thanks to the authors for these fantastic articles on Web Components:

6 Responses to Performance and Custom Elements

  1. [While my spam blocker was broken there was a comment from Dimitri Glazkov:]

    “Sadly, the spec doesn’t provide a way of doing this. Let’s look at two possible workarounds.”

    Have you looked at :unresolved? It’s designed specifically for this purpose.

  2. [While my spam blocker was broken there was a comment from Dominic Cooney:]

    You mention that the only browser you found that supports Web Components is Chrome Canary. Actually, there’s support in every current version of Chrome (behind the same flag) and Firefox (behind similar flags.)

    I get the impression that you think Custom Elements are always loaded asynchronously. That’s not correct. Custom Elements are defined by script (document.register) which as you know can run synchronously or asynchronously. If you want synchronous Custom Elements, just call document.register synchronously; if you want asynchronous Custom Elements, just call document.register asynchronously.

    Regarding the “race condition” of what happens if a Custom Element is used before it is defined, this is well covered in the spec. The element goes through a process called “upgrade” when the definition becomes available.

    The FOUC is a tough one. You’ve got two options: define your elements synchronously (see above), or use style to stop the page jittering around when the elements are upgraded. We have a CSS pseudo-thingie specifically for this, :unresolved, which applies to a Custom Element that does not have a definition. If you want to apply a style only when a definition is available, you can use :not(:unresolved). You still have to work out the size; this problem applies to all asynchronously loaded content.

  3. Dimitri: The :unresolved pseudo class can help by hiding custom elements until the HTML Import finishes loading. That helps the FOUC issue where the style of the custom element changes (colors, etc.). But that doesn’t help the FOUC issue highlighted here where the size of the custom element changes and cause the page to re-layout.

  4. Dominic: Thanks for mentioning the other browsers.

    I’m focusing on Custom Elements delivered via HTML Imports. I think that’s the best packaging – it results in Web Components that are more re-usable and I suspect it’ll be the most typical way Custom Elements are implemented.

    As you point out, the :unresolved pseudo class doesn’t address the FOUC layout issue caused by changes in size. Since Web Components is new perhaps we can solve this problem before throwing in the towel. The “elements” suggestion solves the problem. What do you think of that?

  5. Any reason you didn’t mention :unresolved as another (better) solution to FOUC?

    http://www.polymer-project.org/articles/styling-elements.html#preventing-fouc

  6. Dean: The :unresolved pseudo class can help by hiding custom elements until the HTML Import finishes loading. That helps the FOUC issue where the style of the custom element changes (colors, etc.). But that doesn’t help the FOUC issue highlighted here where the size of the custom element changes and cause the page to re-layout.