skip to content

A JavaScript tagged template sits inside a function that is called many times. What can the tag function assume about the identity of the strings array it receives on each call, and how do libraries exploit that?

level: seniorimportance: nice to knowfreq 18%

answer

  1. The static half never changes per site
  2. Same object, not merely equal contents
  3. Two identical templates elsewhere are different keys
  4. Frozen, so no bookkeeping on the array
  5. WeakMap keyed by the strings array

basics

~20 s

Each tagged-template call site has one template object, reused on every execution, and it is frozen. A tag can therefore use that array as a stable cache key — typically in a WeakMap — to parse or compile the static text once per site rather than once per call.

solid answer

~50 s

Every time the same tagged template expression executes, the engine hands the tag the *same* frozen array object, not a fresh copy — the template object is created once per call site and cached. Two textually identical templates written at two different places in the source are two different objects. Because the array is a stable, frozen identity tied to source position, it makes an ideal `WeakMap` key: the tag parses, validates or compiles the static chunks on first sight, stores the result keyed by the array, and on later calls only splices the new values into the prepared shape. That is the trick behind template-based rendering and query libraries, where the expensive work depends solely on the static text and the cheap work depends on the values. Being frozen also means a tag cannot mutate the chunks it was given, which keeps the cache honest.

code

javascript · 14 lines
javascript
const seen = new WeakMap();

function tag(strings, ...values) {
  let hits = seen.get(strings) ?? 0;
  seen.set(strings, ++hits);
  return `site seen ${hits} time(s), value ${values[0]}`;
}

const build = (x) => tag`v = ${x}`;
console.log(build(1)); // site seen 1 time(s), value 1
console.log(build(2)); // site seen 2 time(s), value 2

const other = (x) => tag`v = ${x}`;   // identical text, different site
console.log(other(3)); // site seen 1 time(s), value 3

go deeper

for a junior

Know that the static text of a tagged template does not change between calls — only the interpolated values do — and that the array a tag receives cannot be modified.

for a middle

Explain that the template object is created once per call site and frozen, and that identical text at two sites yields two distinct objects.

for a senior

Show the production use: memoise per-site analysis in a WeakMap keyed by the strings array, and explain why a Map would retain call sites and why the win only justifies itself for expensive per-site work.

for a principal

Judge when a template-driven DSL is the right foundation at all — the per-site caching property is what makes it perform, so weigh it against debuggability, tooling support, and the cost of teaching the abstraction.

## The identity guarantee A tagged template's first argument is called the *template object*. The engine builds it once for a given tagged-template expression in the source and reuses it for every subsequent evaluation of that expression: ```js function tag(strings) { return strings; } function build(x) { return tag`v = ${x}`; } build(1) === build(2); // true — same array object both times ``` The values change; the array does not. Conversely, identical text at two different call sites gives two different objects: ```js function a(x) { return tag`v = ${x}`; } function b(x) { return tag`v = ${x}`; } a(1) === b(1); // false ``` The key is the position in the source, not the content. (The object is also per-realm: the same module evaluated in two realms yields distinct template objects.) ## Frozen, too The template object and its `raw` array are frozen. A tag cannot rewrite a chunk, push onto the array, or attach bookkeeping properties to it: ```js function tag(strings) { 'use strict'; strings[0] = 'x'; // TypeError in strict mode; silently ignored in sloppy mode } ``` That is deliberate: since one object is shared across every execution of the site, a mutation would be a cross-call side channel. It also means side-band state must live outside the array — which is exactly what a `WeakMap` is for. ## The caching pattern Put the two facts together and you get the standard optimisation. Anything a tag computes purely from the static text — parsing a markup fragment, working out which placeholder sits in which context, compiling a pattern, building the parameterised statement text — depends only on the call site, so it can be computed once: ```js const compiled = new WeakMap(); function render(strings, ...values) { let plan = compiled.get(strings); if (!plan) { plan = analyse(strings); // expensive: runs once per call site compiled.set(strings, plan); } return plan.fill(values); // cheap: runs every call } ``` A `WeakMap` rather than a `Map` because the key is an object whose lifetime you should not extend: when the code holding that call site becomes unreachable, the cache entry goes with it. Using the array's contents as a string key instead would be both slower and wrong, since two call sites with identical text may legitimately need different cached state. This is precisely why template-based rendering libraries can claim that repeated rendering is cheap: the structural analysis of the markup happens once per template in the source, and each subsequent render only updates the dynamic parts. Query-building tags use it the same way, memoising the statement text so only the parameter array is rebuilt. ## Where it misleads - **Do not treat identity as a content check.** Two sites with equal text are different keys, so a cache keyed this way holds one entry per site, not one per distinct string. That is usually what you want, but it means the cache size scales with source sites, not with data. - **A dynamically constructed call is not a call site.** Calling a tag manually — `tag(['a','b'], v)` — passes whatever array you built, so the identity guarantee does not apply and a naive cache would grow without bound. Guard such entry points, or rely on the `WeakMap` to let the garbage collector clean up. - **The optimisation is real but not free.** The lookup itself costs something on every call; it pays off only when the per-site work is genuinely expensive. Do not add a `WeakMap` to a tag whose body is a `reduce`. ## The takeaway The language gives tags a stable, frozen, per-site handle on the static half of a template. That single property is what turns tagged templates from syntactic sugar into a viable foundation for DSLs: the part of the work that depends on the developer-written text can be done once, and the part that depends on runtime data is all that repeats.

  • Why a WeakMap rather than a Map for that cache?
    Because the key is an object owned by the code, and a strong reference would keep the template object — and anything the cached plan closes over — alive after the module or function holding that call site is gone. A WeakMap lets the entry be collected with its key, which matters most when templates are created by dynamically loaded code.
  • Two identical tagged templates appear in different modules. Does the tag see the same array?
    No. The template object is keyed by the call site in the source, not by the text, so each site gets its own frozen array. A cache keyed on identity therefore stores one entry per site, which is normally the desired behaviour since per-site cached state can legitimately differ.
  • What happens if a tag tries to modify the strings array it receives?
    Nothing useful — the template object and its raw array are frozen, so the write is silently ignored in sloppy mode and throws a TypeError in strict mode, which module and class code always is. That is intentional: the array is shared across every execution of the site, so a mutation would leak between calls.

saying these in an interview costs you the question

  • Assumes a fresh strings array on every call
  • Thinks identical template text shares one object
  • Tries to stash state on the strings array
  • Caches with a Map, keeping call sites alive forever
  • Claims manual tag calls get the same guarantee

context