About 40 seconds. Over 46,000 tokens. For *one single* dashboard. That was the initial version of my A2UI solution — and nobody wants to see something like that in production, neither the users staring at an empty screen nor the person paying the provider's bill at the end of the month.
Today, the same solution takes **around 0.1 seconds** and **just over 1,500 tokens**. That's a speedup of **300×** and a token reduction of **30×**. The really interesting part, however, isn't the number itself, but the surprisingly simple lever behind it — and the lesson that can be drawn from it for any LLM-driven UI generation.
In a previous article, I demonstrated how to generatively create entire dashboards with A2UI. The result looked like this:
In this article, I show how I drastically accelerated exactly this solution — and what consequences this optimization entails. Because every performance gain has its price.
The Problem: Why A2UI Generation Was Slow and Expensive
To put things in context:A2UI is a protocol that allows user interfaces — here, a dashboard — to be described in a way that an LLM can generate. The model doesn't produce finished HTML, but rather a structured description of the interface that the client then renders. Precisely this generation was the bottleneck in the first version.
To generate A2UI, one first needs a correspondingly long prompt with good examples — classic one-shot or few-shot prompting. Since LLMs handle such examples excellently, I didn't even copy the complete specification or the JSON schema of A2UI into the prompt. Nevertheless, the whole thing was very extensive: in the example shown at the start, we ended up with **over 43,000 input tokens**.
In addition, generating the A2UI markup based on this description took correspondingly long. The model has to produce an extensive structure token by token — and that's exactly what's expensive.
Beyond that, the model constantly has to issue function calls to fetch the data for the individual tiles. Each of these calls costs additional reasoning and thus time and tokens.
And finally, perhaps the most unpleasant problem: when generating A2UI, the model can get confused and produce faulty markup. The basic design of A2UI — which deliberately avoids deep nesting, for instance — noticeably helps LLMs. But errors can't be entirely ruled out, and every error forces further correction loops. Especially weaker (and thus cheaper) models are affected by this.
That's what a complete run looked like in the original variant:
{
"toolName": "renderDashboard",
"toolCallId": "call_2mO2fjJD4ZpGHna4yXsW8QG0",
"toolInput": {
"tiles": [
{
"type": "boardingPasses",
"count": 2
},
{
"type": "bookedFlightsList",
"showCheckInButton": true
},
{
"type": "flightSearch",
"defaultFrom": "Graz",
"defaultTo": "Hamburg"
},
{
"type": "rentalCars"
},
{
"type": "hotels"
},
{
"type": "weatherList"
}
]
}
}
Clearly visible: the model first collects data via several tool calls and then generates the complete A2UI markup in a single step lasting over 30 seconds. This one step dominates the entire runtime.
The Solution: an Application-Specific DSL Instead of A2UI Markup
The idea is simple: a small, application-specificDSL (Domain-Specific Language) — i.e., a deliberately minimal description language tailored to exactly one purpose — that can describe exactly what should be generated: a dashboard with specific content fitting the application. In our case, this DSL looks like this:
The model is now only instructed to translate the user's request into this DSL. That's all it does. Since this leads to a very short result, the conversion is fast, less error-prone, and even weaker models handle it without any problems.
There's another, often underestimated advantage: the application is thus forced more strongly into a defined framework. The model can no longer do things that the application design hasn't anticipated. The result becomes significantly more controllable — despite the fundamentally non-deterministic behavior of an LLM.
This DSL is subsequently used for two purposes:
- Conversion to A2UI — deterministic, in code, without any model.
- Fetching the necessary data — also without further reasoning by the model.
Consequences: Pros and Cons of the DSL Approach
As always in software architecture, there's no such thing as "free." Let's note the advantages and disadvantages. Pros:- Significantly more performant.
- Significantly lower token consumption — and thus significantly lower costs.
- Behavior becomes more controllable.
- Easier for weaker (cheaper) models.
- The client doesn't need to know all possible display options in advance.
New: Agentic UI with Angular
If you want to embed such performance and architecture decisions around A2UI not just in isolated cases, but cleanly into larger Angular applications: in my book Agentic UI with Angular, I go into exactly these patterns and trade-offs — including DSLs, caching, and open standards like AG-UI, A2UI, and MCP — in detail.
Even More Performance: Caching
As long as the description remains the same, the generated markup can also remain the same. As a rule, that's even desirable: a re-interpretation that leads to slight deviations would rather confuse users. Only the data needs to be fetched anew with each call. Here, a fundamental feature of A2UI plays into our hands: you can **separate structure and data**. AnupdateComponents message can be used to communicate the desired structure:
{
"version": "v0.9",
"updateDataModel": {
"surfaceId": "dash-bf0140eb-f2e4-4c53-a74b-56e9445cc7dc",
"path": "/t0",
"value": {
"tickets": [
{
"ticketId": 1,
"from": "Graz",
"to": "Hamburg",
"date": "2026-06-06",
"delay": 0
},
{
"ticketId": 2,
"from": "Hamburg",
"to": "Graz",
"date": "2026-06-06",
"delay": 0
}
]
}
}
}
This can contain data-binding expressions that refer to a data model. In the example shown, the property path points to an array /t0/tickets, whose entries have properties like ticketId, from, to, etc.
This data can subsequently be sent to the client with its own messages:
This separation is the basis for our caching: with the same description, the structure is deterministically reproducible; mainly the data transmitted via updateDataModel needs to be re-determined. In our implementation, we cache the compact DSL. From it, the entire — and usually relatively extensive — updateComponents message is generated deterministically in application code for subsequent calls; the model is no longer needed for that.
In our implementation, a hash of the textual description supplied by the user serves as the key. In practice, the respective dashboard is often stored as a dataset in a design mode of the application — and exactly the ID of this dataset, which represents the dashboard, then serves as the cache key.
The Result: 300× Faster, 30× Fewer Tokens
The DSL enables drastic speedups. Let's compare the two worlds directly again:
The model only creates the DSL from the request — no longer the complete A2UI markup. This results in a nice side effect: the necessary tool calls for data fetching can also be derived from the DSL. The model therefore no longer has to worry about this itself, which leads to additional, smaller performance improvements — simply because less reasoning is required for individual tool calls.
The second major improvement comes from caching: here, the model is no longer needed at all for identical dashboards. The DSL comes directly from the cache; the structure described with A2UI is generated from it deterministically in application code, and only the current data is freshly loaded.
And with that, we're back at the numbers from the beginning: from **around 40 seconds** and **over 46,000 tokens** to **around 0.1 seconds** and **just over 1,500 tokens** — a factor of 300 in speed, a factor of 30 in tokens. Important: the token reduction by a factor of 30 already results from the DSL alone, because the model only has to generate this compact description instead of the complete A2UI markup. Caching builds on top of that: on a cache hit, the model isn't called at all — so no tokens are incurred whatsoever.
Bottom Line: Let the LLM Handle Intent, Let Code Handle Structure
The core idea here is a deliberate reassignment of duties. Instead of asking the LLM to produce extensive A2UI markup, it now only converts the request into a terse, domain-specific DSL. All the demanding work — building out the structure and retrieving the data — is then carried out by deterministic application code.
The numbers tell the story: 300 times faster, 30 times fewer tokens, and significantly more predictable behavior, with smaller and cheaper models benefiting the most. When caching is added, the model can be taken off the hot path entirely for recurring dashboards.
The trade-off is a reduction in generative freedom — the app can only render what the DSL supports. For most real-world business applications, that is not a loss but a win, since predictability matters more than unbounded dynamism. The overarching takeaway: let the LLM do what it does best — grasp intent — and leave the construction of reliable structures to your code.
Interested in Production-Ready Agentic UI Architectures?
In my workshop we take a deep dive into AG-UI, A2UI, MCP Apps, HITL patterns, and modern Angular architectures for real-world agentic systems.
FAQ
Why is direct A2UI generation with an LLM slow and costly?
The model has to generate a large structure token by token from a long few-shot prompt (over 43,000 input tokens in the example), while also making several tool calls to fetch data. That single generation step dominates the runtime and can easily produce faulty markup, which then forces correction loops.
How does a domain-specific DSL speed up generation?
The LLM only needs to translate the request into a compact DSL instead of full A2UI markup. The output is short, fast, and much less error-prone. The expensive work — converting to A2UI and fetching data — is subsequently handled by deterministic application code, with no model involved.
What additional benefit does caching provide?
A2UI allows separating structure (updateComponents) from data (updateDataModel). In our implementation, the compact DSL is cached; the typically large structure is then generated deterministically from it in application code, and for subsequent calls only the small data messages with fresh data need to be built. For identical dashboards, the model is not needed at all.
What is the downside of the DSL approach?
Dynamism is limited to what the DSL explicitly covers. Anything it does not represent is simply not possible. You are effectively swapping some generative flexibility for speed, cost, and control — a good trade for most business applications.
