- 前端
- 构建工具
- 开发工具
【免费下载链接】panda
🐼 Universal, Type-Safe, CSS-in-JS Framework for Design Systems ⚡️
本篇技术指南围绕 Panda 仓库中 MDX 提取适配器(pandacss_sfc容器适配器)的实测基准报告展开,涵盖时序/内存测量方法、回归验收门禁、本地复现命令,以及基准所验证的底层扫描实现原理。读完本文,你将掌握:MDX 文档中"实时样式"提取相对等价 TSX 的额外开销究竟有多大、如何在本仓库复现这些测量,以及如何用回归门禁守护该功能的性能边界。
本文主体为基准报告 design-notes/bench/mdx-3948.md,配套设计说明见 design-notes/mdx-parser.md。
背景:为什么需要 MDX 提取适配器
Panda 此前会把原始.mdx文档直接当作 TSX 传给 Oxc 处理。但 Markdown 的语法(围栏代码、frontmatter、行内代码、链接目的地等)会被误判为 JSX 或 JS 表达式,导致 Markdown 内容反过来阻碍实时样式(live styles)被提取。本仓库通过pandacss_sfc中已有的容器适配器机制,新增了 MDX 适配器,解决 issue #3948 描述的该问题;凡是已纳入include规则的.mdx文件无需额外配置即可生效。
该适配器只负责提取,不负责改写源码:MDX 的 source transform 不受支持,遇到改写请求会以bailed: true原样返回。提取与 CSS 生成相互独立,MDX 变换不在当前范围内。因此基准关注的核心指标是:在同一份 live style 内容下,MDX 提取相对等价 TSX 提取的时间与内存差异。
测量方法与测量环境
基准以本地 macOS arm64 release 构建、Rust 1.93.0 环境运行,基线提交为5a26802af,测量日期为 2026-10-05。MDX 与 TSX fixture 包含相同的 live styles;MDX 额外包含散文(prose)和围栏示例(fenced examples)。这些都是合成式的内存内 Rust 负载,不是文件系统、Node、Storybook 或 WASM 构建测量,读表时须注意适用边界。
关键方法论细节:
- 时序与分配分别测量:
--timing标志会关闭分配计数器,堆相关字段输出为null;内存画像在启用计数器的独立运行中完成。 - 中位数口径:表中延迟为 5 次进程运行的中位数。
- 采样策略:提取使用 30 次预热调用,按源大小在 20~300 个计时样本间调整(基准代码中体现为
EXTRACTION_WARMUP = 30、MIN_EXTRACTION_SAMPLES = 20、MAX_EXTRACTION_SAMPLES = 300,样本数由EXTRACTION_BYTE_BUDGET / source.len()夹取,见 bench/src/bin/mdx_extract.rs)。 - 混合构建交替顺序:每个进程 100 个样本,且交替场景顺序,以消除顺序偏差。
时序测量:MDX 相对等价 TSX 的开销
| Timing scenario | Equivalent TSX | MDX |
|---|---|---|
| 1 live style block | 4.00 µs | 5.33 µs |
| 20 live style blocks | 34.08 µs | 47.17 µs |
| 200 live style blocks | 308.46 µs | 431.08 µs |
| Changed-file update + CSS, 20 blocks | 69.52 µs | 83.05 µs |
| Fresh 200-file project, replacing 20 files with MDX | 1.349 ms | 1.379 ms |
从数据可以看出:
- 单块开销:MDX 提取每个 live style block 大约多付出 1.3 µs(1 块 5.33 vs 4.00 µs;200 块 431.08 vs 308.46 µs),增长基本线性。
- 混合项目开销:5 次进程运行的混合项目(200 个文件中 20 个替换为 MDX,即 10% MDX 占比)中位配对开销为2.37%。
- 绝对增量:大 MDX 文档 35,269 字节(等价 TSX 18,692 字节),MDX 在提取上多花费约 0.12 ms,每次 changed-file 更新多花费约 14 µs。
- 排序稳健性:独立的十次对比运行在交替运行中反转基线/特性进程顺序,普通 TSX 配对中位变化在 1/20/200 块下分别为+1.07% / +0.82% / +1.07%,且这些场景下基线与特性的分配计数、总分配字节和峰值堆完全相同。小的时间差异仍受本地环境波动影响。
内存测量:更少的峰值堆与分配字节
内存画像在独立运行中启用计数器,统计的是被跟踪的 Rust 分配,而非 RSS 或分配器页保留:
| Memory scenario | Equivalent TSX | MDX |
|---|---|---|
| Peak tracked heap, 200 blocks | 917,810 B | 579,234 B |
| Total bytes allocated, 200 blocks | 1,166,250 B | 892,582 B |
| Allocation count, 200 blocks | 3,875 | 4,510 |
| Peak additional watch heap | 110,038 B | 83,594 B |
| Retained heap change after 1,000 changed edits | 0 B | 0 B |
关键解读:
- 大 MDX fixture 相比等价 TSX 使用少 37% 的峰值堆、少 23% 的分配字节,但同时产生了更多的小分配(4,510 vs 3,875 次)。报告明确指出:这不意味着每个 MDX 文件都使用更少内存。
Retained heap change是带符号的(而非截断为 0)。- watch 验证在计时开始前会确认新颜色已生成、旧颜色已被移除(基准代码中对应
red/teal交替替换与断言,见 bench/src/bin/mdx_extract.rs)。
回归门禁(Regression Gates)
| Gate | Local result |
|---|---|
| Investigate repeatable ordinary TSX timing increases above 2%; preserve allocation counts | Within 2%, allocations unchanged |
| At most 3% added median time for a project with 10% MDX | 2.37% paired overhead |
| Below 0.6 ms and below 2× equivalent TSX for the 35 KB / 200-block MDX fixture | 0.431 ms, 1.40× TSX |
| No retained heap growth after 1,000 changed edits following warmup | No change observed |
| Dense inline JSX should avoid repeated paragraph scans | 100/1,000/4,000 tags: 0.029/0.251/0.985 ms |
| Malformed input should avoid repeated whole-document parses | 25/100/400 KB: 0.346/1.333/5.426 ms |
门禁解读:
- 10% MDX 项目的附加中位时间被约束在 3% 以内,实测 2.37%;
- 35 KB / 200 块的 MDX fixture 需低于 0.6 ms 且不超过等价 TSX 的 2 倍,实测 0.431 ms、1.40×;
- 密集行内 JSX与畸形输入两个门禁分别守护"避免重复段落扫描"与"避免重复整文档解析"两条实现路径,两者样本均呈现大致线性增长(0.029→0.251→0.985 ms;0.346→1.333→5.426 ms),但报告强调这不是对所有源形态的证明;
- 合并前仍需在 CI 与真实文档项目上确认这些门禁;WASM 可编译,但其延迟与体积未测量。
本地复现步骤
从仓库根目录执行:
cargo build -p pandacss_bench --bin mdx_extract --release --locked target/release/mdx_extract --timing target/release/mdx_extract --mixed --timing target/release/mdx_extract --watch --timing target/release/mdx_extract --dense-inline --timing target/release/mdx_extract --esm-stress --timing # Separate allocation profiles: target/release/mdx_extract target/release/mdx_extract --watch # Compare the same harness on an isolated baseline checkout: target/release/mdx_extract --baseline --timing cargo nextest run -p pandacss_sfc -p pandacss_extractor -p pandacss_compiler --locked基线对比说明:把 bench/src/bin/mdx_extract.rs 连同其mdx_extract/辅助目录(含分配计数模块 bench/src/bin/mdx_extract/allocations.rs,本文档中未列出该文件的完整路径,实际位于基准 bin 目录内)复制到5a26802af的独立检出中,用相同 release 命令构建,然后与特性运行交替执行--baseline --timing。仓库不提交任何原始测量文件。
各 CLI 标志对应的场景(见 bench/src/bin/mdx_extract.rs):
--timing:关闭分配计数,仅测时延;--mixed:200 文件项目中 10% 替换为 MDX 的交替采样构建;--watch:20 块 fixture 下 1,000 次 changed-file 更新 + CSS 编译;--dense-inline:100/1,000/4,000 个行内<Box>标签的密集单行 JSX 场景;--esm-stress:20/80/320 行的 ESM export 场景(守护模块边界与数组分隔逻辑);- 默认(不带标志):1/20/200 块的 TSX 与 MDX 提取对比,并追加动态属性与畸形输入(25/100/400 KB 未闭合文档)场景;
--baseline --timing:在独立基线检出上只跑 TSX 侧,用于配对对比。
源码级原理:基准背后验证的实现
基准并非孤立压测,它验证的正是 crates/pandacss_sfc/src/mdx.rs 中扫描器的关键路径。该模块的职责划分如下(见 design-notes/mdx-parser.md):
| Module | Responsibility |
|---|---|
mdx.rs | 按源顺序扫描文档;收集标签、属性与诊断 |
mdx/markdown.rs | 跳过 Markdown 示例与目的地;持有行内代码/图片缓存与行查找 |
mdx/javascript.rs | 用共享 lexer 找出 JavaScript 孤岛边界;校验交给 Oxc |
mdx/extraction_source.rs | 构建同偏移的 JavaScript;持有合成数组与模块分隔符 |
同偏移提取源(ExtractionSource)
extraction_source.rs 在构造时把除换行外的所有字节掩码为空格,之后按原偏移回填内容,保证字节偏移始终锚定原始源(含 UTF-8 与 CRLF)。它有唯一的尾部状态:Empty、Expression(表达式数组)或Module(模块声明)。数组在复制模块前关闭,模块与下一个数组之间用分隔符隔开;在无分号 export 后,分隔符复用已有的空白字节,最紧的情况下替换一个换行。因此诊断能对原始文档解析行列号,而不是对提取源。
表达式被放进一个合成的数组(而非包裹进额外作用域),从而保留标识符查找语义;数组围绕模块声明拆分。密集 JSX 场景(--dense-inline)之所以有专门门禁,正是因为这段合成逻辑要求顺序正确、不产生额外作用域。
Markdown 排除层
markdown.rs 实现了无需 Markdown AST 的排除:
- 行内代码:反引号 run 在首次遇到行内代码时才惰性索引,常见定界符长度用小数组,失败的搜索结果被缓存,避免对整个文档重复扫描;
- 围栏代码、frontmatter、行引用定义、链接目的地:全部被跳过,代码示例永不产出样式;
- 图片标签:失败的图片标签搜索被缓存,标签前瞻有界,避免重复后缀扫描;
- 行查找(
LineCursor):只前进读取之前未读的字节,而不是对每个标签在同一段落里反向扫描——这正是"密集行内 JSX 避免重复段落扫描"门禁守护的路径。
JavaScript 孤岛边界
javascript.rs 只定位顶层孤岛:多行 export 复用共享 JS lexer 与花括号匹配器(find_closing_brace),在顶层空行处停止;import(...)与import.meta不会等待from源。嵌入 JavaScript 内部的 JSX 保持为 JSX 交给 Oxc 处理,本适配器自身不做任何 JavaScript 解析,这也是无运行时依赖的关键。
属性与标签扫描
mdx.rs的开标签使用现有 Astro 属性区间表示(容器中立别名导出);属性表达式复用共享解析的 literal 缓存,缺失或动态的值永远不会触发文档的二次解析。引号属性通过 Oxc 现有实体表解码 XML/数字实体。基准中的动态属性场景(<Box color={unknown} ... />)即守护该路径。
Review 修复与验证范围
基准文档记录了评审修复:重复的行内标签行扫描、无效引用定义后的错误排除、初始 BOM 处理;将时序与分配计数分离、为 Markdown 缓存与提取源状态赋予显式属主、重命名共享的 Astro/MDX 属性收集,并把边界 fixture 迁移为命名测试(带内联快照与提取值断言)。
验证结论:
- 2,451 个作用域 Rust 测试通过,包括既有样式表快照无变化;原生编译器套件、工作区 clippy、格式化与 WASM 检查均通过;
- 提取边界与官方 MDX 解析器在69 个不同的聚焦用例上一致(原始 101 个用例中有 32 个重复文档),另与151 份仓库 MDX 文档一致——这建立的是这些用例的覆盖,而非完全一致性保证;
- 相关测试位于 crates/pandacss_sfc/tests/mdx/(行内边界快照记录
@mdx-js/mdx3.1.1 报告的命名 JSX 元素与 JS 调用区间);编译器测试覆盖 CSS 生成与 watch 替换。
已知局限与后续范围
已知过度提取局限:引用式图片(reference-style image)的标签未做文档级解析,其 alt 文本中的 JSX 仍可能被提取为 live component。例如![<Box color="wrong" />][image]后接[image]: /asset.png时当前会提取出wrong,而官方解析器把该标签视为图片文本。正确处理需要文档级定义解析(含代码示例之外的定义与前向引用),该责任应保留在 Markdown 层,而不是教样式收集器认识图片语法。
文档级引用式图片解析、更广泛的第三方语料、实体与插件变换语法覆盖是后续跟进项;既有 CSS 输出快照必须保持不变化。MDX 改写与 remark/rehype 插件执行不在本特性范围内(见 design-notes/mdx-parser.md 的所有权与源偏移契约)。
小结
MDX 提取适配器在无运行时依赖、不做自身 JS 解析的前提下,把 10% MDX 混合项目的额外时间控制在 2.37%(门禁 3%),大 fixture 延迟 0.431 ms、为等价 TSX 的 1.40×(门禁 0.6 ms / 2×),且 1,000 次 changed-file 更新后无保留堆增长。这些数字是本地回归判据,而非普适的延迟保证,在接入真实文档项目前,应先用本文的复现命令在 CI 与目标项目上复核门禁。
- 前端
- 构建工具
- 开发工具
【免费下载链接】panda
🐼 Universal, Type-Safe, CSS-in-JS Framework for Design Systems ⚡️
相关推荐
ChatGPT-Shortcut 离线版部署指南:企业内网零后端、数据全本地化的提示词库落地方案
ChatGPT Shortcut 离线版部署指南:企业内网零后端、数据全本地化的提示词库落地方案 本文基于 ChatGPT Shortcut 官方离线部署文档(
前端构建工具开发工具Garnet 基准测试体系解析:Resp.benchmark 工具与 BDN CI 性能回归门禁
Garnet 基准测试体系解析:Resp.benchmark 工具与 BDN CI 性能回归门禁 Garnet 仓库内置了一套完整的性能基准测试(Benchma
缓存KV存储后端Hunk 发布性能基线与回归门禁(Release Benchmark Gate)完全指南
Hunk 发布性能基线与回归门禁(Release Benchmark Gate)完全指南 导读 本篇指南围绕 Hunk 仓库中 benchmarks/relea
开发工具代码评审CLIAI 应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考