Building repeated markup by concatenating strings has always been a slow, error-prone default. <template> gives the platform a native container for real, parser-validated, inert HTML ā ready to clone, exactly when needed.
1Genuine Inertness ā Not Just Hidden
Content placed inside <template> is parsed by the browser as real, valid HTML ā but it never renders, and more importantly, nothing inside it activates: an <img>'s src is never fetched, a <script> never executes, a <video>'s autoplay never triggers, and custom elements inside it never upgrade or run their lifecycle callbacks. This is meaningfully different from display: none, which still fully activates and renders its content invisibly ā <template> content simply doesn't exist as live DOM until explicitly cloned out.
This inertness is precisely what makes <template> safe and efficient as a holding area for markup meant to be stamped out repeatedly ā a card, a table row, a list item ā without any of its internal resources loading or side effects firing until an actual instance is genuinely needed.
2The Cloning API: .content And cloneNode(true)
A <template> element's actual markup isn't a direct child of the template in the normal DOM sense ā it lives in the .content property, a DocumentFragment. Calling .content.cloneNode(true) performs a deep clone of that fragment, producing an entirely new, independent copy ready to be modified (setting text, attributes, event listeners) and inserted into the live document via appendChild or similar.
Because cloning never consumes or mutates the original template, the same <template> can be cloned an unlimited number of times ā the standard pattern for rendering a list: clone once per item, populate the clone with that item's data, append it, and repeat.
3The Real Performance Case Against innerHTML String Building
The browser's HTML parser processes a <template>'s markup exactly once, at initial page parse time. Repeatedly building markup with string concatenation and innerHTML += inside a loop forces the parser to re-parse an ever-growing (or repeatedly reconstructed) HTML string on every single iteration ā work that scales poorly as list length grows, and that <template> cloning simply avoids entirely by reusing already-parsed structure.
Beyond raw parsing cost, innerHTML-based string building is also a well-known XSS vector whenever any part of the concatenated string includes unescaped, untrusted data ā a risk <template>'s own static markup sidesteps by construction, since it's authored directly as trusted HTML rather than assembled from runtime strings.
4The Foundation For Reusable Components ā And What Cloning Doesn't Make Safe
<template> is the standard source-of-markup mechanism paired with custom elements: a component's connectedCallback commonly clones a <template>'s content directly into its Shadow DOM, giving the component real, structural HTML instead of JS-generated strings ā covered in depth in the Reusable HTML Components lesson later in this module.
One important caveat: <template>'s inertness protects its own *static, author-written* markup ā it says nothing about the safety of data you actively insert into a cloned copy afterward. Setting .textContent on a cloned node with user-supplied data remains the correct, safe approach; setting .innerHTML on a clone with untrusted user data is exactly as much an XSS risk as it would be anywhere else in the DOM. Template inertness and injection safety are two entirely separate concerns.
5Step-by-Step Breakdown
HTML That Doesn't Render Until You Say So. Building repeated UI (a list item, a card, a table row) by concatenating HTML strings is slow, XSS-prone, and hard to read. <template> holds real, parser-validated HTML that stays completely inert ā invisible, inactive, unexecuted ā until JavaScript explicitly clones it.
Content Inside <template> Is Inert By Default. Anything inside a <template> element ā including images, scripts, and custom elements ā is parsed as valid HTML but never rendered, never fetched (an <img> inside won't request its src), and never executed (a <script> inside won't run) until it's cloned out into the active document.
template Inertness. Does an <img> inside a <template> element fetch its src before the template's content is cloned out?
- āNo ā resources inside a template aren't fetched until cloned into the active document
- āYes, it fetches immediately just like a normal <img>
- āOnly the first image in the template is fetched
content Is A DocumentFragment, Cloned With cloneNode(true). A <template> element's actual markup lives in its .content property, a DocumentFragment (not a direct child of the template in the live DOM tree) ā cloning it with .content.cloneNode(true) produces a fresh, independent fragment ready to append, leaving the original template reusable for the next clone.
Cloning Template Content. What does template.content.cloneNode(true) return?
- āA fresh, independent DocumentFragment copy of the template's content
- āA live reference to the same content, sharing state with the original
- āA plain HTML string requiring innerHTML to use
Parsed Once, Reused Many Times ā A Real Performance Win. Because the browser's HTML parser validates and parses a template's markup exactly once (at page parse time), cloning it repeatedly for a long list is meaningfully faster than repeatedly setting innerHTML with a string, which forces the browser to re-parse the same markup structure on every single iteration.
Why <template> Outperforms innerHTML Concatenation. Why is cloning a <template> repeatedly generally faster than repeatedly appending to innerHTML with a new string for each item?
- āThe template's markup is parsed once; innerHTML concatenation re-parses the growing string on every iteration
- āTemplates always produce smaller HTML file sizes
- āThere's no meaningful performance difference between the two
template Is The Foundation Custom Elements Build On. A <template> is frequently paired with a custom element's connectedCallback (or Declarative Shadow DOM, covered in the reusable-components lesson) as the source markup a component clones into its shadow root ā decoupling a reusable component's internal structure from string-based JS-generated markup entirely.
template And Custom Elements. What role does <template> commonly play inside a custom element's implementation?
- āIt provides the source markup cloned into the component's shadow root
- āIt's entirely unrelated to custom elements
- āIt replaces the need for Shadow DOM entirely
Security: Template Content Is Still Just Trusted, Static Markup. template completely sidesteps innerHTML's dynamic-string XSS risk for its OWN static markup (since it's written directly as trusted HTML in the source), but this protection doesn't extend to data you insert into the cloned copy afterward ā setting .textContent for user data remains correct; setting .innerHTML with untrusted user data inside a cloned template is exactly as unsafe as anywhere else.
template And XSS Safety. Does cloning a <template> automatically make it safe to insert untrusted user-supplied HTML into the clone via innerHTML?
- āNo ā inserting untrusted HTML via innerHTML is still unsafe, regardless of where it's inserted
- āYes, template's inertness protects any content inserted into it afterward
- āOnly unsafe if the template is also used inside Shadow DOM
<template> Mastered. You now know how <template> holds genuinely inert HTML until explicitly cloned, why parse-once-clone-many outperforms repeated innerHTML string building, and how template pairs with custom elements to build reusable, structure-driven components.
Identify A Template For Cloning. A <template>'s content is inert and invisible to normal DOM queries ā scripts find the template itself by id before cloning its content.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Content Cloned From A Template Must Still Meet All Normal Accessibility Requirements
Template inertness has no bearing on accessibility once content is cloned into the live DOM ā headings, labels, and ARIA attributes inside the template's markup still need to be correct, exactly as with any other HTML.
SEO Implications
- 1
Content Left Only Inside An Un-Cloned <template> Is Not Indexed As Page Content
Since template content never renders unless cloned via JavaScript, anything meant to be part of a page's crawlable content must actually be cloned into the live DOM at some point during or shortly after page load, not left permanently inert.
Best Practices
Reach For <template> Cloning Instead Of Repeated innerHTML String Concatenation For Any List Rendering
It avoids repeated re-parsing, sidesteps the XSS risk of string-built markup, and keeps the structural HTML readable and directly inspectable in the DOM/DevTools rather than buried in JS template literals.
Still Use textContent (Not innerHTML) When Populating A Cloned Template With Untrusted Data
Template inertness protects the template's own static markup; it provides no protection for data you actively insert into the clone afterward.
Frequent Bugs
An <img> or <video> inside a <template> unexpectedly appears to never load, even after the page has fully loaded.
This is expected ā resources inside an un-cloned template are never fetched. Clone the template's content into the live DOM for the resource to load.
A long, repeatedly-rendered list (hundreds of items) causes visible jank, built via innerHTML += in a loop.
Switch to cloning a <template> per item instead of string concatenation, avoiding repeated re-parsing of an ever-growing HTML string.
Real-World Examples
Rendering A List From Data With A Cloned Template
A product list rendered by cloning one <template> per item, avoiding both re-parsing overhead and XSS risk from string concatenation.
<template id="product-tpl">
<li class="product"><img><span class="name"></span></li>
</template>
<script>
const tpl = document.getElementById("product-tpl");
products.forEach(p => {
const node = tpl.content.cloneNode(true);
node.querySelector("img").src = p.image;
node.querySelector(".name").textContent = p.name; // safe for untrusted data
list.appendChild(node);
});
</script>