Movatterモバイル変換


[0]ホーム

URL:


Skip to content
Search Gists
Sign in Sign up

Instantly share code, notes, and snippets.

@mizchi
Last activeNovember 29, 2025 10:45
    • Star(0)You must be signed in to star a gist
    • Fork(0)You must be signed in to fork a gist

    Select an option

    Save mizchi/4f9293a9742deed7ce6c26a05bbd0727 to your computer and use it in GitHub Desktop.

    This document analyzes the bundle size of MoonBit JavaScript output, identifying the main contributors to code size and potential optimization opportunities.

    Note: The analyzed output is after terser DCE (Dead Code Elimination) with compress enabled. Unused code has already been removed.

    Analysis Target

    https://github.com/mizchi/js.mbt/blob/main/src/examples/aisdk/main.mbt

    Example: aisdk (AI SDK streaming example)

    • Source: 24 lines of MoonBit (src/examples/aisdk/main.mbt)
    • Output: 115,183 bytes (raw JS)

    Input Source Code

    main.mbt:

    asyncfnmain {// Load environment variables@dotenv.config()|>ignoreprintln("[ai] Starting stream...")// Start streamingletstream=@ai.stream_text(model=@ai.anthropic("claude-sonnet-4-20250514"),prompt="Count from 1 to 10, one number per line",  )// Iterate over text chunkslettext_iter=stream.text_stream()letstdout=@process.stdout()whiletrue {matchtext_iter.next() {Some(chunk)=>stdout.write(chunk)|>ignoreNone=>break    }  }println("\n\n[ai] Stream complete!")// Get final usage statsletusage=stream.usage()println("Tokens used:\{usage.total_tokens.to_string()}")}

    moon.pkg.json:

    {"is-main":true,"import": ["mizchi/js","mizchi/js/node/process","mizchi/js/node/tty","mizchi/js/npm/ai","mizchi/js/npm/dotenv","moonbitlang/async"  ]}

    Size Breakdown

    CategorySizePercentage
    mizchi$js bindings (excluding vtable)48,849 bytes42.4%
    vtable inline expansion33,203 bytes28.8%
    MoonBit runtime (core+async)19,875 bytes17.3%
    Others (helpers/types)9,545 bytes8.3%
    main.mbt (user code)2,680 bytes2.3%
    Unclassified1,031 bytes0.9%

    MoonBit Runtime Details

    ComponentSizeNotes
    moonbitlang$core15,010 bytesMap, Deque, Hasher, etc.
    moonbitlang$async4,865 bytesCoroutine scheduler

    mizchi$js Binding Details

    ComponentSizeNotes
    mizchi$js$npm (ai, dotenv)45,195 bytesNPM package bindings
    mizchi$js (core)34,534 bytesCore JS interop
    mizchi$js$node (process)2,323 bytesNode.js bindings

    vtable Breakdown

    TypeCountSize per instanceTotal
    JsImpl vtable (13 methods)48~558 bytes26,799 bytes
    PropertyKey vtable100~64 bytes6,404 bytes

    Key Findings

    1. Bindings + vtable account for 71.2%

    The JavaScript interop layer dominates bundle size:

    • Binding logic: 48,849 bytes (42.4%)
    • vtable inline expansion: 33,203 bytes (28.8%)

    2. MoonBit runtime is relatively small (17.3%)

    The core runtime includes:

    • Data structures for async (Map, Deque)
    • Hash functions
    • Coroutine scheduler

    3. User code is only 2.3%

    24 lines of MoonBit compiles to 2,680 bytes, but the remaining 97.7% is runtime and bindings.

    Source Code Structure

    JsImpl Trait with Default Implementations

    The binding layer uses a trait with default method implementations (src/impl.mbt):

    pub(open)traitJsImpl {to_any(Self)->Any=_get(Self,&PropertyKey)->Any=_set(Self,&PropertyKey,&JsImpl)->Unit=_call(Self,&PropertyKey,Array[&JsImpl])->Any=_call0(Self,&PropertyKey)->Any=_call1(Self,&PropertyKey,&JsImpl)->Any=_call2(Self,&PropertyKey,&JsImpl,&JsImpl)->Any=_call_throwable(Self,&PropertyKey,Array[&JsImpl])->AnyraiseThrowError=_call_self(Self,Array[&JsImpl])->Any=_call_self0(Self)->Any=_call_self_throwable(Self,Array[&JsImpl])->AnyraiseThrowError=_delete(Self,&PropertyKey)->Unit=_hasOwnProperty(Self,&PropertyKey)->Bool=_}implJsImplwithget(self,key :&PropertyKey)->Any {ffi_get(self.to_any(),key.to_key()|>identity)}implJsImplwithset(self,key :&PropertyKey,val :&JsImpl)->Unit {ffi_set(self.to_any(),key.to_key()|>identity,val.to_any())}

    FFI Definitions

    The FFI layer (src/ffi.mbt) provides simple JavaScript operations:

    extern"js"fnffi_get(obj :Any,key :String)->Any=#| (obj, key) => obj[key]extern"js"fnffi_set(obj :Any,key :String,value :Any)->Unit=#| (obj, key, value) => { obj[key] = value }extern"js"fnffi_call0(obj :Any,key :String)->Any=#| (obj, key) => obj[key]()extern"js"fnffi_call1(obj :Any,key :String,arg1 :Any)->Any=#| (obj, key, arg1) => obj[key](arg1)

    Main Issues

    Issue 1: vtable Inline Expansion (Biggest Impact)

    When using trait objects (&JsImpl), the compiler generates a vtable for each call site.Every JS value operation creates a vtable object inline:

    // Current: 558 bytes per instance{self:value,method_0:mizchi$js$$JsImpl$to_any$9$,method_1:mizchi$js$$JsImpl$get$9$,method_2:mizchi$js$$JsImpl$set$9$,// ... 10 more methodsmethod_12:mizchi$js$$JsImpl$hasOwnProperty$9$}

    This 13-method vtable is expanded inline 48 times.

    Example from generated code:

    // A simple promise.then().catch() generates ~400 bytes:mizchi$js$$JsImpl$call1$9$(mizchi$js$$JsImpl$call1$15$(self,{self:"then",method_0:mizchi$js$$PropertyKey$to_key$3$},{self:_cont,method_0: ...,method_1: ..., ...,method_12: ...}),{self:"catch",method_0:mizchi$js$$PropertyKey$to_key$3$},{self:_err_cont,method_0: ...,method_1: ..., ...,method_12: ...});

    Issue 2: PropertyKey Wrapping

    Simple string keys are wrapped in objects:

    // Current: 64 bytes{self:"model",method_0:mizchi$js$$PropertyKey$to_key$3$}// Could be: 7 bytes"model"

    Optimization Opportunities

    1. vtable Sharing (Potential: -26.7%)

    Share vtable objects instead of inline expansion:

    // Define onceconstJsImpl$vtable$9={method_0:mizchi$js$$JsImpl$to_any$9$,method_1:mizchi$js$$JsImpl$get$9$,// ...};// Use reference{self:value, ...JsImpl$vtable$9}// or{self:value,$v:JsImpl$vtable$9}

    Estimated savings: ~30,763 bytes (26.7%)

    2. Direct FFI for Simple Operations

    For simple property access/calls, generate direct code:

    // Currentmizchi$js$$JsImpl$get$9$(obj,{self:"key",method_0: ...})// Optimizedobj["key"]

    3. Eliminate PropertyKey Wrapper

    Pass strings directly instead of wrapping:

    // Current{self:"key",method_0:mizchi$js$$PropertyKey$to_key$3$}// Optimized"key"

    Optimization Priority

    1. vtable sharing - Highest impact, 28.8% of bundle
    2. PropertyKey simplification - 5.6% of bundle
    3. Direct FFI generation - Reduces binding code complexity
    4. Runtime tree-shaking - Remove unused Map/Set operations

    Running the Analysis

    # Build examplesmoon build --target js# Generate size report with output files./scripts/check_sizes.ts --output-files# Run analysis scriptnode tmp/analyze_size.js

    Related Files

    • scripts/check_sizes.ts - Bundle size measurement tool
    • tmp/check-sizes/*/ - Output files for inspection
    • .bundle_size_baseline.json - Size baseline for comparison
    Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

    [8]ページ先頭

    ©2009-2025 Movatter.jp