Browser-first architecture study

Chunks carry bytes.
Phases decide experience.

LilScript should optimize network placement, execution correctness, progressive enhancement, and cache survival as one delivery problem—without pretending that a critical module can outrun a dependency it synchronously needs.

Dependency law

If Chunk A has a static import from Chunk B, the browser can prioritize A’s request, but it must fetch and evaluate the required B module graph before evaluating A. Priority changes discovery and transport; it does not repeal dependency order.

Your exact scenario

What can run before Module 5?

Switch the dependency strategy. The source behavior stays explicit; the compiler is not allowed to silently make synchronous work asynchronous.

Response 01

Chunk A

Module 1statically imports Module 5
Module 2depends on Module 1
Module 3critical UI work
static importA cannot evaluate yet
Response 02

Chunk B

Module 5required by Module 1
Module 7feature code
Module 8feature code
Cannot satisfy “evaluate A, then load B.”

A native ESM entry waits for its static dependency graph. Module 3 being textually inside A does not create an independent execution unit.

  1. 1

    Discover AHTML requests the entry.

  2. 2

    Discover and fetch BA’s static import exposes Module 5.

  3. 3

    Evaluate BDependency initialization completes.

  4. 4

    Evaluate AOnly now can Module 3 execute.

Remove the lost round trip

Download early. Register safely. Execute by readiness.

The extra latency is usually a discovery waterfall: B’s URL is learned only after A arrives and runs. Put the generated transfer plan in HTML so both requests begin at t0, without granting B permission to activate early.

Serial discovery
HTMLfetch Arun Afetch Bcontinue

B begins one network round trip too late.

Generated transfer plan
A · high priorityregister → run M3
B · warm in parallelregister → unlock M1

Transfer overlaps; semantic execution order remains gated.

generated in HTMLsimplified
<link rel="modulepreload" href="/lilscript/a.cap.js">
<link rel="modulepreload" href="/lilscript/b.cap.js">

<script type="module">
  const aReady = import("/a.cap.js");
  const bReady = import("/b.cap.js");

  const A = await aReady;
  A.register(L);       // definitions only
  await L.run(3);      // critical closure ready

  const B = await bReady;
  B.register(L);
  await L.run([1, 2]); // M5 → M1 → M2
</script>
Transfer

HTML exposes every known URL

Direct modulepreload declarations remove dependency discovery RTT. A can receive high priority while B warms concurrently.

Registration

Capsules are inert ESM wrappers

No eval. Importing a capsule creates factory definitions and export cells, but user-visible module effects remain unexecuted.

Activation

The scheduler runs ready closures

Module 3 executes as soon as A registers. Modules 1–2 remain blocked until Module 5 exists and the required evaluation order is legal.

Policy

Bytes versus latency stays explicit

If B may never be needed, warming it spends bytes. LilScript can choose immediate, intent, visible, idle, or manual transfer separately from activation.

The cost is real—but bounded.

The compiler must first prove that the reordered effects are independent or require an explicit phase declaration; it cannot silently change ordinary module ordering. The runtime must implement live-binding cells, once-only effects, cycle/SCC ordering, failure propagation, cancellation, and perhaps top-level-await state. Capsules should therefore be an opt-in browser delivery backend, with native ESM retained for ordinary graphs. The benchmark must include scheduler and registration-wrapper bytes.

Do not collapse the axes

Four decisions, not one bundle switch

A folder name can inform placement. It cannot safely dictate when code becomes observable.

01

Placement

Which functions and data share an HTTP response? Automatic, folder-preferred, or manually required groups belong here.

02

Discovery and fetch

When does the browser learn each URL, and at what priority? HTML, modulepreload, intent, visibility, and idle policy belong here.

03

Instantiation and evaluation

Static ESM dependencies preserve dependency-first execution. Dynamic imports and generated initializers create explicit later phases.

04

Activation

When may a feature touch the DOM, install listeners, or expose state? Critical, interactive, visible, idle, and manual activation belong here.

Manual and folder-based bundling

Folders are useful affinity. They are not execution semantics.

My recommendation: support folder-based control, but make it one input to a constrained planner. A critical phase, a physical response, and a source directory must remain different concepts.

delivery policyconcept
delivery browser {
  phase critical {
    root Module3;
    budget brotli <= 8kb;
    budget requests <= 2;
  }

  prefer folder "./checkout" as checkout;
  keep "./editor" together;
  isolate "./admin" on interaction;
  share automatic;
}
Default

Automatic packing

The planner searches placements using exact per-response Brotli/gzip bytes, request depth, reuse, invalidation, and activation scenarios.

Soft control

prefer folder

Preserve team ownership and cache locality when competitive. The optimizer may cross the folder boundary, but must explain the measured win.

Hard control

keep / isolate

Honor deliberate product, security, licensing, or rollout boundaries. If semantics make a rule impossible, fail with the exact dependency path.

Separate axis

phase

Defines what must become usable and when. It computes a dependency closure; it does not assume everything beside it in a directory is critical.

Why folder-only loses

Feature folders often import shared state, design systems, and utilities. Blind one-folder/one-chunk output creates tiny requests, duplicate helpers, cache-coupled barrels, or a giant “shared” chunk.

Why full automation loses

The cost model cannot infer every deployment boundary, business-critical interaction, permission surface, or intentional cache cohort. Manual constraints are essential—not an afterthought.

The compiler contract

First build the semantic activation DAG. Then pack its ownership sets into responses under hard rules and soft affinities. Finally print a “why this chunk?” report with rejected alternatives.

Linker operations

“Inject it into A” has four different meanings

Each technique has a different execution and identity contract. LilScript should choose only among transformations it can prove equivalent.

Native edge

Emit an ESM import

Smallest runtime and real live bindings. Correct for always-needed dependencies, but a static edge makes B part of A’s evaluation prerequisite.

Use when dependency order is intentional.
Activation edge

Emit a typed initializer

Keep Module 1’s body behind initModule1(module5). Module 3 can activate first; the initializer runs exactly once after import(B) resolves.

Needs effect and ordering analysis.
Ownership move

Hoist or clone proven-safe code

Move a required pure function or immutable constant into A, or clone it into both chunks when import scaffolding costs more.

Never clone observable identity or state.
Generated bridge

Emit a tiny orchestration module

A generated bridge maps activation IDs to dynamic imports, normalizes failures, and wires progressive HTML to features without reimplementing the ESM loader.

Pay only when several phases share it.

Non-negotiable invariants

  • Mutable state and live exports have one authoritative owner.
  • Module side effects execute once and in a semantically valid order.
  • Functions, classes, symbols, and singleton objects are not cloned when identity is observable.
  • Deferred failures and cancellation remain visible through typed tasks.
  • Import/export, bridge, manifest, and loader bytes enter the chunk cost.

Language design sketch

Express user-visible phases, not loader tricks

This is proposed syntax, not a shipped promise. The important part is the compile-time contract behind it.

progressive-checkout.lilconcept
progressive Checkout {
  critical {
    render CheckoutShell();
    capture intent for "submit";
  }

  enhance form on interaction
    from "./checkout/validation";

  enhance totals on visible
    from "./checkout/pricing";

  prepare recommendations on idle
    from "./recommendations";
}

critical

The compiler computes its complete dependency closure and enforces byte/request budgets. A later-phase synchronous dependency is an error unless safely hoisted or cloned.

enhance … on

Declares activation policy: interaction, visibility, idle, intent, or manual. It compiles to a typed dynamic boundary and must upgrade existing HTML.

capture intent

Installs a tiny generated listener before the feature arrives. It records declared, replay-safe intent instead of blindly replaying arbitrary browser events.

Delivery budgets

Critical Brotli bytes, request count, graph depth, and activation latency are build constraints. An annotation without a verified closure is merely a comment.

Compiler rule:a phase may depend synchronously only on the same or an earlier phase. A dependency on a later phase must return a typed task or cross a compiler-generated activation interface.

Generated orchestrator

A tiny phase linker can be worth its bytes

The browser still fetches and caches ordinary ESM capsule files. Inside those files, LilScript can take responsibility for source-module registration and readiness-ordered execution when that removes a measured waterfall.

Layer 0 · always

Semantic HTML and CSS

Content, forms, links, and essential actions work before enhancement. Build/server output owns critical markup and preload declarations.

Layer 1 · generated

Per-app phase linker

Factory registry, live-binding cells, dependency readiness, event delegation, import scheduling, cancellation, and failure signals. Tree-shaken to only used semantics.

Layer 2 · on demand

Typed capsule or ESM chunks

Native ESM for simple graphs; inert registration capsules for critical, interaction, visible, idle, and manual activation groups.

Layer 3 · optional

Offline/cache worker

Versioned Service Worker only when offline or background behavior is requested. It is not required for ordinary HTTP caching.

Inline core

Wins: immediate discovery and no request. Costs: repeated HTML bytes, CSP nonce/hash handling, and no independent cache.

External core

Wins: long-lived cache and shared pages. Costs: another discovery edge unless preloaded, plus version coordination.

Generated hybrid

Recommendation: inline the minimal transfer plan and critical scheduler; keep reusable live-binding and cycle machinery in a content-addressed module when the selected graph requires it.

Per-build choice

Native ESM backend: nearly zero runtime, best for ordinary static and dynamic graphs. Capsule backend: pays a small phase-linker cost to decouple response arrival from source-module execution and remove critical discovery waterfalls.

Cache architecture

Optimize the second deployment, not only the first visit

A small initial build can still be poor if one leaf change invalidates every parent URL or strands users on deleted chunks.

HTML + manifest

Revalidate

Short-lived or no-cache. Carries the build ID, current content-addressed URLs, critical preloads, and activation manifest.

Hashed chunks

Immutable

Long-lived cache headers. Keep older build artifacts during rollout so already-open documents can finish lazy imports.

Stable logical IDs

Indirect

Import maps or manifest-driven dynamic imports can prevent a changed child hash from rewriting otherwise unchanged lazy parents.

Service Worker

Explicitly versioned

Only for offline or advanced policy. CacheStorage is separate from HTTP cache and requires deliberate install, activation, cleanup, and rollback behavior.

The planner needs navigation scenarios

Cold documentcritical bytes, requests, depth, HTML overhead
First interactionmarginal feature bytes and activation delay
Later navigationshared-cache hits versus unnecessary vendor downloads
Next deploymentinvalidated bytes, hash cascades, and old-build coexistence

Compression remains per physical response. For every candidate plan, LilScript must re-run name assignment, declaration layout, pooling, import/export aliases, and exact gzip/Brotli measurement per chunk—then score the route graph, not a concatenated global artifact.

JavaScript, C, and native

Shared logic; capability-specific delivery

Browser orchestration should not corrupt the portable language core or pretend that dynamic JavaScript modules have a C ABI.

Portable core

Pure logic, data structures, validation, state machines, and deterministic critical computations continue compiling to JavaScript, C, and native execution.

Browser capability

DOM rendering, visibility, interaction scheduling, module loading, and Service Worker policies require an explicit browser profile or typed extern capability.

Honest diagnostics

A native target either receives a real platform adapter or reports that the delivery primitive is browser-only. Silent stubs would make multi-output claims meaningless.

Recommended direction

Build phase-aware chunking before adding a large runtime

  1. 01

    Model activation groups in IR

    Track critical, interactive, visible, idle, and manual closures independently from source folders and output chunks.

  2. 02

    Generalize chunks to arbitrary ownership sets

    Permit several modules or selected functions per chunk, while carrying origin, side effects, identity, and cross-phase constraints.

  3. 03

    Emit a delivery manifest for HTML/server use

    Critical modulepreloads should be discovered in HTML, not injected only after the entry script begins executing.

  4. 04

    Generate only the orchestration used

    No runtime for a static page; native ESM for ordinary graphs; a measured capsule linker for phase-critical graphs; a Service Worker only for explicit offline requirements.

  5. 05

    Benchmark dependency conflicts

    Add this exact A→B scenario, route transitions, slow networks, cache-warm revisits, deployment churn, and failure recovery against Vite/Rolldown and Closure chunk output.

Foundation today

LilScript already exposes single, split, and preserve-modules modes and scores emitted chunks with raw, gzip, Brotli, request, depth, preload, and reuse costs.

Architectural limit

The current planned-chunk model has one owning source module. It cannot yet represent arbitrary multi-module or function-level activation groups.

Next compiler seam

ActivationGraph → OwnershipPlan → TransferPlan → Emission, with exact measurement and semantic validation feeding back into the search.

Platform references

Constraints come from the browser, not taste