Overview of Marko 6 Runtime Tags
mainThe runtime-tags package provides the Marko 6 runtime and its translator. It is responsible for core modern Marko features including:
- Fine-grained reactivity
- Serialization
- Resume capabilities
repository·main·Indexed 12 days ago
https://github.com/marko-js/markoA declarative, HTML-based language for building dynamic and reactive user interfaces. It features a built-in reactivity system, components, and a concise syntax alternative to standard HTML. The library includes a compiler and runtime, with support for both single-file and multi-file components, as well as specialized agent skills for AI-assisted coding and migrations from Marko 5 to Marko 6.
The runtime-tags package provides the Marko 6 runtime and its translator. It is responsible for core modern Marko features including:
The following skills are available to assist with Marko development:
marko-best-practices: Use this to write idiomatic Marko 6 when creating or editing .marko files.marko-5-to-6-migration: Use this to migrate Marko 5 (Class API / legacy widgets) applications and libraries to Marko 6, either fully or incrementally using the interop layer.When using dynamic whole-value class or style attributes (e.g., class={variable}), the compiler typically uses heavy normalization logic to handle objects and arrays. If the value is provably a string or a falsy primitive, the runtime can use lightweight _attr_class_str or _attr_style_str helpers that perform a simple setAttribute instead of a recursive object walk.
Optimization Criteria:
"", 0, false, null)._attr, ensuring that class="" still correctly removes the attribute.// Optimized pattern for string-based classes/styles
// Instead of:
// _attr_class(el, 'class', toDelimitedString(value))
// It uses:
// _attr_class_str(el, 'class', value)Instead of a single .marko file, you can use a directory within components/ to group a component with its assets (like CSS or images). Marko supports two directory patterns:
index.marko file.app-header/app-header.marko).ProTip: You can nest a components/ directory inside a component's directory to create 'subcomponents' that are only available to that specific parent component.
<!-- Example of nested subcomponents -->
components/
app-header/
components/
navigation.marko
app-header.markoIn Marko, _hoist reads (used in <script> blocks and event handlers) can be optimized by replacing the generator-based traverse with a plain recursive resolve. This reduces latency by avoiding generator allocation for single-value path resolutions.
Performance Impact:
Implementation Constraints:
arguments instead of a rest parameter, as it deoptimizes the function (e.g., 229 ns/op).for (const fn of setHtml)).// Optimization target in packages/runtime-tags/src/dom/signals.ts
// Replace generator allocation with recursive resolve for single-value paths
traverse(scope, path, args).next().valueWhen using a dynamic tag for content passthrough (e.g., <${expr}/>), the compiler typically builds a full _dynamic_tag signal. This signal pulls in several dependencies (_attrs, _attrs_content, _attrs_script, and controllableRenders) into the shared chunk.
A slimmer _dynamic_tag_content exists for direct exports. To allow bundlers to tree-shake the heavier _dynamic_tag signal, the section check in isDirectContentBinding should be relaxed. This allows idiomatic patterns like:
<if=input.aside>
<${input.aside.content}/>
</if>Even though the read now lives in the <if> body section (different from the input's section), it remains a parameter-less, input-less passthrough that can safely use the slim _dynamic_tag_content export.
The Marko 6 compiler transforms .marko templates into a dependency graph that lowers to either streaming HTML with resume state or fine-grained DOM code. To work with the compiler's output or extend it, you must use its specific terminology:
<script>, <lifecycle>, or handler attachments. Resumable effects use _script to register per scope.state (non-parameter) or param (input/body-parameter).The visit-retention fallback in render.m (used for re-processing branch visits when a lazily loaded module calls enableBranches()) is now gated behind a module-level flag. This flag is set in dom/load.ts only when a template uses the import ... with { load: ... } syntax.
Impact:
visits array during every resume.The getNodeContentType function currently returns ContentType.Dynamic for all <show> tags. This causes adjacent placeholders to be classified as SiblingText.Before, adding unnecessary runtime boundaries (e.g., <!> in client templates).
Optimization:
If the <show> display attribute is statically truthy (e.g., <show=true>), getNodeContentType should return the body's startType/endType directly. This allows the compiler to treat the body as inline content rather than a dynamic boundary, reducing the complexity of the client template.
<div><show=true><b/></show>${input.x}</div>
<!-- Optimized: treats <b> as inline rather than a dynamic boundary -->If Marko cannot find a component via the standard discovery process, it will check if the tag name matches an in-scope variable. This allows you to dynamically determine which tag to render.
import SomeTag from "./somewhere.marko"
$ const MyTag = input.href ? "a" : "button";
<SomeTag/>
<MyTag/>When using a <for> loop without a by attribute (an unkeyed loop), the runtime can use a specialized index-diff loop instead of the standard loop() factory. The standard factory includes logic for key-based reconciliation (Map building, common-suffix scanning, and LIS move planning) which is unnecessary for index-based loops and prevents effective tree-shaking.
By using the specialized variant, you reduce the bundle size and improve update performance because the runtime avoids the overhead of Map construction and the LIS (Longest Increasing Subsequence) pass during every render.
<!-- Standard unkeyed loop: uses index-diff helper internally -->
<for|item| of=list>
<div>{item}</div>
</for>
<!-- Standard keyed loop: uses full loop() factory with reconciliation -->
<for|item| of=list by=item.id>
<div>{item.name}</div>
</for>In optimized output, the rendererKey (which joins a content ID and an owner scope ID) can be shortened.
Current Format: "a0 1" (where "a0" is the ID and "1" is the owner, separated by a space).
Optimized Format: "a01" (concatenated).
Why it works:
In optimized builds, encodeTemplateId generates IDs using n % 53, which covers a-z, A-Z, and $, but never digits. Therefore, ownerScopeId + id is unambiguous.
Note: Debug IDs use relative paths and can start with digits, so they must retain the space separator to avoid ambiguity.