Marko in June 2026
- Marko 6 is now the default
markoon npm - Native asset management with automatic lazy loading
- A Zed extension and a tree-sitter grammar for Marko
- Synchronous
<await>for values that are not promises
June's headline is a milestone: marko@6 is now tagged latest on npm, making the Tags API the default for new installs. The rest of the month centered on the parts of Marko that developers feel every day: how assets reach the browser, how editors understand .marko files, and how clearly the compiler explains a mistake, alongside a large batch of resumability and interop fixes.
Latest on npm
Marko 6 is now the default. The marko package is tagged latest on npm, so npm install marko brings in the Tags API runtime rather than the older Class API. New projects start on Marko 6 with no extra steps, while existing Marko 5 applications keep working and can move over when ready, as described in Marko 5 Interop.
Assets
Marko now manages native assets directly, with lazy loading built in. The JavaScript for a tag can be split into its own bundle and fetched only after a trigger fires, so a page ships what it needs up front and defers the rest. A below-the-fold widget loads when it scrolls into view, while its server-rendered HTML appears immediately.
import ProductReviews from "<product-reviews>" with { load: "visible.reviews" };
<section class="reviews">
<try>
<ProductReviews sku=input.sku/>
<@placeholder>Loading reviews...</@placeholder>
</try>
</section>import ProductReviews from "<product-reviews>" with { load: "visible.reviews" }
<section.reviews>
<try>
<ProductReviews sku=input.sku/>
<@placeholder>Loading reviews...</@placeholder>
</try>
</section>import ProductReviews from "<product-reviews>" with { load: "visible.reviews" };
section class="reviews"
try
ProductReviews sku=input.sku
@placeholder -- Loading reviews...import ProductReviews from "<product-reviews>" with { load: "visible.reviews" }
section.reviews
try
ProductReviews sku=input.sku
@placeholder -- Loading reviews...The full set of triggers, including render, idle, media, and event-based loading, is covered in Lazy Loading, along with facade tags for making a tag lazy at every call site. The Vite integration understands the new compiler assets API and wires this up automatically, so no configuration is required (introduction, vite#277).
Editor Support
Marko gained a tree-sitter grammar and a dedicated Zed extension, bringing syntax highlighting and structural understanding to Zed, Emacs, and the growing set of tools that rely on tree-sitter, including several LLM-based assistants (tree-sitter, zed). The full list of supported editors lives in Tooling Integrations.
The language server moved forward on several fronts:
- CSS module support, so scoped class names resolve and autocomplete correctly (language-server#527).
- Compiler diagnostics surface as code actions, turning many errors into one-click fixes (language-server#525).
- Richer completions, offering enum attribute values as snippet choices, completing tag names inside shorthand import strings, and cleaning identifiers for component auto-imports (#528, #531, #530).
- A command to view the compiled output of a template, making the compiler's work visible from the editor (language-server#539), plus commands to restart or reload the server without reopening the workspace (language-server#536).
For example, scoped class names from a CSS module now resolve and autocomplete, whether imported:
import * as styles from "./card.module.css";
<div class=styles.card/>import * as styles from "./card.module.css";
div class=styles.cardor defined inline with a <style> tag variable:
<style/styles>
.card { padding: 1rem }
</style>
<div class=styles.card/><style/styles>
.card { padding: 1rem }
</style>
<div class=styles.card/>style/styles
--
.card { padding: 1rem }
--
div class=styles.cardstyle/styles
--
.card { padding: 1rem }
--
div class=styles.cardPerformance
Server rendering got faster by moving more work to compile time. Native attribute values that can be determined statically are now inlined into the compiled output instead of being recomputed on every render, which trims both server render time and the size of the serialized result. In isolated server-render benchmarks attribute rendering runs several times faster, up to about 10x for class-heavy markup, with end-to-end gains scaling with how attribute-heavy a template is (marko#3281).
Consider a tab whose classes depend on state:
<li class=["tab", {
active
}]>${input.label}</li><li class=["tab", { active }]>${input.label}</li>li class=["tab", {
active
}] -- ${input.label}li class=["tab", { active }] -- ${input.label}The class list resolves at build time, so the array and object are never allocated during a render and only the toggle stays dynamic. The class attribute compiles to roughly:
`class=${active ? '"tab active"' : "tab"}`With more toggles, the compiler builds a small table of class strings up front, indexed by the toggles, so each is read once with no concatenation or quote scanning.
Further optimizations collapse redundant signal forwarding, skip serializing the controlled type of static controllable values, and take a faster path for text-only conditionals and placeholder text. The cumulative effect is smaller payloads sent to the browser and less work to resume them. The same principles, applied at the application level, are collected in Optimizing Performance.
Await
The <await> tag renders synchronously when the value it receives is not a promise. An already-resolved value no longer schedules an extra asynchronous tick or a placeholder render, which removes a class of subtle ordering issues and avoids unnecessary work.
<await|cart|=input.cart>${cart.items.length} items</await>await|cart|=input.cart -- ${cart.items.length} itemsWhen input.cart is not actually a promise, for example a cart already loaded on the server and passed straight through, the contents render in the same pass instead of waiting a tick. The same synchronous behavior applies when <await> is nested inside <try> (marko#3269, marko#3271). For the streaming side of asynchronous rendering, see HTML Streaming.
TypeScript
The expression parser ends a type annotation at the function body brace, so typed methods and callbacks in .marko files parse correctly. The same fix landed in the tree-sitter grammar (htmljs-parser#224, tree-sitter#4).
A pair of parser refactors makes parsing roughly 40% faster on real-world templates. States can now process multiple characters at once (htmljs-parser#220), and expression scanning takes a fast path for identifier characters while short-circuiting keyword scans (htmljs-parser#223).
Editors beyond VS Code can integrate Marko's type support through the standalone @marko/ts-plugin package, including typed input inside templates (language-server#505). The plugin scopes itself to Marko projects so it no longer conflicts with Vue (language-server#532). The supported syntax is described in TypeScript Syntax.
Clearer Errors
A recurring theme this month was catching mistakes earlier and explaining them better. The compiler now reports clear errors for object or function literals used as native attribute values, non-function event handlers, invalid dynamic tag values, and invalid <for> by keys. When markup uses a non-Marko attribute idiom, the error includes a suggested correction. A React-style className, for instance, is flagged with class as the intended form:
<div className="card"/>Two new development warnings help as well. One fires when a controlled <select> value matches no option, and another in debug mode when a <for> by key is not a string or number.
Surfacing these problems sooner helps developers, and the coding agents working alongside them, catch mistakes earlier.
Concise Syntax
Comments are now allowed between line attributes in concise mode, which keeps annotated markup readable (htmljs-parser#225).
button
,type="submit"
// the primary form action
,class="primary"
-- SaveResumability
Several resumption bugs were fixed. Resumed conditional branches are now preserved instead of being rebuilt, control-flow branches link correctly through markers on resume, and lazily-loaded branch effects run after insertion. A resumed <for to> range loop now serializes its key so it keeps element identity, and pending scope timing for client renders of <await> is more accurate. For background on how resuming differs from rehydration, see Resumability.
Interop
Mixing the Class API and the Tags API grew more dependable. Class API inline scripts flush correctly across the interop layer, dynamic tags that import Tags API components route through the compatibility layer, and re-rendering an inert Class API child inside a Tags API parent no longer crashes. Self-hydrating split components work across the interop boundary. Projects combining both APIs can follow Marko 5 Interop for the directory and comment conventions that select each compiler. An ambiguous file can opt in explicitly with a comment:
// use tags
<h1>Welcome ${input.name}</h1>Windows
A round of path fixes means the language server resolves files and projects correctly on Windows (language-server#500).
Playground
The playground on markojs.com gained copy and share buttons for results, an embedded console panel, and a format command in the editor (website#148, website#149, website#150). A series of fixes made multi-tab editing feel right: undo history and selection are isolated per tab, editing no longer floods browser history, tab renaming behaves correctly, and the format command no longer appends stray newlines.
Full details for every change are in the release notes of each package on GitHub.
Contributors
Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.