Marko

repository·main·Indexed 12 days ago

https://github.com/marko-js/marko

A 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.

Tokens
104.6K
Snippets
385
Records
543
Agent score
92%

What's inside Marko

  1. Available Marko agent skills

    main

    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.
  2. Optimize class and style attribute writes

    main

    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:

    • The value is a string or a falsy primitive ("", 0, false, null).
    • The optimization is applied when the expression is a simple identifier or a template literal that resolves to a string.
    • Note: This does not retarget the call at _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)
  3. Organize component directories

    main

    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:

    1. Index Pattern: A directory containing an index.marko file.
    2. Name-Match Pattern: A directory named after the component containing a file with the same name (e.g., 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.marko
  4. Optimize hoist reads in `<script>` and event handlers

    main

    In 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:

    • Single-segment path: ~137.6 ns/op $\rightarrow$ 21.0 ns/op (~6.5x speedup).
    • Note: This affects handler latency, not render latency.

    Implementation Constraints:

    • Do not use arguments instead of a rest parameter, as it deoptimizes the function (e.g., 229 ns/op).
    • The generator must be preserved for cases where the iterator form is required (e.g., 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().value
  5. Use slim `_dynamic_tag_content` for nested-section passthroughs

    main

    When 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.

  6. Understand the Marko 6 Compiler Model

    main

    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:

    • Section: The fundamental unit of analysis and codegen (e.g., a template program or a non-inlined tag body). Sections own bindings and signals.
    • Binding: A compiler record for a template value or property. It tracks reads, assignments, aliases, and sources. This is distinct from a standard Babel lexical binding.
    • Signal: A collection of client-side work for a section, keyed by setup, a binding, or an intersection. It handles the actual runtime updates.
    • Effect: Client work queued after renders, such as <script>, <lifecycle>, or handler attachments. Resumable effects use _script to register per scope.
    • Intersection: A set of bindings that coordinate work; the shared work waits for every member in the current render generation.
    • Sources: The roots that make a binding relevant to the browser, categorized as state (non-parameter) or param (input/body-parameter).
  7. Performance optimization: Gated lazy-resume visit retention

    main

    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:

    • Non-lazy pages that do not use lazy loading can now tree-shake the visit-retention logic entirely, preventing unnecessary writes and truncations to the visits array during every resume.
  8. Optimize `<show>` bodies in `getNodeContentType`

    main

    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 -->
  9. Optimize unkeyed <for> loops for better performance

    main

    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>
  10. Shorten owner-qualified dynamic tag content keys

    main

    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.