Skip to main content

Command Palette

Search for a command to run...

There's a hidden function call inside every html template you write.

Most JS and TypeScript developers have never written a tagged template literal, yet they use them through Lit, styled components, and WebJs every day. Here's how they work, the stable identity trick that makes them fast, and why WebJs is built on one.

Updated
7 min readView as Markdown
There's a hidden function call inside every html template you write.
V
Product Engineering Consultant | AI enthusiast | Worked for Salesforce, Harness, Aetna, PWC, Wolters Kluwer, Litera, BASF Chemicals, Abu Dhabi Government, & multiple startups.

You use template literals every single day. The backtick string with ${} holes in it:

const name = "Vivek";
const greeting = `Hi ${name}, you have ${3} messages`;
// "Hi Vivek, you have 3 messages"

Nothing new there. But there's a second form of this feature that most JavaScript and TypeScript developers have never used on purpose, even though they lean on it constantly through the libraries they use. Kyle Simpson calls it out in You Don't Know JS and then most of us close the book and forget about it. It's called a tagged template literal, and once you see it, you can't unsee it. It's the thing that makes Lit, styled components, GraphQL's gql, and (the reason I'm writing this) my own framework WebJs tick.

So let me show you.

You can put a function name in front of the backtick

That's the whole trick. When you stick an identifier right before a template literal, you're not building a string anymore. You're calling that identifier as a function, and JavaScript hands it the pieces of the template pre-split for you:

function tag(strings, ...values) {
  console.log(strings); // ["Hi ", ", you have ", " messages"]
  console.log(values);  // ["Vivek", 3]
  return "I can return anything I want";
}

const name = "Vivek";
const out = tag`Hi ${name}, you have ${3} messages`;

Look at what tag receives. The first argument is an array of the static string parts, the bits you literally typed between the holes. Every argument after that is one dynamic value, the result of evaluating each ${}. They interleave perfectly. There's always exactly one more string than there are values, because the literal always starts and ends with static text (even if that text is an empty string).

And here's the part that surprises people the first time: the return value doesn't have to be a string. A normal template literal always produces a string. A tagged one produces whatever the tag function decides to return. A number, an object, a DOM node, a description of some UI. The tag is in full control.

A tiny example you'd actually use

Say you're building HTML from user input and you want to escape it so nobody can inject a <script> tag. Most people reach for a library. You don't need one. You need a six-line tag:

const escapeHtml = (s) =>
  String(s).replace(/[&<>"]/g, (c) =>
    ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));

function safe(strings, ...values) {
  return strings.reduce(
    (out, str, i) => out + str + (i < values.length ? escapeHtml(values[i]) : ""),
    ""
  );
}

const userInput = '<script>alert(1)</script>';
safe`<p>Comment: ${userInput}</p>`;
// "<p>Comment: &lt;script&gt;alert(1)&lt;/script&gt;</p>"

Notice what happened. The static <p>Comment: and </p> passed straight through, untouched, because you wrote those and they're trusted. Only the interpolated value got escaped, because that's the part that came from outside. The tag can treat the static skeleton and the dynamic holes as two completely different kinds of thing. That separation is the superpower, and it's the reason serious rendering libraries are built on this feature instead of on plain string concatenation. When you glue everything into one string with +, you've thrown away the boundary between "stuff I wrote" and "stuff that came from a variable". A tag keeps that boundary.

The part nobody tells you: the strings array is the same array every time

This is the bit that took me a while to appreciate, and it's the reason tagged templates are fast enough to build a whole framework on.

The array of static strings that JavaScript passes to your tag is not freshly created on every call. The engine builds it once per location in your source code, and hands you the exact same array object every single time that line runs. Same identity. You can prove it to yourself:

let captured;
function grab(strings) {
  if (captured) console.log(captured === strings); // strict identity check
  captured = strings;
}

for (let i = 0; i < 3; i++) {
  grab`static ${i} parts`;
}
// logs: true, true

Three trips through the loop. The ${i} value is different each time (0, 1, 2), so the values differ. But the strings array is byte-for-byte the same object, === and all. The engine froze it and cached it against that spot in your file the first time it saw it.

Sit with that for a second, because it's genuinely useful knowledge. It means the static shape of your template is a stable, reusable key. You can hang a WeakMap off it. The first time a given template runs, you do the expensive work of parsing that static skeleton into something real. Every time after that, you look it up by the array's identity, find your cached result instantly, and only deal with the values that actually changed. The garbage collector cleans up the cache entry for free when the template goes away, because it's a WeakMap.

That is the entire performance story behind template-based rendering, and it falls out of a language feature, not a build tool.

One more uncommon detail: .raw

While we're here, the strings array has a hidden property most people never touch. strings.raw gives you the escape sequences un-processed, exactly as typed:

function show(strings) {
  console.log(strings[0]);      // "line\nbreak"  -> a real newline
  console.log(strings.raw[0]);  // "line\\nbreak" -> literal backslash-n
}
show`line\nbreak`;

The built-in String.raw tag is just a tag that uses .raw. It's why String.raw`C:\Users\me` keeps every backslash instead of turning \U into a broken escape. Handy for regexes and Windows paths, and now you know it's not magic, it's a tag function you could have written yourself.

So what does this have to do with WebJs?

Everything, actually. When you write a component in WebJs, you write markup with the html tag:

render() {
  return html`<p class=${this.tone}>${this.message}</p>`;
}

That html is a tag function, and here is the whole thing, straight from the framework source. It's not a metaphor, this is the real code that runs:

export function html(strings, ...values) {
  return { _$webjs: 'template', strings, values };
}

That's it. It doesn't touch the DOM. It doesn't render anything. It captures the static skeleton and the dynamic values and hands them back as a plain object. All the actual rendering happens later, when a renderer (on the server for the first paint, or in the browser for updates) consumes that object.

And the renderer does exactly the trick from a few paragraphs ago. It keeps a WeakMap keyed on the strings array. The first time your component renders, WebJs parses the static HTML once into a real <template> element and remembers where every hole is. On every re-render after that, it finds that parsed template by the identity of the strings array (which, remember, never changes for a given line of code) and only updates the values that moved. There is no virtual DOM. There is no diffing a tree of JavaScript objects against another tree. There's a cached skeleton and a handful of holes, sitting on top of a caching key the language handed the framework for free.

When I was building this, that was the quiet realization that kept repeating: I didn't need to invent a rendering primitive. JavaScript already shipped one in 2015 and most of the ecosystem walked right past it. The html tag in WebJs is maybe the smallest, most boring function in the codebase, and it's load-bearing for the entire component system.

That's kind of the whole thesis of WebJs, honestly. It's a full-stack framework, but the closer you look at it, the more it turns out to be JavaScript you already know, just arranged deliberately. The less of the framework there is to learn, the better it teaches an AI agent, and the more your own hard-won language knowledge keeps paying off instead of getting overridden. Tagged template literals are one small proof of that. There are more.

If any of this made you want to go type tag`hi ${1}` into a console and poke at the strings array, that was the whole goal. And if you want to see what a framework looks like when it's built on the language instead of around it, WebJs is live at webjs.dev. A star on GitHub is more than welcome : )