Gatsby 大规模 Markdown 基准测试:用 markdown_id 探究按 id 索引建站的性能表现
【免费下载链接】gatsbyReact-based framework with performance, scalability, and security built in.项目地址: https://gitcode.com/gh_mirrors/ga/gatsby
本篇技术指南围绕 Gatsby 官方基准测试项目 benchmarks/markdown_id 展开,介绍一个以gatsby-starter-blog为基础、通过程序化生成海量随机 Markdown 页面、并在查询阶段以节点id(而非slug)作为页面索引键的完整基准方案。读完本文,你将掌握该基准的目录结构与运行流程、NUM_PAGES与MAX_NUM_ROWS两个核心环境变量的作用、页面生成脚本与模板的实现原理,以及如何复现并对比「按 id 索引」与「按 slug 索引」两种建站方式在构建性能上的差异。
基准测试的定位:为什么专门做一个 "Index = id" 的变体
在 Gatsby 源码仓库的benchmarks/目录下,官方维护了一批用于度量 Gatsby 构建能力的基准项目,其中 Markdown 系列包含 markdown_id 与 markdown_slug 两个高度对称的姊妹基准。二者共享几乎相同的代码骨架,唯一的关键差异体现在 README 的第一行:
markdown_id:Markdown Benchmark Index = id,即构建时在 GraphQL 查询里以 Markdown 节点的id字段作为页面定位依据;markdown_slug:Markdown Benchmark Index = slug,即同样场景下改用从文件路径推导出的slug字段。
该基准的 README 明确指出(原文:"This particular markdown benchmark will query pages by theirid, which is (at the time of writing) faster than indexing pages by theirslug"):截至文档撰写时,按id查询页面比按slug索引更快。这正是该基准存在的意义——通过可控的页面规模,量化不同索引键对 Gatsby 构建管线的影响,为后续的查询性能优化提供可复现的度量样本。
整个基准的工作流非常直观:
- 用脚本预生成一批伪随机Markdown 文件,存放在
markdown-pages/目录; - 通过
gatsby-source-filesystem+gatsby-transformer-remark将这些文件转换为 MarkdownRemark 节点; - 在
createPages阶段查询全部节点,将节点的id注入页面context; - 页面模板通过
markdownRemark(id: { eq: $id })精确取回单个节点并渲染 HTML; - 执行
gatsby build,统计整条管线的耗时与资源占用。
基准目录结构一览
先完整认识该基准的组成,以下文件均位于仓库根目录下:
| 路径 | 职责 |
|---|---|
| benchmarks/markdown_id/README.md | 基准说明与使用文档(本文的主体) |
| benchmarks/markdown_id/md.generate.js | 页面批量生成脚本,受环境变量控制 |
| benchmarks/markdown_id/md.tpl.js | 单页 Markdown 模板,基于 faker 生成内容 |
| benchmarks/markdown_id/gatsby-node.js | createPages与onCreateNode的 API 实现 |
| benchmarks/markdown_id/gatsby-config.js | 站点与插件配置 |
| benchmarks/markdown_id/package.json | bench/benchnb等核心脚本定义 |
| benchmarks/markdown_id/src/templates/blog-post.js | 文章页模板,内含按 id 查询的 pageQuery |
| benchmarks/markdown_id/src/pages/index.js | 首页模板,展示全部文章列表 |
| benchmarks/markdown_id/src/components/ | bio、layout、seo 等通用组件 |
| benchmarks/markdown_id/scripts/data-update.ts | 预留的数据更新脚本(当前为 noop 占位) |
其中markdown-pages/目录是运行时由生成脚本创建的产物,不出现在源码树中,安装阶段即会被自动生成(见下文postinstall)。
页面生成原理:md.generate.js 与 md.tpl.js
生成脚本的入口逻辑
md.generate.js 是整个基准的数据源头,它通过两个环境变量控制数据规模:
NUM_PAGES:要生成的 Markdown 文件总数,默认值为1000(parseInt(process.env.NUM_PAGES || 1000)),README 与package.json中的脚本则统一按2000作为标准运行量;MAX_NUM_ROWS:每个页面正文中 API 表格的最大行数,默认值为25。
脚本对这两个变量做了防御性校验:如果解析后不是正整数(typeof !== 'number' || !Number.isInteger(...) || <= 0),会直接抛出错误并给出明确提示,避免在异常输入下静默产出脏数据。生成过程带进度输出,每完成 10% 打印一次百分比,便于在NUM_PAGES较大时观察进度。
核心生成逻辑非常简单直观:
const root = `markdown-pages` console.time(`Generated in`) let p10 = Math.round(NUM_PAGES / 10) for (let step = 0; step < NUM_PAGES; step++) { if (step > 0 && step % p10 === 0) console.log(`--> ` + (step / p10) * 10 + `%`) let page = template(step) let where = path.join(root, step + `.md`) fs.writeFileSync(where, page) } console.timeEnd(`Generated in`)即循环NUM_PAGES次,每次调用md.tpl.js导出的template(step)生成一页内容,并以0.md、1.md……递增命名写入markdown-pages/。如果目录不存在会先创建(fs.mkdirSync(root, { recursive: true }))。
单页模板:faker + gray-matter 构造真实感内容
md.tpl.js 使用faker生成随机文本,用gray-matter序列化 frontmatter,构造出「看起来像真实博客文章」的页面,以逼近真实站点的内容形态与 Markdown 解析成本:
module.exports = index => `${matter .stringify(``, { title: faker.lorem.sentence(), description: faker.lorem.sentence(), path: `/${faker.helpers.slugify(faker.lorem.sentence())}`, date: faker.date.recent(1000).toISOString().slice(0, 10), tags: `[${faker.random .words(3) .split(` `) .map(w => `"${w}"`) .join(`, `)}]`, }) .trim()} ## Page #${index} : ${faker.random.words(4)} ### API ${new Array(faker.random.number(MAX_NUM_ROWS)) .fill(undefined) .map(() => ` |${faker.lorem.word()}|${faker.lorem.sentence()}|${faker.random.boolean()}| `.trim() ) .join(`\n`)} ### More Detail ${faker.lorem.paragraphs()} `每页由四部分构成:
- frontmatter:随机 title、description、path、date(近 1000 天内的日期,格式
YYYY-MM-DD)、tags(3 个随机单词标签)。path字段配合createFilePath用于最终页面路由; - 二级标题:
## Page #N : ...,用序号标识页码,便于人工核对; - API 表格:
0 ~ MAX_NUM_ROWS行随机 Markdown 表格(单词 / 句子 / 布尔值),模拟含数据表格的技术类文章; - 正文段落:
faker.lorem.paragraphs()生成若干随机段落,充实 HTML 体积。
需要说明的是,md.tpl.js中声明的MAX_NUM_ROWS是独立读取的(默认 25),与md.generate.js中的同名变量保持一致即可获得预期的表格行数。
关键环节:如何在 GraphQL 中按 id 索引页面
onCreateNode:为节点推导 slug
gatsby-node.js 中的onCreateNode钩子对每个MarkdownRemark节点调用gatsby-source-filesystem提供的createFilePath({ node, getNode }),把文件路径转换为 slug 并写入fields.slug:
exports.onCreateNode = ({ node, actions, getNode }) => { const { createNodeField } = actions if (node.internal.type === `MarkdownRemark`) { const value = createFilePath({ node, getNode }) createNodeField({ name: `slug`, node, value, }) } }注意:slug 仍被生成并被首页列表使用,但文章页的查询不走 slug,而是走 id——这正是本基准与 slug 变体的分水岭。
createPages:查询全部文章并把 id 注入 context
createPages先通过 GraphQL 一次性取回所有文章(按 frontmatter 日期倒序),随后为每一篇调用createPage:
posts.forEach((post, index) => { const previous = index === posts.length - 1 ? null : posts[index + 1].node const next = index === 0 ? null : posts[index - 1].node createPage({ path: post.node.fields.slug, component: blogPost, context: { slug: post.node.fields.slug, id: post.node.id, previous, next, }, }) })每页的path依然来自fields.slug(保证 URL 与真实站点一致),但在context中同时携带了id。previous/next用于文章底部的上一篇 / 下一篇导航,与 starter-blog 的行为保持一致。
页面模板:markdownRemark(id: { eq: $id })
文章模板 src/templates/blog-post.js 的pageQuery是整条基准链路的落点:
query BlogPostById($id: String!) { site { siteMetadata { title } } markdownRemark(id: { eq: $id }) { id excerpt(pruneLength: 160) html frontmatter { title date(formatString: "MMMM DD, YYYY") description } } }查询通过$id(来自pageContext.id)用markdownRemark(id: { eq: $id })精确定位节点。Gatsby 的id是每个节点在数据层的唯一主键,查询时走索引查找;而 slug 需要先经createFilePath推导、再经字段过滤匹配,多一层间接性——这正是 README 中「按 id 查询更快」结论的工程背景。作为对照,markdown_slug 基准 的文章模板查询改为markdownRemark(fields: { slug: { eq: $slug } }),其余部分几乎完全一致,非常适合做 A/B 对比实验。
首页 src/pages/index.js 则与文章页不同,它一次性查询全部文章的 slug、日期、摘要用于列表渲染(allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC })),这部分逻辑在两个基准中完全一致。
站点配置与依赖:gatsby-config.js / package.json
数据源与插件配置
gatsby-config.js 完整复用了 starter-blog 的插件栈,核心配置如下:
- gatsby-source-filesystem:第一个实例指向
${__dirname}/markdown-pages(即生成脚本产出的目录),第二个实例指向content/assets存放站点静态资源; - gatsby-transformer-remark:挂载
gatsby-remark-images(maxWidth: 590)、gatsby-remark-responsive-iframe、gatsby-remark-prismjs、gatsby-remark-copy-linked-files、gatsby-remark-smartypants等 remark 插件,确保生成的页面包含完整的 Markdown 处理链路——这也是基准要计入的成本之一; - gatsby-plugin-benchmark-reporting:基准专用的上报插件,由仓库根目录下 packages/gatsby-plugin-benchmark-reporting 提供,用于在自动化运行中汇总构建指标;
- 其余为 sharp 图片处理、Google Analytics(
trackingId: 'do-not-track'占位)、manifest、offline、react-helmet、typography 等 starter 标配插件。
依赖方面,package.json 锁定了gatsby ^2.19.5及配套插件版本,同时引入faker、gray-matter(经 md.tpl.js 间接使用)、del-cli、ts-node、typescript等工具链,其中ts-node/typescript服务于data-update脚本(当前为占位 noop)。
脚本定义:bench 与 benchnb 的差异
package.json中定义了四个与本基准直接相关的脚本:
"bench": "rm -r markdown-pages; NUM_PAGES=${NUM_PAGES:-2000} node md.generate.js; gatsby clean; node --max_old_space_size=2000 node_modules/.bin/gatsby build", "benchnb": "gatsby clean; node --max_old_space_size=2000 node_modules/.bin/gatsby build", "postinstall": "del-cli markdown-pages && gatsby clean && NUM_PAGES=${NUM_PAGES:-2000} node md.generate.js"三者的分工是:
yarn bench:完整流程——先删除旧的markdown-pages,重新生成NUM_PAGES(默认 2000)个页面,gatsby clean清理缓存,再以 2000MB 最大老生代堆内存执行生产构建;yarn benchnb(no-build 数据复用):只做gatsby clean与构建,不重新生成页面。当你希望用同一批页面重复测量构建、对比不同改动的影响时使用它,保证数据一致、可复现;postinstall:安装依赖后自动清理并预生成 2000 个页面,使首次yarn bench不必等待生成。
此外build/clean/develop/serve均为常规 Gatsby 命令,data-update调用ts-node scripts/data-update.ts。
完整运行步骤:从零复现一次基准
README 给出了两条路径——一键式与分步式,均可直接照抄运行(需 Node 8+ 环境,README 以nvm use 8为例提示切换 Node 版本;仓库当前以 Gatsby 2.x 为基准)。
方式一:一键运行(推荐)
yarn yarn benchpostinstall会替你完成页面预生成,yarn bench随后重新生成并构建。若想复用同一批页面、避免重新生成带来的数据波动,改用:
yarn benchnbREADME 特别说明:无论哪种方式,中途使用gatsby clean都是安全的,它只清理.cache与public,不影响markdown-pages中的数据。
方式二:分步执行(适合调试与深入观察)
# 需要 Node 8+ # nvm use 8 yarn rm -r markdown-pages NUM_PAGES=2000 node md.generate.js gatsby clean node --max_old_space_size=2000 node_modules/.bin/gatsby build各步骤含义:
yarn安装依赖。README 指出:如果你打算配合gatsby-dev本地开发 Gatsby 核心,建议用yarn;否则yarn与npm install差异不大;rm -r markdown-pages清掉旧数据,保证从零生成;NUM_PAGES=2000 node md.generate.js以环境变量指定规模执行生成脚本;gatsby clean清空缓存,避免旧缓存污染构建结果;node --max_old_space_size=2000 node_modules/.bin/gatsby build执行生产构建。最后一步在页面数量足够小的时候可以直接用gatsby build;--max_old_space_size=2000的作用是提高 V8 老生代内存上限——README 明确写道这是"for a larger number of pages"(针对大页面量)所必需的,页面量大时 Node 默认堆内存可能不足。
调整数据规模
页面数量由NUM_PAGES环境变量控制(README 示例为 400,标准脚本默认 2000)。md.generate.js在未显式设置该变量时会打印提示:Set 'NUM_PAGES=200' to change the volume(该提示文案中的 200 为示例,实际按需传值即可)。做小规模冒烟测试可用NUM_PAGES=50,观察构建耗时的线性增长时则建议 500 / 1000 / 2000 逐档递增。
与 markdown_slug 对比:如何做一次有意义的性能对照实验
markdown_id的对照实验对象是同目录下的 markdown_slug,两个项目除查询字段外结构完全同构:
| 维度 | markdown_id | markdown_slug |
|---|---|---|
| README 声明 | 按id查询,"at the time of writing" 更快 | 按slug查询,相对更慢 |
| 页面 context | id+slug | id+slug |
| 模板查询 | markdownRemark(id: { eq: $id }) | markdownRemark(fields: { slug: { eq: $slug } }) |
| 其余配置 | 与 slug 变体一致 | 与 id 变体一致 |
建议的实验流程:
- 在
markdown_id与markdown_slug下各执行一次yarn bench,控制相同的NUM_PAGES(如 2000); - 记录两次
gatsby build的总耗时、峰值内存与public/产物规模; - 如需排除冷启动噪声,可改用
yarn benchnb重复 2~3 次取中间值; - 对比两个基准的
createPages阶段耗时差异——该阶段是id与slug索引路径最直接的分水岭。
需要强调:README 中「按 id 更快」是文档撰写时的结论,且基准锁定在 Gatsby 2.x(package.json中gatsby ^2.19.5)。Gatsby 数据层在此后多个版本中持续演进,若要得出当前版本的结论,务必以本仓库实际环境重新跑一遍对照实验为准,不应将历史结论直接外推。
小结:这个基准能带给你什么
从使用者的角度,markdown_id至少提供了三重价值:
- 可复现的规模化基准:一条
yarn bench命令即可在本地造出 2000 个含 frontmatter、表格、段落、图片插件的"类真实" Markdown 页面,完整覆盖source-filesystem → transformer-remark → createPages → 模板查询 → HTML 渲染的整条 Gatsby 构建链路; - 索引策略的对照样本:与
markdown_slug构成天然对照,使「按主键 id 查询 vs 按派生字段 slug 查询」的性能差异可以被量化测量,是研究 Gatsby 查询层性能的现成实验台; - 标准化的目录骨架:生成脚本、模板、配置、脚本命令分层清晰,无论是扩展数据形态(改
md.tpl.js)还是调整规模(改NUM_PAGES),成本都极低,完全可以作为自建性能测试站点的起点。
如果你对构建性能指标的上报机制感兴趣,还可以进一步阅读本仓库的 gatsby-plugin-benchmark-reporting 插件源码,了解基准数据如何被采集与记录;而markdown_slug变体的完整实现则在 benchmarks/markdown_slug 中可随时对照。
【免费下载链接】gatsbyReact-based framework with performance, scalability, and security built in.项目地址: https://gitcode.com/gh_mirrors/ga/gatsby
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考