Vue 3.4 Ditches the Token Array for a Streaming State-Machine Parser
Streaming parse-and-emit removes the memory pressure of a full token array on large templates and cuts one pass from the compilation pipeline. For anyone maintaining a Vue SFC-heavy codebase or building tooling that touches the compiler, the single-pass architecture simplifies reasoning about parse performance and memory profiles.
Before Vue 3.4, template compilation ran a Scanner to build a full token array, then a Parser consumed it to produce the AST — a two-pass design that forced long templates to hold every token in memory. The new Tokenizer is a character-by-character state machine that recognizes text, tags, interpolations, and attributes inline, firing callbacks that construct AST nodes immediately. No intermediate token array exists.
The change is visible in `baseParse`, which now instantiates a `Tokenizer` with an element stack and callbacks like `onText`. As the state machine walks the input, hitting `<` triggers a text-segment callback and a state transition into tag parsing; ordinary characters pass through with zero overhead. After the loop, a `cleanup` step flushes any remaining text fragment, and `condenseWhitespace` normalizes whitespace — deleting pure-whitespace nodes between elements or at boundaries, compressing internal runs to a single space, and inside `<pre>` only unifying Windows line endings.
Position tracking (`loc`) uses a pre-collected array of newline indices. For arrays longer than 100 entries, a binary search maps a character offset to a line:column pair; shorter arrays use a reverse linear scan. This is what powers precise error messages in Vue’s template compiler.
The old Scanner→Parser pipeline was a textbook two-pass compiler frontend; Vue’s move to a callback-driven state machine mirrors how many production parsers (e.g., SAX-style XML parsers) avoid building intermediate representations to stay memory-bounded.
Recording newline indices and deferring line:column calculation to `getPos` is a deliberate trade-off: it adds a small per-character branch in the hot loop but keeps position lookups O(log n) and avoids storing per-character metadata.
The whitespace condensing rules are surprisingly nuanced — pure-whitespace nodes between elements are deleted only if they contain a newline, otherwise collapsed to a single space. This preserves intentional inline spacing while stripping formatting whitespace, a heuristic that has caused subtle rendering differences across frameworks.