Build1 publisher3 min readPublished
7MB to 52KB: WASM size is a compiler decision, and Go's runtime sets the floor
A browser-side HTML-to-Markdown converter shed 99.3 percent of its WebAssembly payload across three rewrites. Most of the bytes came off when the toolchain changed, not the flags.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened
- The HTML-to-Markdown converter runs entirely in the browser via WebAssembly with no server and no uploads; the .wasm binary is downloaded by every visitor on first load, so binary size directly impacts time-to-interactive.
- Over three rewrites the author went from 7MB down to 52KB, a 99.3 percent reduction.
- The first version used Go with the html-to-markdown library by JohannesKaufmann and golang.org/x/net/html for DOM parsing, built with GOOS=js GOARCH=wasm go build, producing about 7 MB uncompressed.
- Go's WASM output includes the entire Go runtime: the goroutine scheduler, the garbage collector, and the full reflect package pulled in by encoding/json as a transitive dependency.
- Even with encoding/json stripped and buildvcs=false, the runtime overhead remained substantial; Go's WASM target was designed for feature-completeness, not binary size.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A developer shipping a browser-side HTML-to-Markdown converter cut the WebAssembly payload from roughly 7MB to 52KB over three rewrites, a 99.3 percent reduction [2]. The distribution of that saving matters more than the endpoint: the single largest drop came from swapping compilers, and the release-profile flags that usually absorb the attention delivered none of it [5][8].
The tool runs entirely in the browser, with no server and no uploads, so every visitor downloads the binary on first load and the size lands directly on time-to-interactive [1]. Version one was Go, using JohannesKaufmann's html-to-markdown plus golang.org/x/net/html, built with GOOS=js GOARCH=wasm, and it came out around 7MB uncompressed [3]. According to the author, that output carries the whole Go runtime: the goroutine scheduler, the garbage collector, and the full reflect package dragged in transitively by encoding/json [4]. Stripping encoding/json and setting buildvcs=false left the runtime overhead substantially intact, because Go's WASM target was built for feature-completeness rather than binary size [5].
Recompiling the same program with TinyGo, which uses an LLVM backend, a simpler runtime, a different garbage collector and aggressive dead-code elimination, produced about 936KB after wasm-opt, a 7x reduction [6]. On the author's numbers, that one substitution accounts for roughly 87 percent of every byte eventually removed [1]. It also has a cost: TinyGo's limited reflect support created compatibility friction with the html-to-markdown library and the x/net/html parser, and the author describes hitting a ceiling there [7].
The third rewrite is the instructive one. Moving to Rust with scraper (html5ever) and htmd, built with opt-level 'z', LTO, one codegen unit, panic=abort and stripped debug info, landed at 936KB after wasm-bindgen [8]. That is the same figure TinyGo produced, meaning the language change bought nothing by itself [2]. html5ever pulls in markup5ever, tendril, phf, string_cache and about 124 crates in total [9], and the author puts its compiled cost at roughly 900KB, comparable to TinyGo's entire runtime [14]. A runtime and a spec-complete parser are the same class of expense.
The 52KB version dropped every dependency except wasm-bindgen and replaced the DOM with a single-pass streaming converter: a tag-level state machine splitting on angle brackets, a noise-depth counter, content extraction by substring search for main, article or body, and direct tag-to-Markdown emission [10]. That is an 18x cut from the 936KB plateau [3]. It is also less capable by design: no error recovery for malformed markup, no CSS selector queries, no encoding detection beyond assuming UTF-8 with a lossy fallback, and density extraction reduced to grabbing main or article [11]. The author judges those acceptable for the job at hand, converting well-structured documentation HTML for AI consumption [12].
The load-bearing conclusion is the author's own: in Go and TinyGo the runtime dominates, and output starts in the hundreds of kilobytes even with zero application logic [13]. The final 52KB binary sits below that floor, so no amount of application-level pruning inside a Go toolchain could have reached it [4].
Worth watching if you are sizing a browser payload: measure the empty-binary floor of your toolchain before you touch a single flag, and price your parser separately from your language, since on this evidence the two can cost the same [14][13].