We Measured the LLM Token Cost of 4 Markup Formats: TSX Costs 94% More Than Pug

Wait 5 sec.

Part 3 of “The Tokenomics of Web Development” — a controlled comparison of 23 web technologies, 115 idiomatic snippets, one tokenizer.Nobody picks a markup format. It arrives with the framework you already chose, and after that you write it for years without a second thought about what it costs.That was fine while the only reader was a person. Now paste a component into Claude or Cursor and ask for one more field, one different breakpoint. Look at what you actually sent: a wrapper, a tree of tags, a dozen class attributes, and somewhere in there, the two lines you cared about. On most frontend requests, the bulk of what you send is presentation, not logic, and it is the layer nobody has priced.In Part 1 five frontend frameworks came out 38% apart. In Part 2 five languages came out 63% apart. Markup is worse than both: 94% between the cheapest and the most expensive way to describe the same rendered pixels. It is the widest spread anywhere in this study.The premium everyone blames for it turned out to cost exactly zero.Who’s in focus and whyFour formats, five samples each, static structure with no behavior at all: a heading with a paragraph, a three-item list, a login form, a card, and a semantic article with header, two sections and footer. Same tags, same classes, same text, same nesting in every format. The only variable is the syntax you type.HTML is the baseline. Angle brackets, closing tags, class. Everything else in the group is a proposal to write it differently.Pug is the terse end of the scale: indentation instead of closing tags, p.meta instead of . It’s a preprocessor from the Node.js era, and it’s much rarer in modern stacks than it was a decade ago. I included it because it removes both of HTML’s structural costs at once, which makes it a useful floor even if you never adopt it.JSX is markup living inside a JavaScript function. Component wrapper, fragments, className, camelCased attributes. This is what most of the industry writes now.TSX is JSX plus a typed component signature. In Part 2 the type annotations on plain functions were the single most expensive thing measured. The question here is what typing costs when there’s almost nothing to type.Counting is tiktoken with cl100k_base, the encoding used by GPT-4 and GPT-3.5. Every vendor tokenizes differently, and one of those differences is now large enough that a line I wrote in both previous parts needs correcting. I said there that Anthropic’s tokenizer differs slightly. It no longer does: Anthropic documents that Claude 4.7 and later use a newer tokenizer producing roughly 30% more tokens for the same text. So the counts below are not what a current Claude model sees. On those, every count here runs about 30% low. Across months of running these benchmarks I’ve found the rankings hold across BPE-family tokenizers; the absolute numbers move, sometimes a lot.Results analysis1. Leaderboard: the one group with no upsetsFrameworks and languages both had samples where the ranking flipped: SolidJS took the data-fetching component off Svelte, CoffeeScript took fizzbuzz off ZeroLang. Markup has none of that. Pug wins all five samples, HTML is second on all five, TSX is last on all five.That consistency matters more than the size of the gap. A ranking that survives all five samples is more likely to reflect the syntax than the examples I happened to choose.2. Two costs that behave completely differentlyLook at the same table as ratios against Pug:HTML’s overhead stays roughly proportional as the file grows. It’s a per-tag cost, and it never goes away: a bigger tree just means more of it.The JS-based formats behave differently. On a two-line paragraph TSX is three times Pug. By the article sample it has settled to about 1.9×. That shape is a fixed cost being amortized, and the number it settles at tells you the fixed part is large. On the smallest file, most of what you send isn’t markup at all.3. Fewest characters and best compression, at the same timeTokens-per-char is a compression score: how well the tokenizer digests the text. Lower is better.HTML has the worst score in the group. Angle brackets and closing tags are punctuation that merges into nothing useful. JSX and TSX score better despite being far longer, because most of what they add is JavaScript, and the tokenizer ate JavaScript by the terabyte in training.Pug wins on both axes at once: least text and best compression. In Parts 1 and 2 the two axes pulled in opposite directions: Svelte had the worst compression in its group and won anyway, and JavaScript and TypeScript compressed best and still lost on volume. Here the obvious answer happens to be the right one.4. Where HTML’s 61% premium over Pug comes fromTwo mechanisms, both mechanical.Closing tags. The article sample has 15 of them, costing 48 tokens, 31% of that file’s entire budget. Pug emits zero. In these files, indentation carries the same information at no measurable token cost, because the tokenizer folds leading whitespace into the token next to it instead of charging for it separately.Attribute syntax.The text content accounts for 10 tokens in both. All six tokens of difference are structural: class="…" collapses into .meta, and disappears.Note why is expensive: it costs 3 tokens because the tag name between the brackets varies, so the tokenizer has nothing stable to merge. That detail looks minor here. It turns out to be the mechanism behind almost every result in this series, and Part 4 is where it stops being minor.5. The className mythThe standard explanation for JSX being expensive, the one in every discussion of React verbosity, is that JSX pays for className instead of class, for camelCase attributes, for htmlFor. It's also wrong, and one line of Python says why:className is a single token. So is class. The tokenizer learned className as one unit because React code saturates the training corpus. Across all twelve class attributes in these samples, class costs 53 tokens and className costs 53 tokens. Identical, to the token.So I stripped the component wrapper off each JSX file, dedented the body, and counted again:Read the bolded column. On list, form and card, the JSX body is token-for-token identical to the HTML. 36 = 36. 76 = 76. 88 = 88. On those three, the markup itself costs nothing extra. The other two carry a fragment and a Prettier reflow, both accounted for below.JSX’s entire 63-token premium over HTML decomposes into:•53 tokens of export const Name = () => ( … );, the component wrapper, 11 tokens per file (9 on the paragraph);•7 tokens for the fragment … on the paragraph sample, which adds the root element required by JSX but not by HTML;•3 tokens on the article sample, where Prettier reflowed an inline across lines.TSX over JSX is cleaner still. : React.FC is exactly 3 tokens, on exactly 5 files. 451 − 436 = 15 = 3 × 5. Nothing else differs at all. That’s a striking contrast with Part 2, where TypeScript’s annotations added 31% to plain JavaScript. Types are expensive when there’s logic to annotate. On a component that takes no props, the type system costs three tokens.6. What this reframesThe toll is fixed: about 11 tokens per component boundary, plus whatever fragments your tree forces. className doesn’t explain any of it.Which inverts the usual advice. If the tax were per attribute, you’d reduce it by writing fewer classes, and people do try that. Since it’s per component, you reduce it by writing fewer, larger components.A 200-component codebase carries about 2,200 tokens of pure export const X = () => ( ceremony before a single element is described. Every component you extract because a file felt long costs 11 tokens every time that file enters a context window.What we overpay for markupMultipliers against the winner: Pug ×1.000 · HTML ×1.608 · JSX ×1.879 · TSX ×1.944.Rather than a model-by-model table that expires the next time a vendor changes a price, here is what that UI costs when the Pug version comes to a million input tokens. Find your rate, read across.For scale, on prices pulled from OpenRouter on 22 August 2026: Claude Haiku 4.5 sits at $1 per million input tokens; Claude Sonnet 5, GPT-5.6 Sol, Gemini 3.1 Pro and Grok 4.6 all sit at $2; Claude Opus 5 at $5, and Anthropic’s Fable 5 at $10. The cheap tier runs well below the table — GPT-5.6 Luna is $0.20. Prices move monthly; re-pull yours before budgeting anything.Input only. That’s the right lens for part of your traffic and the wrong one for the rest. Paste a file and ask for one change, and input is nearly the whole bill. Ask for the component to be written from scratch and it inverts: you send a sentence and get back a file. A full agentic workflow lands in between: a study of multi-agent development traces (Salim et al., 2026) found input tokens averaged 53.9% of consumption once output and reasoning are counted.The ratios survive the inversion, because they describe the syntax, not the direction of travel. A component the model writes in TSX costs the same 94% more than the same component in Pug. And output is priced around five times higher than input on current price cards, so on generation-heavy work this gap costs more, not less.Two things this table understates. It’s a single pass, and nothing is a single pass: the same study found the iterative review stage alone accounts for 59.4% of tokens, and every one of those cycles re-sends the file. And on current Claude models the newer tokenizer adds roughly 30% on top of every figure here.And one thing the table can’t show, which cuts against my own argument. Frontier input pricing has dropped roughly threefold in a year: Opus 5 costs $5 per million where Opus 4 cost $15. Run this same ratio against last year’s prices and the gap between Pug and TSX was $14.16 on the top model. Today it’s $4.72. If you were planning to justify a migration on cost alone, the numbers stopped supporting that argument some time ago.What hasn’t deflated is context pressure. A markup layer 94% larger means 94% more of the window spent on structure instead of logic, and that gets paid in output quality rather than dollars. That’s now the better reason to care.Treat the ratios as the finding and the dollars as illustration. Your bill depends on your traffic, not on my samples.So should you drop JSX? NoSame answer as Part 2, for the same reason. Nobody is rewriting a React codebase in Pug to save tokens, and I wouldn’t advise it if you asked.There’s also an honest objection to the whole exercise, and it applies to Pug harder than to anything else measured so far: fewer tokens is not automatically cheaper AI coding. Real cost lives in re-runs and debugging as much as in generation. Pug has a fraction of JSX’s representation in training data, so a model is more likely to fumble it. If an agent fails twice on Pug and lands JSX first try, JSX was cheaper. I did not measure hallucination rate, and I’m not going to pretend the token count settles it.What the token count does give you is the floor. Every review cycle re-sends the file, and every retry pays for it again. So the useful moves are the ones that don’t require changing your stack:•Stop extracting components for tidiness. Splitting a file has a price now, and you know its exact denomination. This isn’t an argument for 800-line components. It’s an argument against a 12-line one that exists because the parent looked crowded.•Send the body, not the file. When you paste a component into a chat to change one attribute, the wrapper and the imports are pure overhead in a request that only needed the tree.•On content-heavy surfaces, consider a compiled authoring layer. Landing pages, docs, email templates: markup-dominant work where the tree is the whole job. Pug cuts that surface by 38% relative to HTML, and compiles to the HTML you were shipping anyway. Weigh the ecosystem friction honestly, but know what you’re paying to avoid it.•Don’t optimize attributes. Whatever className costs you, it isn’t tokens.Check it on your own code in three linesTake any component you send to a model regularly. Count it, remove the component wrapper, dedent the remaining markup, and count again. In the samples here that means deleting the first and last line: the export and the closing paren.import tiktoken, sysenc = tiktoken.get_encoding("cl100k_base")print(len(enc.encode(open(sys.argv[1]).read())), "tokens")The conclusionWe spent twenty years making code more explicit. Closing tags so the structure is unambiguous, annotations so the intent is legible, small components so a colleague can hold one in their head. All of it was right for a human reader, and all of it was paid for in text. Three parts of this series now say the same thing: what you write out has a price, and what the compiler works out for itself is free.className showed the other half of that. The cost was never in the attribute. It was in the wrapper around it, which nobody thinks of as markup, because it’s a function.The tooling for that is missing. We’ve had bundle analyzers for years: they tell you what every import costs the browser, and CI fails when a file gets too heavy. Nothing does that for the model, which reads the same source and never gets counted. No linter counts the wrapper. No build step offers to send the tree without the ceremony around it, though it already knows the difference. That’s the tool I expect next: a token budget in the build, sitting next to the size budget.Languages and frameworks have been priced before. Markup hadn’t been, until this.Next upPart 4 takes the whole styling layer: CSS, Stylus, Sass, SCSS and Less, plus Tailwind, UnoCSS, twind and Master CSS. Braces and semicolons turn out to be nearly free. The expensive character in CSS is one nobody suspects. It sits in every declaration you have ever written, and Sass is the format that kept it.The utility half has a stranger result. One of those four frameworks writes the shortest source code in the group, character for character, and still loses on tokens to the framework it set out to improve on.The full methodology, the sample set, and the counting script publish with the final part of the series. Every table here is reproducible on your own codebase in the meantime.