Software

My 128-line Markdown parser is 3x faster than marked — and fails 13 of 14 tests

I wrote a dependency-free Markdown converter, then actually benchmarked it. My stated reason turned out to be nonsense; the real reason was something else entirely.

Bu yazının Türkçesi: Türkçe sürüm.

While putting this blog together I made a small decision: I would write the Markdown converter myself. I needed a script that reads content/blog/*.md and emits HTML into public/blog/, and instead of typing npm i marked I sat down and wrote 128 lines of code.

The reason I gave myself at the time was "it'll be faster". Today I sat down and measured. My reason was nonsense. The decision was still right, but for a different reason than I thought. Both the measurements and the reasoning are below.

The setup

I did not want to benchmark on synthetic text. I concatenated the repository's own Markdown files (the README and a planning document) six times: 70,960 characters, 9,085 words. That is roughly eight blog posts — the size of a real build.

Three converters: my own md.mjs, marked, and markdown-it. Twenty warm-up runs each, then 50 measured runs; the numbers below are medians. Node 24.18.0 on an Apple Silicon Mac.

ConverterMedianBestInstalled size
md.mjs (mine)0.88 ms0.76 ms4.8 kB, one file
marked2.55 ms2.20 ms484 kB
markdown-it2.63 ms2.28 ms1.9 MB

Cold start differs too: measured in a separate process, loading the marked module takes 5 ms and markdown-it 5.9 ms. My file's load cost fell below measurement noise.

So the "3x faster" claim is true. Now let me explain why it is completely irrelevant.

Why the speed argument is nonsense

This converter runs at build time. Not when a visitor opens a page — when I type node tools/build-blog.mjs. Over its lifetime it will run maybe five times a day.

On a 70 kB input the difference is 1.7 milliseconds. Five runs a day times 1.7 ms is 8.5 milliseconds saved per day. Collected over a year that is about three seconds. Writing and debugging 128 lines cost me thousands of times that.

In short: the speed argument is real and irrelevant. It is the first thing everyone who writes their own version says, and it is usually hollow for the same reason.

What I lost: 13 of 14 tests

Here is the interesting part. I fed 14 common Markdown constructs to both my converter and marked and compared the output. Exactly one matched.

Some of the mismatches are cosmetic: marked puts whitespace between tags and I don't; it adds align attributes to table cells and I handle that with a class. Those are fine.

But these six are real losses:

ConstructWhat I emitWhat it should be
Nested listSub-items flattened to the top levelNested ul
Image syntaxThe exclamation mark stays as text, the rest becomes a linkAn img element
Autolink (a bare URL in angle brackets)Escaped plain textA clickable link
Italic inside boldThe asterisks show up on screenNested strong and em
Indented code blockAn ordinary paragraphA pre block
Two trailing spacesLines get joinedA line break

None of these throw an error. They silently emit wrong HTML. I did not notice the image problem until the day I tried to add an image and saw an exclamation mark sitting on the page.

That is the true cost of writing your own converter: nobody hands you the list of features you gave up. You find it yourself, usually after publishing.

I manage this two ways. First, I wrote down the supported subset and I stick to it when writing. Second, I don't use images on this site anyway — I don't want to deal with images of uncertain provenance — so losing img never bites. The nested-list loss I work around by flattening, and honestly the posts do not read any worse for it.

For the record, the one test that passed was inline code inside a heading. The only construct where I produce byte-identical HTML to marked is the simplest one I wrote without thinking about it. There is probably a lesson in that.

The supply chain argument is also nonsense

My second reason was "no dependencies, supply chain risk". I checked that too, and I was wrong again:

So I have no supply chain argument to make against marked. One package, its own code, pulling in nothing else. The "an npm dependency drags in hundreds of packages" fear does not apply here.

So what was the real reason

Eliminating those left exactly one, and it turns out to be strong enough.

This repository has no package.json. None. No node_modules, no lockfile, no npm ci step. The deployment pipeline is: check out the source, copy the public/ folder to the server. Two steps.

Adding marked is not adding a 484 kB library. It is adding:

  1. A package.json and a lockfile
  2. An npm ci step in CI, and its dependence on the network
  3. A dependency-update process someone has to watch
  4. A future incompatibility between a Node version and a package version

Each is small on its own. But this is a one-person studio's brochure site, maintained by one person. Every maintenance item here is an hour not spent on the game. The value of keeping a zero-dependency repository at zero dependencies shows up when you open it three years later and don't have to repair npm install before you can change a word.

So the real reason was not speed. It was preserving the absence of things.

Should you do this

Probably not. I would reduce the decision to this:

A closing caveat: these measurements come from one machine and one input. Text that is heavier on tables or inline formatting could shift the ratios. But the conclusion of this post does not rest on the speed ratio — it rests on the speed ratio not mattering.

Advertise on this blog, or work with us

MCALAB is an independent studio. For sponsorship, cross-promotion or a partnership:

ads@mcalab.com.tr

Details: Advertise & partner. For user support, see the support page.