v2.0.9

Markdown 輸出比較:NanoMD 與 marked

最後更新


本頁以 NanoMD 與 marked 18.1.1 並排渲染同一份 Markdown,並比較解析速度。

差異


項目NanoMD Parse().html()marked marked.parse()
輸出結構帶 class 的元素,如 <span class="md-h1">、<span class="md-line">,由 .nanomd 內的 parser.css 套用樣式,見 HTML 輸出標準的 <h1>、<p> 等標籤,不附樣式
==mark==、H~2~O、x^2^<mark>、<sub>、<sup>保留為文字;~2~ 變成 <del>
> [!NOTE] 提示區塊帶標題的 md-alert 引用區塊一般引用區塊,保留 [!NOTE] 文字
註腳 [^1]參照連結與註腳區保留為文字
裸網址帶 target="_blank" 的連結連結
原始 HTML 的事件屬性(onload)移除保留並執行
javascript: 連結不轉成連結保留為 href
JSON ASTParse() 先回傳樹狀結構再 html(),見 AST 節點marked.lexer() 的 token 陣列

避免 XSS


Markdown 常來自使用者輸入,渲染器決定了一則留言或筆記能不能在頁面上執行腳本。NanoMD 會消毒自己的輸出;marked 不消毒輸出,需要自行把 HTML 交給 DOMPurify 等消毒函式庫處理。
輸入NanoMD未加消毒的 marked
[click](javascript:console.log('clicked'))不轉成連結,Markdown 原文以純文字顯示,沒有可點擊的目標,內容也不會消失<a href="javascript:...">,點擊即執行腳本
<img src="..." onload="...">移除 onload,保留圖片保留 onload,圖片載入時執行

只有 http(s)、mailto:、tel:、相對路徑與 #錨點 會轉成連結。下方範例中,[click](javascript:...) 在 NanoMD 那側顯示為文字,只有 marked 那側會在 log 輸出 onload ran。完整的跳脫與消毒規則見安全性。

速度


將本站的文件頁合併成一份文件,重複放大到各尺寸後,在 macOS 的 headless Chrome 155 量測轉成 HTML 的耗時。每個案例先暖機 5 次,再連續執行至少 1.5 秒取平均。
文件大小NanoMD Parse().html()marked marked.parse()marked / NanoMD
6.1 KB0.097 ms0.127 ms1.30×
42.2 KB0.87 ms1.08 ms1.24×
405.2 KB9.15 ms10.2 ms1.11×
1.00 MB23.1 ms26.3 ms1.14×
2.00 MB44.6 ms53.9 ms1.21×
4.00 MB88.3 ms101.7 ms1.15×

兩者輸出的結構不同,數字代表同一量級而非完全相同的工作量:NanoMD 另外建立 AST 並消毒 HTML,輸出也大約多 16%。

範例


同一份 Markdown,兩種渲染


左側為 NanoMD,右側為 marked。圖片的 onload 只在 marked 那側執行,並輸出到下方 log。
  1. <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:1rem">
  2. <section><h4>NanoMD</h4><div id="nano" class="nanomd"></div></section>
  3. <section><h4>marked</h4><div id="marked"></div></section>
  4. </div>
  5. <script type="text/markdown" id="md">
  6. # Release Notes
  7. **Bold**, *italic*, ~~deleted~~, ==marked==, H~2~O, x^2^ and `code`.
  8. > [!NOTE]
  9. > GitHub alert blockquote.
  10. - [x] Task done
  11. - [ ] Task pending
  12. - Nested item
  13. | Left | Center | Right |
  14. |:---|:---:|---:|
  15. | a | b | c |
  16. Footnote reference[^1] and https://nanomd.pardn.io bare URL.
  17. ~~~js
  18. console.log("hello");
  19. ~~~
  20. <img src="/static/image/dog-7128749_640.jpg" width="120" onload="console.log('onload ran')">
  21. [click](javascript:console.log('clicked'))
  22. [^1]: Footnote definition.
  23. </script>
  24. <script type="module">
  25. import { marked } from "https://cdn.jsdelivr.net/npm/marked@18.1.1/lib/marked.esm.js";
  26. const md = document.getElementById("md").textContent;
  27. document.getElementById("nano").innerHTML = MDParser.Parse(md).html();
  28. document.getElementById("marked").innerHTML = marked.parse(md);
  29. </script>
即時預覽 · @pardnchiu/nanomd@2.0.9

在你的瀏覽器量測


按下按鈕後,將範例重複放大到約 10 KB、100 KB、1 MB,各案例執行一秒,結果表格由 NanoMD 渲染。
  1. <button id="run">Run benchmark</button>
  2. <div id="result" class="nanomd"></div>
  3. <script type="text/markdown" id="md">
  4. ## Section
  5. Paragraph with **bold**, *italic*, `code`, a [link](https://nanomd.pardn.io) and ~~deleted~~ text.
  6. - Item one
  7. - Item two
  8. 1. Nested ordered
  9. 2. Another
  10. | Name | Value |
  11. |---|---:|
  12. | alpha | 1 |
  13. | beta | 2 |
  14. > Quoted line with *emphasis*.
  15. ~~~js
  16. const sum = (a, b) => a + b;
  17. ~~~
  18. </script>
  19. <script type="module">
  20. import { marked } from "https://cdn.jsdelivr.net/npm/marked@18.1.1/lib/marked.esm.js";
  21. const base = document.getElementById("md").textContent;
  22. const button = document.getElementById("run");
  23. const result = document.getElementById("result");
  24. const nano = (s) => MDParser.Parse(s).html();
  25. const mark = (s) => marked.parse(s);
  26. function bench(fn, input) {
  27. for (let i = 0; i < 10; i++) fn(input);
  28. let runs = 0;
  29. const start = performance.now();
  30. while (performance.now() - start < 1000) {
  31. fn(input);
  32. runs++;
  33. }
  34. return (performance.now() - start) / runs;
  35. }
  36. button.addEventListener("click", async () => {
  37. button.disabled = true;
  38. const rows = ["| Size | NanoMD | marked | marked / NanoMD |", "|---|---|---|---|"];
  39. for (const kb of [10, 100, 1000]) {
  40. result.innerHTML = MDParser.Parse(`Running ${kb} KB...`).html();
  41. await new Promise((r) => setTimeout(r, 50));
  42. const input = Array(Math.ceil((kb * 1024) / base.length)).fill(base).join("\n");
  43. const a = bench(nano, input);
  44. const b = bench(mark, input);
  45. rows.push(`| ${(input.length / 1024).toFixed(1)} KB | ${a.toFixed(3)} ms | ${b.toFixed(3)} ms | ${(b / a).toFixed(2)}× |`);
  46. }
  47. result.innerHTML = MDParser.Parse(rows.join("\n")).html();
  48. console.log(navigator.userAgent);
  49. button.disabled = false;
  50. });
  51. </script>
即時預覽 · @pardnchiu/nanomd@2.0.9
EN