There are concerns around HTML Imports that does sync or blocking loading as the default. I’d like to address these concerns. Especially I’d like to understand the actual use cases that “sync” loading hurts. This document aims to be a portal of that effort.
Before discussing use cases, let’s clarify which part of page loading process HTML Imports possibly blocks. The blocking model of HTML Imports is modeled after the one for external stylesheets. There are two main bits of the blocking:
HTML Imports block
<script> elements. The
<script> doesn’t run until its preceding imports are loaded. This is because code in
require() for the
<script>. Otherwise, import users have to use AMD-like boilerplates. That will hurt the developer ergonomics.
<script> means the HTML parser stops there, which is bad. There are several workarounds for it. Also, there is a plan to make this better.
The first workaround is
<script async>. Async script is executed later and doesn’t block the parser. A variation of this is approach to put the
<script> at the end of HTML. A shortcoming of this approach is that there is no order guarantee and you need to take care of it by youself.
The second workaround is the forthcoming
<module> element (or
<script type=module>). This is similar to
<script async>. The difference is that it defines an ES6 module in HTML and is able to use ES6 loading system to resolve script dependency. HTML Imports is going to be integrated with the ES6 system loader so the ES6 module loading and
<module> works with HTML Imports well.
As spec editors, we are also thinking about letting
<script> in imports not block the parser, and just defer the script execution instead. This change needs tricky integration with HTML standard and we’re not ready to do this though. Follow bug 24042 to track the progress. Note that it is less risky to make this change in a future version of the spec because
document.write() in imports doesn’t work. See discussion on bug 24623. Note that this doesn’t affect the behavior of
<script> in the master document.
HTML Imports also block rendering. This means that the browser doesn’t render the page until all pending imports are loaded. This is similar to how stylesheet loading works today.
Talking about stylesheet, it blocks rendering because of FOUC. If a page is rendered before specified stylesheets are loaded, it will show some unintentional result. HTML Imports can have stylesheet, it follows same pattern. You can think about custom element definitions in same way: Unresolved custom elements can be rendered poorly. This is analogous to unstyled content and results in a poor user experience.
As explained in the following section, you can suppress this rendering-blocking behavior. However, you have to take care of FOUC by youself if you decide to load imports asynchronously.
For pages that adopt whole Web Components as its underlying framework, the FOUC prevention could be done by applying proper
:unresolved pseudo class styles to your custom elements. The style might be
display:none, or some thing that indicates the loading state or initial non-interactive state.
For pages that use HTML Imports but not Custom Elements, you could achieve same effect by toggling elements’ classes from loading to loaded, for example.
If an import doesn’t contain
<script>, it is applied even if previous imports haven’t loaded yet. If an import contains
<script> it blocks on imports before it. Sub-imports that are imported by other imports are also loaded in the same way. This utilizes browsers’ loading pipeline so we can expect faster load even without any workarounds.
HTML Imports spec defines @async attribute that tells HTML Import not to do any of the blocking. Yes, HTML Imports does support asynchronous loading!
The focus of this document is, therefore, to understand the motivation to make the some kind of asynchronous behaviour as the default.
So what to block? In summary:
- Imports block script execution and pending script execution blocks the parser.
- Script execution is delayed so that the author can use imports as dependency declaration like
- Pending script execution blocks the parser for historical reasons.
- We’re going to have some way to let the parser go without blocking on the script execution in the future like
<module>. We aren’t just there yet.
- Imports block rendering to avoid FOUC.
- All these blocking behavior can be disabled using
- If you go with
@async, you have to take care of FOUC. The
:unresolvedpseudo class is your friend.
The primary goal of asynchronous loading is, in my understanding, progressive loading and rendering of the page. There are many flavors of progressive loading these days, and each of them might need different solution. Here I’d like to see how HTML Imports fit in each context.
You might want to show something other than a blank page before your large application gets loaded. Typically this is some loading indicator.
Here is an assumption: When talking about big apps, the majority of the application binary size comes from the code (including templates) but not the contents. I’d like to to talk about progressive loading of contents (not the code) in a separate scenario that follows.
First of all, many smaller apps won’t have to make it async as many existing applications don’t. Mid-sized apps can also be loaded synchronously if you preprocess to concatenate multiple imports into one and reduce the network overhead.
For bigger apps, you might want to use
@async. You also have to prepare against FOUC if you adopt async imports.
Either way, FOUC prevention requires page authors some deliberation. If async loading is the default for HTML Imports, lack of care from the page author results in FOUC. If sync loading is the default on the other hand, the lack of care for loading speed (proper CDN usage, etc.) and missing @async results in a longer blank screen. This is a trade-off.
Your app can be loaded quickly enough so that you don’t need any loading indicator, but your contents might not come that fast. In this case, the app shows its navigation frame first, then loads the contents later, probably in incremental manner.
No fit. I don’t think people use imports as a dynamic content itself. imported components can use XHR to fill the content instead.
It is certainly possible to define and use
<x-tweet> though. In that case, async-as-default makes some sense: It allows asynchronous loading of the content.
For less often used UI, you might want to load them lazily to make the initial load size smaller.
The async attribute will help. Also, you can create the
<link> elements on demand through scripting, that behaves as if it has
@async attribute. It cannot be automatic because only the author knows which part of the app is less used beforehand.
For this purpose, it might be nice to have some imperative API, instead of using a
<link> element. It’s in the scope but won’t be in the initial version.
Third party contents like social buttons, typically embedded through
<iframe>, could be packaged as HTML Imports. The component will build some scaffolding in shadow (or non-shadow) DOM, and host
<iframe> in it.
There is a variation of this use case, that is drop-in widgets like image gallery and news ticker. The difference between drop-in widgets and third-party mashup here is that for drop-in widgets, the page author owns and controls its contents, thus it might be provided directly in the page DOM, instead of hosted in
These third party contents typically aren’t part of the master document, so it makes sense to load them asynchronous, progressive manner. As it is loaded asynchronously, the developer takes care of FOUC prevention. In practice, component providers will include such prevention tricks in their snippets and user will just copy-and-paste it.
Note that these import files won’t contain its contents in most cases. Instead, the component defined in the import will load the content dynamically through
Does the async-as-default discussion matter here? Less likely. The third-party providers tend to be experienced developers so they will use the @async and accompanying FOUC guard if necessary and provide reasonable snippets. The users will just copy it.
The contents of imports don’t have to be Web Components. Existing Libraries and frameworks could be packaged as an import as well.
I believe these are not the use case of asynchronous loading. They have to be loaded before the page is rendered. It will cause FOUC otherwise. I list this to clarify there are cases where people don’t need asynchronicity.
Here I'd try to summarize these use cases:
There are some cases where the author might prefer async loading:
- Large App (GMail)
- Content (Desktop Facebook)
- Third party Mashup (Like buttons)
I don’t think the “Large App” is good justification for deciding default as there are fewer number of large apps than smaller sized apps. The “Content” use-case is also less appealing: It is not what HTML Imports standard is designed or and there are good existing alternative like XHR.
The “Third-party Mashup” scenario is more compelling. They tend to be self contained and each is used as a blackbox. These “componentized” widgets use-case is one of the target of HTML Imports and Web Components in general.
So who wants sync loading? If you build apps using Web Components based frameworks, you are likely to prefer sync. In such apps, there is nothing to render until components are loaded. Imported components consist the essential part of the page. In this context, CSS Frameworks are similar to Web Components frameworks. If the whole page depends on imports, it makes less sense to load the dependency asynchronously.
This is contrasting to third-party scenario where imported components are more optional: You can read the page content without such third-party widgets. Another characteristics of third party components is that its interaction to other part of the page is less frequent. These components tend to be self-contained, and needs little API call to that.
In Web Components-based framework, each component instances is wired each other. The connection might be just a method calls or it is done by data binding or event propagation. It’s harder to build robust components with asynchronous loading than it is done in sync world where the component assume the component availability.
From performance perspective, giving up rendering-blocking has one disadvantage. That is “Style Churn” - If you don’t block rendering, the browser has to recalculate the style and repaint the page each time new stylesheet is added to the page. Same thing is said to custom element definition. Each new custom element definition is given, its population process results repaint. These recalculation and repaint is inefficient and results unpredictable slowness and junk.
This matters less if the number of stylesheet and custom element definitions in imports is small enough. This number will vary depending on the usage pattern of imports.
Third-party widgets including social buttons will have smaller number of imports. It will pack the whole set of features into one import and provide the single entry point. These imports will be coarse grained. Frameworks and its apps so far tend to go opposite. They provide large number of fine grained components through imports. Each import could have small component including tiny stylesheet. This means rendering-blocking makes more sense for Framework-built Apps in terms of style churn.
HTML Imports blocks script execution as default. This is because we designed the system as the dependency resolution system of Web Components umbrella standard. We don’t want to force component developers to manage asynchronous dependency resolution. We want to take care of it as a part of the platform.
The parser blocking is needed mostly because of historical reasons. We have plans to reduce it through upcoming standards including
We’re aware this is more controversial decision compared to script execution blocking. And the reasoning is same here. We designed this for a part of Web Components system, where apps are made with Custom Elements and Shadow DOM.
Specifically, there are two blocking factors. The first factor is mandating FOUC prevention to the developers. We don’t want to force developers to progressive loading. It complicates the development and worth less when the apps isn’t large. Another factor is style churn. Fine grained imports will result frequent style update and repainting, that will hurt the performance.
For rendering-blocking, I have a hope to have some knobs for controlling the blockage behavior. That isn’t necessarily a part of HTML Imports, as isn’t either. Because rendering-blocking isn’t imports specific problem, there should be more generic solution which HTML Imports can hook into.
We’re optimistic about @async adoption for proper area. The majority of third party widget SDK, the primary target customer of @async, provides the installation instruction as snippets, so adding @async there cannot be that hard. This is especially because HTML Imports supports
@async and there is no “legacy” imports.
Once ES6 module and
<module> become real, ES6 integration will become next big target for HTML Imports. As both modules and imports are dependency resolution system, there could be some coherence between two. The first step would be to define imperative API for HTML Imports. Having it, developer can share the patterns and idioms between these two systems. Then modules and imports can co-evolve based on the experience that the developer community will gain.
Another big topic is migration to HTML living standard. HTML Imports heavily patches HTML standard. That is inevitable but bad. So we’re thinking about merging the HTML Imports standard into the HTML standard once the base design is settled. We need some more research before actually starting the work, though.