Skip to content
These are temporary community docs. The official docs are in progress.Help improve them
Markless
Esc
↑↓navigate↵open⌘Jpreview
On this page

When code downloads, and when it runs

The compiler splits handlers and updates into symbols, production builds pack them into a few preloaded chunks, and each symbol runs only when its trigger fires.

Last page, every subscription and every handler turned out to be a small function. Where does that code live, and when does it reach the browser?

This page answers two questions that sound like one. When does the code download? When does it run? Markless gives them different answers.

The problem

Downloads cost network time. Running code costs main-thread time. A page that waits for the first click to download its click code makes that click slow. A page that runs all its code at load makes the load slow.

Markless separates the two. In a production build, the code for first interactions downloads early. It still runs only when its event fires.

The mental model

The compiler cuts your component into symbols. A symbol is one function with a stable ID, such as symbol:0. The compiler plans one symbol for each of these:

  • an event handler, such as onClick
  • a DOM update, such as the text that shows count
  • an attach={...} behavior
  • the derive function of a sync computed(), and the runner of an async one
  • a state() initializer and a shared() seed
  • a few more internal kinds, such as @if branch updates

A chunk is one JavaScript file that the browser downloads. One chunk can hold many symbols.

Think of a kitchen. That is an analogy. Packing a chunk is like a delivery. Running a symbol is like cooking one dish. The ingredients can arrive early, and the cook still waits for each order.

Press +1 twice. Then press Settings. Watch the two lanes.

When does click code download, and when does it run?

Press +1 twice. Then press Settings. Compare the two lanes.

The pageTry it
/Production build

Count: 0

Your code: Counter.tsrx
import { state } from '@markless/core'; export default function Counter() @{ let count = state(0); <main> <p>Count: {count}</p> <button onClick={() => count++}>+1</button> <a href="/settings">Settings</a> </main>}
What Markless did
Downloads after page load0Clicks add none
Times click code ran0Handler plus update, per click

Downloaded

  1. loadedFirst-use pack, with the page, through modulepreload. It holds the onClick handler and the {count} update.
  2. noteNothing in it has run yet.

Ran

  1. noteNothing has run yet. Press +1.

Simplified. A production client build packs code by default. The bundler cuts the packs from your routes, in packages/bundler/src/build/route-pack-groups.ts. The navigation pack applies when the app navigates on the client. With packing: false, each module ships as its own chunk. Dev builds are never packed.

Notice that the first-use pack downloads before your first click. Clicks fill the ran lane and download nothing new.

When code runs

A symbol runs only when its trigger fires. A click runs its handler. A write runs the updates that read the written path. An onVisible handler runs when its element scrolls into view.

At mount, no handler symbol runs. Markless’s test for render() checks this. After mount, the list of loaded symbols is empty. One click loads exactly one symbol, symbol:click.

When code downloads: packing

Production client builds turn on packing by default. Packing puts the lazily loaded modules into a few chunks instead of one chunk per module. The bundler cuts those chunks by what each page or route needs, and when:

Pack Holds Downloads
First use Code that the page load and its first interactions run With the page, through modulepreload
Navigation Code that only a client-side navigation runs On navigation intent, never on a landing page
Deferred Code that no page load, first use, or navigation needs On its first import()

A <link rel="modulepreload"> tag tells the browser to fetch and parse a module early. It does not run the module. A production build adds these tags for the handler chunks, at high fetch priority.

One packed chunk still runs lazily inside. Each module in it initializes only when something first imports it. A Markless bundler test proves this with two modules in one chunk.

Set packing: false to ship one chunk per module. Markless never packs dev builds.

What a symbol can capture

A symbol runs later, after the component body has finished. On a server-rendered page, it also runs on a different machine. So it can only reach values that Markless can find again. The compiler checks every symbol when it compiles the file. A symbol can use:

  • state and computed values, by graph reference
  • element handles from element()
  • props and shared() values
  • module imports
  • serializable constants, such as a string or a Date

Anything else fails the build. A DOM node in a local variable is a common case:

const panel = document.querySelector('#panel');

// MARKLESS_EVENT_HANDLER_EMIT_UNSUPPORTED
<button onClick={() => panel?.scrollIntoView()}>Show</button>

Use an element handle instead. This example comes from Markless’s codegen size corpus:

let field = element();
let status = state('idle');

<input el={field} />
<button onClick={() => { field.focus(); status = 'focused'; }}>Focus</button>

A local helper function fails the same way. Move it to module scope, or turn its result into a computed().

Symbol kind Error code
Event handler or callback prop MARKLESS_EVENT_HANDLER_EMIT_UNSUPPORTED
attach={...} behavior MARKLESS_BEHAVIOR_SYMBOL_EMIT_UNSUPPORTED
Any other symbol MARKLESS_CAPTURE_UNSUPPORTED_VALUE

Next: The graph and its symbols are data and small functions. Does a browser have to run them? Native targets →

Was this page helpful?