Bun.markdownを試してみた

2026-03-24T15:10:58.397Z

5segです。
このブログはMarkdownで書いてて、パーサーにMarked.jsを使っているのですが、どうやらBunにはマークダウンのパーサーがあるらしいです。

https://bun.com/docs/runtime/markdown

じゃあMarkedいらないじゃんとなりそうですが、まだ不安定のようで、これから仕様が変わるかもしれません。
とはいえ全然使えそうな気もするので、試してみました。

実験するにあたって書いたコードはここにあります。私が理解できて、動けばいいので構造は汚いです。
また、コードも改善の余地はあると思います。後半で比較を行っていますが、timeによる計測であることと、記法次第で結果が大きく揺らぐことに注意してください。

使い方

const md = await Bun.file("./foo.md").text();
const html = Bun.markdown.html(md);
await Bun.write("./out.html", html);

以上。あほほど簡単です。
とはいえハイライターがついていないので、このままだとコードが見にくいです。なんなら背景すらないですし。
なのでshikiをがっちゃんこします。するとこうなります:

let langs: string[] = [];
const renderCallbacks: markdown.RenderCallbacks = {
  heading: (children, { level }) => `<h${level}>${children}</h${level}>`,
  paragraph: (children) => `<p>${children}</p>`,
  strong: (children) => `<strong>${children}</strong>`,
  emphasis: (children) => `<em>${children}</em>`,
  codespan: (children) => `<code>${children}</code>`,
  code: (children, meta) => {
    const lang = meta?.language ?? "text";
    if (!langs.includes(lang)) langs.push(lang);
    return `<pre><code class="shiki ${lang}">${Bun.escapeHTML(children)}</code></pre>`;
  },
  link: (children, { href, title }) =>
    title
      ? `<a href="${href}" title="${title}">${children}</a>`
      : `<a href="${href}">${children}</a>`,
  image: (children, { src, title }) =>
    title
      ? `<img src="${src}" alt="${children}" title="${title}" />`
      : `<img src="${src}" alt="${children}" />`,
  list: (children, { ordered, start }) =>
    ordered ? `<ol start="${start}">${children}</ol>` : `<ul>${children}</ul>`,
  listItem: (children) => `<li>${children}</li>`,
  blockquote: (children) => `<blockquote>${children}</blockquote>`,
  hr: () => `<hr />`,
  strikethrough: (children) => `<del>${children}</del>`,
  table: (children) => `<table>${children}</table>`,
  thead: (children) => `<thead>${children}</thead>`,
  tbody: (children) => `<tbody>${children}</tbody>`,
  tr: (children) => `<tr>${children}</tr>`,
  th: (children) => `<th>${children}</th>`,
  td: (children) => `<td>${children}</td>`,
};
const customized = await highlight(
  Bun.markdown.render(md, renderCallbacks),
  langs,
);
await Bun.write("./out-customized.html", customized);

記述量が一気に増えました。codeの部分だけいじくり回してますが、それ以外はBunがベンチマーク用に使ってるのであろうコードから拝借しています。
defaultRenderCallbacksとか用意してくれればいいんですけどね。それか、Bunがデフォルトでハイライターを搭載してくれれば...
とりあえずパーサーはこんな感じで、ハイライターはこう:

export const highlight = async (html: string, langs: string[]) => {
  const hightlighter = await createHighlighter({/* ... */});
  let lang: string;
  const rewriter = new HTMLRewriter().on("code.shiki", {
    element(elem) {
      lang = elem.getAttribute("class")!.split(" ")[1]!;
    },
    text(text) {
      const code = text.text
        .replaceAll("&quot", '"')
        // ...
        .replaceAll("&gt;", ">");
      text.replace(
        (() =>
          hightlighter.codeToHtml(code, {/* ... */}))(),
        { html: true },
      );
    },
  });
  return rewriter.transform(html);
};

ちょっと端折ってます。
shiki建てて、BunのHTMLRewriterで一致箇所をハイライトされたコードに置き換えるだけのシンプルなコードです。

速度比較

16KBのsample.mdtime bun run {name}.tsした結果です。
index.tsはBun、with-marked.tsはMarkedです。

with shiki

NameTime
index.ts1.37s
with-marked.ts1.46s

Markedはパースとハイライトを同時に行っているのに対し、Bunはパース → ハイライトという2ステップを踏んでいます。しかしそのハンデがある上で約0.1秒の高速化に成功しています。素晴らしい!

without shiki

純粋に、Markdown → HTML の変換で勝負した場合の結果です。

NameTime
index.ts0.01s
with-marked.ts0.24s

圧勝です。Bun最強。

デカめのも試す

サンプルファイルで遊んでみました。
5MBぐらいのサンプルがあったのでやってみたら、Bunは約1.7秒で完了。Markedの方は何分経っても終わらないのでやめました。
両方結果を確認できたのは1MBのサンプルで、Bunは0.02秒、Markedは0.38秒でした。

結論

十分に使えるでしょう。
今後仕様が変わるかもしれませんが、その時はその時でなんとかすればいいです。

それでは。


記事一覧 ↩️