You are shipping three markdown renderers
Bundle analysis on an agentic surface commonly finds duplicated parsers, an over-registered syntax highlighter, or an unintentionally-bundled date library — the fix in each case is deletion, not compression.
The flight chatbot works. It streams, it stops, and you have a profile that says presentation delay is where the time goes. Before you optimise a single render, run a bundle analyzer against it, because the first thing a host team will ask you is not how fast it feels. It is how big it is.
What comes back is a picture of your dependency tree rather than of your code, and three kinds of thing keep turning up in it: a parser you are carrying twice, a syntax highlighter that registered every language it knows, and a utility library that arrived as somebody else’s transitive dependency.
The suspect with a documented mechanism
Two of the three suspects are things you have to go and look for. The third is a documented default, and it is the one to check first because checking it takes ten seconds. From highlight.js’s own usage page: “By default, when you import the main package, all 193 languages will be loaded automatically.”
Nobody asks for 193 languages. You write the import the README shows, you get a working code block, and the cost is invisible until something weighs it. The same page gives the fix in four lines:
import hljs from 'highlight.js/lib/core'
import javascript from 'highlight.js/lib/languages/javascript'
hljs.registerLanguage('javascript', javascript)Note whose page that is. highlight.js is documenting its own default behaviour and its own escape hatch. That is a library describing itself, the same way react.dev is Meta describing React — which does not make it wrong, because the claim is checkable in your own analyzer output in under a minute. Check it anyway. A default this expensive is exactly the kind of thing that changes between major versions, and the number in that sentence is only true for the version the page currently documents.
The one you carry twice
A duplicated parser is rarely something anybody imported twice. It is two of your dependencies each depending on a different major version of the same package, and the resolver keeping both because it cannot prove they are interchangeable. Nothing is broken. Both copies work. You just ship both.
In a markdown-rendering surface the candidates are obvious once you look for them: a renderer, a sanitiser, a plugin and a highlighter can each pull their own copy of a parser or a tokenizer. The analyzer will draw them as two boxes with almost the same name, which is the visual you are scanning for. Then confirm it against the lockfile rather than the picture:
pnpm why marked
pnpm why <the package the analyzer drew twice>That prints every path in the tree that asked for it, which is the difference between “we have two copies” and “this specific dependency is why.” One of those sentences you can act on.
The one that arrived without an import
The third suspect is a utility library nobody in your code imports. It got there through a dependency, and it stays because deleting it means replacing whatever pulled it in. Date formatting is the recurring case, and it is worth pushing on before you accept it, because the platform already formats dates: Intl.DateTimeFormat is in the browser whether or not you ship anything.
The general move is the same in all three cases. You are not looking for a heavy dependency. You are looking for a dependency whose job is already being done by something else in the tree.
Why the fix is deletion and not compression
The reflex when a bundle is too big is to reach for the transfer: turn on Brotli, tune the compression level, argue about the CDN. That helps the network and does nothing to the main thread, and the main thread is what this course is about.
Compression is a transfer encoding. The browser decompresses the response before it parses it, so a dependency that compresses well still costs the main thread every byte of parse and evaluate it always cost. On the mid-tier phone in your unluckiest quartile, that parse is competing with the render loop you already know is saturated.
Deletion is the only operation that removes work rather than moving it. Code splitting, which the guest-in-someone-else’s-bundle lesson is about, moves it — and moving it is genuinely useful, but it is a different claim and it should be made in different words.
Where people get burned
Do not put a percentage on any of this until you have measured it on your own build. There is no published figure for what deleting a duplicated parser saves, because the answer depends entirely on which parser, which version, and what else in your tree was sharing it. A number you read in a blog post about somebody else’s bundle is not evidence about yours, and quoting it to a host team is the fastest way to lose the argument you were about to win.
Check your recall
Answer from memory — no scrolling back.
Retrieval check
The analyzer shows a large dependency that is genuinely used and has no duplicate. Deletion is off the table. What is left?
Check your answer
Three things, in descending order of honesty. First, check whether it has a smaller entry point — the highlight.js core build is exactly this shape, and plenty of libraries ship one without advertising it. Second, check whether it is needed at mount or only on a path most sessions never take, which is a code-splitting question and belongs to the guest-in-someone-else’s-bundle lesson. Third, replace it.
What is not on the list is compressing it harder. The main thread pays for the decompressed bytes either way, and a surface whose problem is main-thread saturation gets nothing from a smaller download.
Hands on
Get a real number for what you cost
Done when: MEASUREMENTS.md has a marginal-payload line naming your surface’s largest three dependencies by name and size from your own analyzer run, plus at least one duplicate or over-registered dependency identified by lockfile path — not by guess.
- Run a bundle analyzer against a production build of the surface. A dev build tells you nothing useful here: it is unminified and carries development-only code that will not ship.
- Write down the three largest entries by name and size, exactly as the analyzer reports them. Do not round and do not convert units for tidiness — you want to be able to re-run this later and compare.
- Check the highlighter specifically. If any import in your tree reads
highlight.jsrather thanhighlight.js/lib/corewith individually registered languages, you are shipping the default that page documents. Note it either way, including “not present” — an absence you checked is worth writing down. - Scan the analyzer output for two boxes with the same package name. For each one, run
pnpm whyagainst that package and record which dependency path is responsible. - Delete or narrow exactly one thing, then re-run the analyzer and record the new size for that entry next to the old one. One change, one measurement. Two changes at once and you have learned nothing about either.
- Bring both numbers into the chat, with the change you made. I will push back if the before and after came from different build modes, or if the finding is a size without a named cause.
What this does not cover
This lesson is about bytes that should not exist. It says nothing about bytes that should exist but should not arrive yet, which is a different and larger part of what you owe a host page. The guest-in-someone-else’s-bundle lesson takes that up: what your marginal cost to a page you did not write actually means, how to state it as a delta rather than as the size of your package, and how lazy keeps the initial chunk near zero until somebody opens the panel.
Nor does it touch what those dependencies cost once they are running. A markdown parser you were right to keep can still be the reason your presentation delay is bad, because it runs on every chunk rather than once. That is the cost-of-parsing-every-token lesson, at the end of this module.
And it stays on this side of the course boundary. How the stream itself is transported, chunked, aborted or resumed belongs to the Streaming interfaces course. This one starts once the bytes are already arriving in the browser and asks only what they cost.
Read this next — primary source
Usagehighlight.js — free; page current at 5 September 2026. The library documenting its own default, which is the default that costs you.
This lesson takes one sentence from it: what happens to your bundle when you import the main package. The full page is worth reading because it also gives you the two escape routes — the core build with individually registered languages, and the CDN builds — and because it shows how ordinary the mistake is. Nobody chose to register 193 languages. They wrote the import from the README and moved on.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.