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.
| Converter | Median | Best | Installed size |
|---|---|---|---|
md.mjs (mine) | 0.88 ms | 0.76 ms | 4.8 kB, one file |
marked | 2.55 ms | 2.20 ms | 484 kB |
markdown-it | 2.63 ms | 2.28 ms | 1.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:
| Construct | What I emit | What it should be |
|---|---|---|
| Nested list | Sub-items flattened to the top level | Nested ul |
| Image syntax | The exclamation mark stays as text, the rest becomes a link | An img element |
| Autolink (a bare URL in angle brackets) | Escaped plain text | A clickable link |
| Italic inside bold | The asterisks show up on screen | Nested strong and em |
| Indented code block | An ordinary paragraph | A pre block |
| Two trailing spaces | Lines get joined | A 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:
marked: zero dependencies. One package, 12 files, 484 kB.markdown-it: 6 direct dependencies, 7 packages in total.
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:
- A
package.jsonand a lockfile - An
npm cistep in CI, and its dependence on the network - A dependency-update process someone has to watch
- 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:
- If your project already has a
package.json, installmarkedand forget this post. There is no upside to writing your own and there are 13 tests waiting to bite you. - If you want control over the output (mine wraps tables in a scrollable box and adds
relto external links automatically), you still don't need your own parser —markedsupports custom renderers and 20 lines gets you the same result. - If your repository genuinely has no dependencies and you maintain that as a principle, then yes. But do not write down "it's faster" as your reason, because it isn't. Write down "I am not letting npm into this repo." That one is honest.
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.trDetails: Advertise & partner. For user support, see the support page.