Shipping a tiny HTML parser

Posted on by Sam

Most HTML parsers are big. That’s fine on a server, but painful in a 1 MB edge worker. This post covers what we learned building one that fits in ~20 KB of wasm.

Goals

  1. Never panic – on any input.
  2. Zero dependencies:
    • no regex
    • no html5ever
      • (it’s great, just big)
  3. Forgiving recovery.

    Unclosed <p> and <li> tags are everywhere.

The core loop

The tree builder keeps an explicit stack, so 100k nested <div>s can’t overflow it:

fn build(tokens: &[Token]) -> Dom {
    let mut open = Vec::new();
    for tok in tokens {
        // `match` on every token kind
        handle(&mut open, tok);
    }
    finish(open)
}

Numbers

ParserSize (gzip)Deps
dom-tree-rs19.5 KB0
html5ever~180 KB12
a | bpipes are escaped

Make it work, make it right, make it fast.

— Kent Beck

Flame graph of the tokenizer
Where the time goes.

Read the API docs or open issues. Questions?