HTML Imports
Include for web
Why imports? #
Think about how you load different types of resources on the web. For JS, we have <script src>
. For CSS, your go-to is probably <link rel="stylesheet">
. For images it's <img>
. Video has <video>
. Audio, <audio>
… Get to the point! The majority of the web's content has a simple and declarative way to load itself. Not so for HTML. Here's your options:
<iframe>
- tried and true but heavy weight. An iframe's content lives entirely in a separate context than your page. While that's mostly a great feature, it creates additional challenges (shrink wrapping the size of the frame to its content is tough, insanely frustrating to script into/out of, nearly impossible to style).
- AJAX - I love
xhr.responseType="document"
, but you're saying I need JS to load HTML? That doesn't seem right. - CrazyHacks™ - embedded in strings, hidden as comments (e.g.
<script type="text/html">
). Yuck!
See the irony? The web's most basic content, HTML, requires the greatest amount of effort to work with. Fortunately, Web Components are here to get us back on track.
Getting started #
HTML Imports, part of the Web Components cast, is a way to include HTML documents in other HTML documents. You're not limited to markup either. An import can also include CSS, JavaScript, or anything else an .html
file can contain. In other words, this makes imports a fantastic tool for loading related HTML/CSS/JS.
The basics #
Include an import on your page by declaring a <link rel="import">
:
<head>
<link rel="import" href="/path/to/imports/stuff.html">
</head>
The URL of an import is called an import location. To load content from another domain, the import location needs to be CORS-enabled:
<!-- Resources on other origins must be CORS-enabled. -->
<link rel="import" href="http://example.com/elements.html">
Feature detection and support #
To detect support, check if .import
exists on the <link>
element:
function supportsImports() {
return 'import' in document.createElement('link');
}
if (supportsImports()) {
// Good to go!
} else {
// Use other libraries/require systems to load files.
}
Browser support is still in the early days. Chrome 31 was the first browser to see an implementation but other browser vendors are waiting to see how ES Modules play out. However, for other browsers the webcomponents.js polyfill works great until things are widely supported.
Bundling resources #
Imports provide convention for bundling HTML/CSS/JS (even other HTML Imports) into a single deliverable. It's an intrinsic feature, but a powerful one. If you're creating a theme, library, or just want to segment your app into logical chunks, giving users a single URL is compelling. Heck, you could even deliver an entire app via an import. Think about that for a second.
A real-world example is Bootstrap. Bootstrap is comprised of individual files (bootstrap.css, bootstrap.js, fonts), requires JQuery for its plugins, and provides markup examples. Developers like à la carte flexibility. It allows them buy in to the parts of the framework they want to use. That said, I'd wager your typical JoeDeveloper™ goes the easy route and downloads all of Bootstrap.
Imports make a ton of sense for something like Bootstrap. I present to you, the future of loading Bootstrap:
<head>
<link rel="import" href="bootstrap.html">
</head>
Users simply load an HTML Import link. They don't need to fuss with the scatter-shot of files. Instead, the entirety of Bootstrap is managed and wrapped up in an import, bootstrap.html:
<link rel="stylesheet" href="bootstrap.css">
<link rel="stylesheet" href="fonts.css">
<script src="jquery.js"></script>
<script src="bootstrap.js"></script>
<script src="bootstrap-tooltip.js"></script>
<script src="bootstrap-dropdown.js"></script>
...
<!-- scaffolding markup -->
<template>
...
</template>
Let this sit. It's exciting stuff.
Load/error events #
The <link>
element fires a load
event when an import is loaded successfully and onerror
when the attempt fails (e.g. if the resource 404s).
Imports try to load immediately. An easy way avoid headaches is to use the onload
/onerror
attributes:
<script>
function handleLoad(e) {
console.log('Loaded import: ' + e.target.href);
}
function handleError(e) {
console.log('Error loading import: ' + e.target.href);
}
</script>
<link rel="import" href="file.html"
onload="handleLoad(event)" onerror="handleError(event)">
Or, if you're creating the import dynamically:
var link = document.createElement('link');
link.rel = 'import';
// link.setAttribute('async', ''); // make it async!
link.href = 'file.html';
link.onload = function(e) {...};
link.onerror = function(e) {...};
document.head.appendChild(link);
Using the content #
Including an import on a page doesn't mean "plop the content of that file here". It means "parser, go off an fetch this document so I can use it". To actually use the content, you have to take action and write script.
A critical aha!
moment is realizing that an import is just a document. In fact, the content of an import is called an import document. You're able to manipulate the guts of an import using standard DOM APIs!
link.import #
To access the content of an import, use the link element's .import
property:
var content = document.querySelector('link[rel="import"]').import;
link.import
is null
under the following conditions:
- The browser doesn't support HTML Imports.
- The
<link>
doesn't haverel="import"
. - The
<link>
has not been added to the DOM. - The
<link>
has been removed from the DOM. - The resource is not CORS-enabled.
Full example
Let's say warnings.html
contains:
<div class="warning">
<style>
h3 {
color: red !important;
}
</style>
<h3>Warning!
<p>This page is under construction
</div>
<div class="outdated">
<h3>Heads up!
<p>This content may be out of date
</div>
Importers can grab a specific portion of this document and clone it into their page:
<head>
<link rel="import" href="warnings.html">
</head>
<body>
...
<script>
var link = document.querySelector('link[rel="import"]');
var content = link.import;
// Grab DOM from warning.html's document.
var el = content.querySelector('.warning');
document.body.appendChild(el.cloneNode(true));
</script>
</body>
Scripting in imports #
Imports are not in the main document. They're satellite to it. However, your import can still act on the main page even though the main document reigns supreme. An import can access its own DOM and/or the DOM of the page that's importing it:
Example - import.html that adds one of its stylesheets to the main page
<link rel="stylesheet" href="http://www.example.com/styles.css">
<link rel="stylesheet" href="http://www.example.com/styles2.css">
<style>
/* Note: <style> in an import apply to the main
document by default. That is, style tags don't need to be
explicitly added to the main document. */
#somecontainer {
color: blue;
}
</style>
...
<script>
// importDoc references this import's document
var importDoc = document.currentScript.ownerDocument;
// mainDoc references the main document (the page that's importing us)
var mainDoc = document;
// Grab the first stylesheet from this import, clone it,
// and append it to the importing document.
var styles = importDoc.querySelector('link[rel="stylesheet"]');
mainDoc.head.appendChild(styles.cloneNode(true));
</script>
Notice what's going on here. The script inside the import references the imported document (document.currentScript.ownerDocument
), and appends part of that document to the importing page (mainDoc.head.appendChild(...)
). Pretty gnarly if you ask me.
Rules of JavaScript in an import:
- Script in the import is executed in the context of the window that contains the importing
document
. Sowindow.document
refers to the main page document. This has two useful corollaries:- functions defined in an import end up on
window
. - you don't have to do anything crazy like append the import's
<script>
blocks to the main page. Again, script gets executed.
- functions defined in an import end up on
- Imports do not block parsing of the main page. However, scripts inside them are processed in order. This means you get defer-like behavior while maintaining proper script order. More on this below.
Delivering Web Components #
The design of HTML Imports lends itself nicely to loading reusable content on the web. In particular, it's an ideal way to distribute Web Components. Everything from basic HTML <template>
s to full blown Custom Elements with Shadow DOM [1, 2, 3]. When these technologies are used in tandem, imports become a #include
for Web Components.
Including templates #
The HTML Template element is a natural fit for HTML Imports. <template>
is great for scaffolding out sections of markup for the importing app to use as it desires. Wrapping content in a <template>
also gives you the added benefit of making the content inert until used. That is, scripts don't run until the template is added to the DOM). Boom!
import.html
<template>
<h1>Hello World!</h1>
<!-- Img is not requested until the <template> goes live. -->
<img src="world.png">
<script>alert("Executed when the template is activated.");</script>
</template>
index.html
<head>
<link rel="import" href="import.html">
</head>
<body>
<div id="container"></div>
<script>
var link = document.querySelector('link[rel="import"]');
// Clone the <template> in the import.
var template = link.import.querySelector('template');
var clone = document.importNode(template.content, true);
document.querySelector('#container').appendChild(clone);
</script>
</body>
Registering custom elements #
Custom Elements is another Web Component technology that plays absurdly well with HTML Imports. Imports can execute script, so why not define + register your custom elements so users don't have to? Call it…"auto-registration".
elements.html
<script>
// Define and register <say-hi>.
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {
this.innerHTML = 'Hello, <b>' +
(this.getAttribute('name') || '?') + '</b>';
};
document.registerElement('say-hi', {prototype: proto});
</script>
<template id="t">
<style>
::content > * {
color: red;
}
</style>
<span>I'm a shadow-element using Shadow DOM!</span>
<content></content>
</template>
<script>
(function() {
var importDoc = document.currentScript.ownerDocument; // importee
// Define and register <shadow-element>
// that uses Shadow DOM and a template.
var proto2 = Object.create(HTMLElement.prototype);
proto2.createdCallback = function() {
// get template in import
var template = importDoc.querySelector('#t');
// import template into
var clone = document.importNode(template.content, true);
var root = this.createShadowRoot();
root.appendChild(clone);
};
document.registerElement('shadow-element', {prototype: proto2});
})();
</script>
This import defines (and registers) two elements, <say-hi>
and <shadow-element>
. The first shows a basic custom element that registers itself inside the import. The second example shows how to implement a custom element that creates Shadow DOM from a <template>
, then registers itself.
The best part about registering custom elements inside an HTML import is that the the importer simply declares your element on their page. No wiring needed.
index.html
<head>
<link rel="import" href="elements.html">
</head>
<body>
<say-hi name="Eric"></say-hi>
<shadow-element>
<div>( I'm in the light dom )</div>
</shadow-element>
</body>
In my opinion, this workflow alone makes HTML Imports an ideal way to share Web Components.
Managing dependencies and sub-imports #
Sub-imports #
It can be useful for one import to include another. For example, if you want to reuse or extend another component, use an import to load the other element(s).
Below is a real example from Polymer. It's a new tab component (<paper-tabs>
) that reuses a layout and selector component. The dependencies are managed using HTML Imports.
paper-tabs.html (simplified):
<link rel="import" href="iron-selector.html">
<link rel="import" href="classes/iron-flex-layout.html">
<dom-module id="paper-tabs">
<template>
<style>...</style>
<iron-selector class="layout horizonta center">
<content select="*"></content>
</iron-selector>
</template>
<script>...</script>
</dom-module>
App developers can import this new element using:
<link rel="import" href="paper-tabs.html">
<paper-tabs></paper-tabs>
When a new, more awesome <iron-selector2>
comes along in the future, you can swap out <iron-selector>
and start using it straight away. You won't break your users thanks to imports and web components.
Dependency management #
We all know that loading JQuery more than once per page causes errors. Isn't this going to be a huge problem for Web Components when multiple components use the same library? Not if we use HTML Imports! They can be used to manage dependencies.
By wrapping libraries in an HTML Import, you automatically de-dupe resources. The document is only parsed once. Scripts are only executed once. As an example, say you define an import, jquery.html, that loads a copy of JQuery.
jquery.html
<script src="http://cdn.com/jquery.js"></script>
This import can be reused in subsequent imports like so:
import2.html
<link rel="import" href="jquery.html">
<div>Hello, I'm import 2</div>
ajax-element.html
<link rel="import" href="jquery.html">
<link rel="import" href="import2.html">
<script>
var proto = Object.create(HTMLElement.prototype);
proto.makeRequest = function(url, done) {
return $.ajax(url).done(function() {
done();
});
};
document.registerElement('ajax-element', {prototype: proto});
</script>
Even the main page itself can include jquery.html if it needs the library:
<head>
<link rel="import" href="jquery.html">
<link rel="import" href="ajax-element.html">
</head>
<body>
...
<script>
$(document).ready(function() {
var el = document.createElement('ajax-element');
el.makeRequest('http://example.com');
});
</script>
</body>
Despite jquery.html being included in many different import trees, it's document is only fetched and processed once by the browser. Examining the network panel proves this: HTML Imports are totally awesome but as with any new web technology, you should use them wisely. Web development best practices still hold true. Below are some things to keep in mind. Reducing network requests is always important. If you have many top-level import links, consider combining them into a single resource and importing that file! Vulcanize is an npm build tool from the Polymer team that recursively flattens a set of HTML Imports into a single file. Think of it as a concatenation build step for Web Components. Many people forget that the browser's networking stack has been finely tuned over the years. Imports (and sub-imports) take advantage of this logic too. The Think of content as inert until you call upon its services. Take a normal, dynamically created stylesheet: The browser won't request styles.css until Another example is dynamically created markup: The The same concept holds true for the import document. Unless you append it's content to the DOM, it's a no-op. In fact, the only thing that "executes" in the import document directly is Imports block rendering of the main page. This is similar to what To be completely asynchronous and not block the parser or rendering, use the The reason You can also create an async import, dynamically: Imports don't block parsing of the main page. Scripts inside imports are processed in order but don't block the importing page. This means you get defer-like behavior while maintaining proper script order. One benefit of putting your imports in the Depending on your app structure and use case, there are several ways to optimize async behavior. The techniques below mitigate blocking the main page rendering. Scenario #1 (preferred): you don't have script in My recommendation for placing Here's an example: Everything is at the bottom. Scenario 1.5: the import adds itself Another option is to have the import add its own content. If the import author establishes a contract for the app developer to follow, the import can add itself to an area of the main page: Scenario #2: you have script in If you have an import that takes a long time to load, the first Alternatively, add the import near the end of the An import's mimetype is Resources from other origins need to be CORS-enabled. Imports from the same URL are retrieved and parsed once. That means script in an import is only executed the first time the import is seen. Scripts in an import are processed in order, but do not block the main document parsing. An import link doesn't mean "#include the content here". It means "parser, go off an fetch this document so I can use it later". While scripts execute at import time, stylesheets, markup, and other resources need to be added to the main page explicitly. Note, HTML Imports allow bundling HTML/CSS/JS as a single resource. While useful by themselves, this idea becomes extremely powerful in the world of Web Components. Developers can create reusable components for others to consume and bring in to their own app, all delivered through HTML Imports are a simple concept, but enable a number of interesting use cases for the platform. Distribute related HTML/CSS/JS as a single bundle. Theoretically, you could import an entire web app into another. Code organization - segment concepts logically into different files, encouraging modularity and reusability**. Deliver one or more Custom Element definitions. An import can be used to register and include them in an app. This practices good software patterns, keeping the element's interface/definition separate from how its used. Manage dependencies - resources are automatically de-duped. Chunk scripts - before imports, a large-sized JS library would have its file wholly parsed in order to start running, which was slow. With imports, the library can start working as soon as chunk A is parsed. Less latency! Parallelizes HTML parsing - first time the browser has been able to run two (or more) HTML parsers in parallel. Enables switching between debug and non-debug modes in an app, just by changing the import target itself. Your app doesn't need to know if the import target is a bundled/compiled resource or an import tree.Performance considerations #
Concatenate imports #
Imports leverage browser caching #
http://cdn.com/bootstrap.html
import might have sub-resources, but they'll be cached.Content is useful only when you add it #
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'styles.css';link
is added to the DOM:document.head.appendChild(link); // browser requests styles.css
var h2 = document.createElement('h2');
h2.textContent = 'Booyah!';h2
is relatively meaningless until you add it to the DOM.<script>
. See scripting in imports.Optimizing for async loading #
Imports block rendering #
<link rel="stylesheet">
do. The reason the browser blocks rendering on stylesheets in the first place is to minimize FOUC. Imports behave similarly because they can contain stylsheets.async
attribute:<link rel="import" href="/path/to/import_that_takes_5secs.html" async>
async
isn't the default for HTML Imports is because it requires developers to do more work. Synchronous by default means that HTML Imports that have custom element definitions inside of them are guaranteed to load and upgrade, in order. In a completely async world, developers would have to manage that dance and upgrade timings themselves.var l = document.createElement('link');
l.rel = 'import';
l.href = 'elements.html';
l.setAttribute('async', '');
l.onload = function(e) { ... };Imports do not block parsing #
<head>
is that it lets the parser start working on the content as soon as possible. With that said, it's critical to remember <script>
in the main document still continues to block the page. The first <script>
after an import will block page rendering. That's because an import can have script inside that needs to be executed before the script in the main page.<head>
<link rel="import" href="/path/to/import_that_takes_5secs.html">
<script>console.log('I block page rendering');</script>
</head><head>
or inlined in <body>
<script>
is to avoid immediately following your imports. Move scripts as late in the game as possible…but you're already doing that best practice, AREN'T YOU!? ;)<head>
<link rel="import" href="/path/to/import.html">
<link rel="import" href="/path/to/import2.html">
<!-- avoid including script -->
</head>
<body>
<!-- avoid including script -->
<div id="container"></div>
<!-- avoid including script -->
...
<script>
// Other scripts n' stuff.
// Bring in the import content.
var link = document.querySelector('link[rel="import"]');
var post = link.import.querySelector('#blog-post');
var container = document.querySelector('#container');
container.appendChild(post.cloneNode(true));
</script>
</body>import.html:
<div id="blog-post">...</div>
<script>
var me = document.currentScript.ownerDocument;
var post = me.querySelector('#blog-post');
var container = document.querySelector('#container');
container.appendChild(post.cloneNode(true));
</script>index.html
<head>
<link rel="import" href="/path/to/import.html">
</head>
<body>
<!-- no need for script. the import takes care of things -->
</body><head>
or inlined in <body>
<script>
that follows it on the page will block the page from rendering. Google Analytics for example, recommends putting the tracking code in the <head>
, If you can't avoid putting <script>
in the <head>
, dynamically adding the import will prevent blocking the page:<head>
<script>
function addImportLink(url) {
var link = document.createElement('link');
link.rel = 'import';
link.href = url;
link.onload = function(e) {
var post = this.import.querySelector('#blog-post');
var container = document.querySelector('#container');
container.appendChild(post.cloneNode(true));
};
document.head.appendChild(link);
}
addImportLink('/path/to/import.html'); // Import is added early :)
</script>
<script>
// other scripts
</script>
</head>
<body>
<div id="container"></div>
...
</body><body>
:<head>
<script>
// other scripts
</script>
</head>
<body>
<div id="container"></div>
...
<script>
function addImportLink(url) { ... }
addImportLink('/path/to/import.html'); // Import is added very late :(
</script>
</body>Things to remember #
text/html
.<style>
don't need to be added explicitly. This is a major difference between HTML Imports and <iframe>
, which says "load and render this content here".Conclusion #
<link rel="import">
.Use cases #
`<link rel="import" href="chunks.html">`:
<script>/* script chunk A goes here */</script>
<script>/* script chunk B goes here */</script>
<script>/* script chunk C goes here */</script>
...