Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / UI

chunks — assets a control loads on demand

control X { chunks { Math; Diagrams; } }

A `chunks { }` block declares assets a control ships but does not need at mount — a maths renderer, a diagram engine, a stylesheet only one feature uses. The shim asks for one by name when it needs it, so a heavy optional feature costs nothing on the pages that never use it.

stable2 examples compiled by CIuicontrolsauthoringperformance

Summary#

A control is one bundle, and every page that mounts it downloads all of it. That is the right shape for a control's own code, and the wrong shape for a feature most documents never touch: a maths renderer is a quarter of a megabyte, a diagram engine several times that, and bundling either means charging every reader for a thing one page in fifty uses.

A chunks { } block is the second address. It names the assets a control ships separately, and the shim asks for one by name at the moment it needs it:

if (documentHasMath) {
  const { render } = await import(host.chunkUrl("Math"));
  render(el, source);
}

A chunk is stored, content-addressed, cached and garbage-collected exactly like the bundle beside it. What is different is only that the platform does not load it — the control decides when, and whether at all.

A chunk can be a single file or a whole package directory. The package shape exists for libraries that load their own parts at run time — a worker, a .wasm sibling, a renderer per diagram type — which no bundler can flatten into one module. See One file, or a package below.

Signature#

control <Name> {
  chunks {
    <ChunkName>;
    <ChunkName>;
  }
}

Registered per name — as one file, or as a directory:

osy control add controls/editor.js \
  --chunk Math=vendor/temml.js \
  --chunk MathCss=vendor/temml.css \
  --chunk-package Diagrams=dist/diagrams/index.js

Description#

Declaring a chunk and pointing it at a file#

The declaration names the chunks; the registration says which file each one is.

control MarkdownEditor {
  contractVersion "1.1"
  participation headless
  props { string ownerType; }
  chunks {
    /// A MathML renderer, loaded the first time a document contains `$…$`.
    Math;
    /// Its stylesheet.
    MathCss;
  }
}

[Page("/doc")] [AllowAnonymous]
component DocPage() {
  render {
    MarkdownEditor(ownerType: "Document");
  }
}

The two halves are checked against each other. A --chunk name the control does not declare is refused, naming the ones it does; a declared chunk with no file registered is a compile error once anything renders the control, and a warning while nothing does — so the bundle can be registered first and its chunks next, without a broken state in between.

Nothing about a chunk reaches the app. A chunks { } block changes no call site, no prop, and no page: it is a statement about how the control ships itself.

Getting a chunk's URL in the shim — host.chunkUrl#

host.chunkUrl(name) returns a same-origin URL. The generated typings key it to the names you declared, so a typo does not compile:

export interface MarkdownEditorHost {
  chunkUrl(name: "Math" | "MathCss"): string;
}

It returns a URL, not a module, and that is deliberate. One member then covers both kinds of asset — a module you import(), and a stylesheet you put in a <link>:

// a module
const temml = await import(host.chunkUrl("Math"));

// a stylesheet
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = host.chunkUrl("MathCss");
document.head.appendChild(link);

A member that returned a module could not express the stylesheet at all, and one that decided when to load would be answering a question only the control knows the answer to.

The URL is content-addressed and immutable, so caching it forever is safe and re-asking is free — chunkUrl does no work beyond a lookup. Loading twice is the shim's own concern: keep the promise.

One file, or a package#

A chunk name resolves to one of two things, and the registration decides which. The declaration is the same either way — a chunk name is a name, and host.chunkUrl() returns something you can load.

A single file is served at its own content address, with no directory around it. So a relative specifier inside one has nothing to resolve against:

import { helper } from "./helper.js";   // ✗ refused in a single-file chunk — there is no "." to be relative to

osy control add refuses that at registration and names the specifier it found. Bundle the chunk to one file (esbuild --bundle --format=esm), or register it as a package instead.

The same applies to a stylesheet's url() in a single-file chunk. Its CSS cannot reference a font by relative path; register the face with web fonts — shipping a typeface with your app and point the url() at the address that returns.

A package is a whole directory, served under a real base — so relative imports resolve exactly as they do on a static host:

osy control add controls/editor.js --chunk-package Diagrams=dist/diagrams/index.js

You give the entry file; the package is the directory containing it, taken recursively. host.chunkUrl("Diagrams") returns that entry's URL, and everything the entry reaches from there — ./chunks/x.js, a .wasm sibling, a worker started with new Worker(new URL("./worker.js", import.meta.url)) — resolves against the directory it genuinely sits in.

Reach for a package when a library loads its own parts at run time. Many do, and flattening one into a single file is both wasteful and sometimes impossible: a diagram engine that lazy-loads a renderer per diagram type becomes one module carrying every renderer, and a library that starts a worker or fetches a .wasm cannot be bundled at all.

⚠️ Register your own build output, never a package's distribution directory. A published package typically ships several build flavours and a full type-definition tree — one common diagram library's is 83 MB across 1167 files. Point a bundler at the library and register what it writes:

esbuild node_modules/some-lib/dist/index.mjs --bundle --format=esm --minify --splitting --outdir=dist/diagrams
osy control add controls/editor.js --chunk-package Diagrams=dist/diagrams/index.js

A path inside node_modules is refused by name, before anything is read.

A package's identity is its whole file set, so re-registering an unchanged directory writes nothing, and a file added or deleted since it was registered fails the next compile rather than shipping a set nobody registered.

What may be served#

A chunk's content type comes from its extension, and the set is closed:

ExtensionServed as
.js, .mjstext/javascript
.csstext/css
.jsonapplication/json
.wasmapplication/wasm
.woff2, .woff, .ttfthe matching font type

Anything else is refused. .html and .svg are refused deliberately: both would give the app a same-origin address that executes script, and no control needs one.

Anything else is refused, in a package member exactly as in a single file — and it is refused by name rather than skipped, so a build output containing (say) a source map tells you which file to exclude instead of registering cleanly and 404ing later on the one file nobody thought about.

The per-asset size limit is the bundle's — 5 MB, applied per file — and every asset a control ships counts toward the app's total.

A chunk is not a prop, a style, or a slot#

Four ways a control takes something from outside, and they answer different questions:

What it isDecided by
propa value the control renders withthe app, per call site
stylea look knob the app may overridethe app, or its theme
slotcontent the app writes inside the controlthe app
chunkcode or an asset the control ships itselfthe control

A chunk is the only one of the four the app knows nothing about. If an app should be able to decide something, that is a prop; if it should be able to supply something, that is a slot. A chunk is the control's own weight, moved off the critical path.

Unlike styles { }, a chunk is legal at every participation rung — including opaque. A chunk is neither look nor app data; it is the control's own code, and an opaque island is exactly the kind of control that ships a heavy renderer.

Examples#

A diagram control that loads its engine once, on the first diagram it meets:

control DiagramView {
  contractVersion "1.1"
  participation opaque
  props { string source; }
  chunks { Engine; }
}
let engine: Promise<{ render(el: HTMLElement, src: string): void }> | undefined;

export function mount(el, props, host) {
  const draw = async (src: string) => {
    // Loaded ONCE and remembered — a second diagram on the page costs nothing.
    engine ??= import(host.chunkUrl("Engine"));
    (await engine).render(el, src);
  };
  if (props.source) void draw(props.source);
  return {
    update(next) { if (next.source) void draw(next.source); },
    destroy() { el.replaceChildren(); },
  };
}

The page that renders DiagramView with no source downloads the control and nothing else.

See also#

Related

control — foreign UI controls (charts, grids, maps)

A `control` block declares the contract of a foreign UI widget — a chart, a data grid, a map — that a small JavaScript…

commands — the verbs a control accepts

A `commands { }` block declares the verbs a control accepts — the mirror of its events. An event is the control telling…

styles — a control's own look knobs

A `styles { }` block declares the look values a control owns — its paddings, widths, shadows — as named knobs an app…

web fonts — shipping a typeface with your app

Naming a font in your theme asks for it; `osy font add` ships it. The command pins a font file in your project's lock…

The markdown editor kit — a rich editor you opt into

A full rich-text markdown editor — sections, partial saves, a block menu, tables, find and replace, maths and diagrams…