The <script main> Block
Experimental — this syntax style is being explored in issue #314. Editor tooling (Volar) does not understand the block yet, and details may change.
Main Thread Script lets individual functions run
on the Lynx main thread by marking each one with a 'main thread' directive.
That works, but it scatters the thread boundary across the component: the
reader has to inspect every function body to know where it runs.
The <script main> block is an alternative syntax style for the same
feature: all of a component's main-thread code lives in one dedicated SFC
block, and the block boundary is the thread boundary.
Every top-level function in <script main> is compiled as a main thread
function — no per-function directive. Everything you know from the directive
style applies unchanged, because the block is the directive style after
compilation: the compiler lowers the block into the exact same worklet
machinery (value capture, MainThreadRef, runOnMainThread /
runOnBackground, shared modules).
Semantics
A <script main> block accepts these top-level statements:
Structural rules:
- At most one
<script main>per component. langmust match<script setup>(bothts, or both plain).<script main setup>and<script main src="...">are rejected.- Main thread functions in the block can call each other, and can be passed
to
runOnMainThread()from background code — same as directive-marked functions.
Under the hood the block is lowered before the Vue SFC compiler runs: each
top-level function gets the 'main thread' directive injected and the block
merges into <script setup>. Both threads compile the identical merged
source, so the worklet content hashes that link the background context
objects to the main-thread registrations agree by construction.
Migrating from the directive style
Migration is mechanical — move main-thread functions into the block and drop their directives:
Three of the repository's MTS examples are written in this style — each was migrated from the directive style and compiles to identical worklet code (verified by diffing the emitted main-thread registrations of both versions, and by driving the built bundles in Lynx for Web):
- shared-module — a main-thread tap handler calling a
with { runtime: 'shared' }import directly on the main thread:
- script-main-block — the cross-thread calls
round trip (
runOnBackground+runOnMainThread) with both main-thread functions in one block:
- Gallery (complete) — the gallery tutorial's
finished MTS scrollbar, with a worklet-calling-worklet pair in the block
(
onScrollMTS→adjustScrollbarMTS), and SwiperMTS from the swiper tutorial, whose three touch handlers shareMainThreadRefstate across the block's functions.
When to prefer which style
Both styles compile to the same output — this is purely about source organization:
<script main>shines when a component has several main-thread functions that form a unit (gesture handler sets, scroll-driven effects): one glance shows everything that runs on the main thread.'main thread'directives remain the only option for worklets declared in plain.ts/.jsmodules (composables), inside conditionals, or in Options API components — the block is an SFC +<script setup>feature.
Current limitations
- Editor support: Volar treats
<script main>as an Options API script block, so you may see spurious type errors in the IDE. Builds are unaffected. - One block per component;
langmust match the setup block. - A
</script>inside a string literal of the block is not supported (block scanning is tag-based).