Build1 distinct publisher3 min readUpdated
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

Compiled by The EngineerSomething wrong?How this is made
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].
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
The html5ever parser pulls in markup5ever, tendril, phf, string_cache and about 124 crates in total.
The author states html5ever adds roughly 900KB of compiled code, comparable to TinyGo's entire runtime, and that if optimizing for size the parser choice matters as much as the language choice.
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.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Detailed but unverified single-author measurements
The build commands, dependency lists and release profiles are specific enough to be reproduced in principle, and the reported sizes are arithmetically consistent with the headline reduction. But everything rests on one self-reported account from one publisher: no repository, no artifact, no compressed transfer sizes, no baseline build behind the Go runtime floor claim, and no independent replication.
One personal deployment, no external uptake
The only adoption fact in the cluster is that the author's own converter is live and client-side. There is no user count, traffic figure, downstream dependent, or any other party reported to have applied the technique.
Concrete numbers, over-generalised conclusion
The per-build figures are stated plainly and the tradeoffs are unusually candid, which limits the gap. It is positive rather than zero because a single n=1 project is presented as a general rule about compilers and runtime floors: the hundreds-of-KB floor is never measured, the ~900 KB attributed to html5ever is a difference rather than an isolated figure, and the custom parser's adequacy is asserted from one content domain with no fidelity testing.
Self-promotional engineering post
The author is writing about his own product, links the hosted tool by name, and closes with engagement-seeking questions on a community platform where reach rewards dramatic before-and-after framing. That biases toward emphasising the 99.3 percent number; it is offset somewhat by the candid list of capability regressions, which a purely promotional post would omit.
Plausible and specific, but unreplicated
The mechanism matches well-known properties of the Go, TinyGo and Rust WASM toolchains and the numbers hang together, so the core narrative is credible. Confidence stays mid-range because there is one source, one author, no replication, and the story's most quotable conclusions are the parts with the least measurement behind them.
build
A one-ULP divergence, caught because a fifteen-year-old test suite refused an epsilon1 distinct publisher
build
796 pages of semantic search with no vector database, and what it cost to skip one1 distinct publisher
build
The only way to prove a contract test can fail is to ship a server that lies1 distinct publisher
build
Your meter now runs on someone else's machine: signed receipts, fsync, and failing open1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 17, 2026