Gatsby 大规模 Markdown 基准测试:用 markdown_id 探究按 id 索引建站的性能表现
2026/9/18 18:13:07 网站建设 项目流程

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_PAGESMAX_NUM_ROWS两个核心环境变量的作用、页面生成脚本与模板的实现原理,以及如何复现并对比「按 id 索引」与「按 slug 索引」两种建站方式在构建性能上的差异。

基准测试的定位:为什么专门做一个 "Index = id" 的变体

在 Gatsby 源码仓库的benchmarks/目录下,官方维护了一批用于度量 Gatsby 构建能力的基准项目,其中 Markdown 系列包含 markdown_id 与 markdown_slug 两个高度对称的姊妹基准。二者共享几乎相同的代码骨架,唯一的关键差异体现在 README 的第一行:

  • markdown_idMarkdown Benchmark Index = id,即构建时在 GraphQL 查询里以 Markdown 节点的id字段作为页面定位依据;
  • markdown_slugMarkdown 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 构建管线的影响,为后续的查询性能优化提供可复现的度量样本。

整个基准的工作流非常直观:

  1. 用脚本预生成一批伪随机Markdown 文件,存放在markdown-pages/目录;
  2. 通过gatsby-source-filesystem+gatsby-transformer-remark将这些文件转换为 MarkdownRemark 节点;
  3. createPages阶段查询全部节点,将节点的id注入页面context
  4. 页面模板通过markdownRemark(id: { eq: $id })精确取回单个节点并渲染 HTML;
  5. 执行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.jscreatePagesonCreateNode的 API 实现
benchmarks/markdown_id/gatsby-config.js站点与插件配置
benchmarks/markdown_id/package.jsonbench/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 文件总数,默认值为1000parseInt(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.md1.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中同时携带了idprevious/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-imagesmaxWidth: 590)、gatsby-remark-responsive-iframegatsby-remark-prismjsgatsby-remark-copy-linked-filesgatsby-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及配套插件版本,同时引入fakergray-matter(经 md.tpl.js 间接使用)、del-clits-nodetypescript等工具链,其中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 bench

postinstall会替你完成页面预生成,yarn bench随后重新生成并构建。若想复用同一批页面、避免重新生成带来的数据波动,改用:

yarn benchnb

README 特别说明:无论哪种方式,中途使用gatsby clean都是安全的,它只清理.cachepublic,不影响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

各步骤含义:

  1. yarn安装依赖。README 指出:如果你打算配合gatsby-dev本地开发 Gatsby 核心,建议用yarn;否则yarnnpm install差异不大;
  2. rm -r markdown-pages清掉旧数据,保证从零生成;
  3. NUM_PAGES=2000 node md.generate.js以环境变量指定规模执行生成脚本;
  4. gatsby clean清空缓存,避免旧缓存污染构建结果;
  5. 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_idmarkdown_slug
README 声明id查询,"at the time of writing" 更快slug查询,相对更慢
页面 contextid+slugid+slug
模板查询markdownRemark(id: { eq: $id })markdownRemark(fields: { slug: { eq: $slug } })
其余配置与 slug 变体一致与 id 变体一致

建议的实验流程:

  1. markdown_idmarkdown_slug下各执行一次yarn bench,控制相同的NUM_PAGES(如 2000);
  2. 记录两次gatsby build的总耗时、峰值内存与public/产物规模;
  3. 如需排除冷启动噪声,可改用yarn benchnb重复 2~3 次取中间值;
  4. 对比两个基准的createPages阶段耗时差异——该阶段是idslug索引路径最直接的分水岭。

需要强调:README 中「按 id 更快」是文档撰写时的结论,且基准锁定在 Gatsby 2.x(package.jsongatsby ^2.19.5)。Gatsby 数据层在此后多个版本中持续演进,若要得出当前版本的结论,务必以本仓库实际环境重新跑一遍对照实验为准,不应将历史结论直接外推。

小结:这个基准能带给你什么

从使用者的角度,markdown_id至少提供了三重价值:

  1. 可复现的规模化基准:一条yarn bench命令即可在本地造出 2000 个含 frontmatter、表格、段落、图片插件的"类真实" Markdown 页面,完整覆盖source-filesystem → transformer-remark → createPages → 模板查询 → HTML 渲染的整条 Gatsby 构建链路;
  2. 索引策略的对照样本:与markdown_slug构成天然对照,使「按主键 id 查询 vs 按派生字段 slug 查询」的性能差异可以被量化测量,是研究 Gatsby 查询层性能的现成实验台;
  3. 标准化的目录骨架:生成脚本、模板、配置、脚本命令分层清晰,无论是扩展数据形态(改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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询