ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

TanStack Router 客户端导航 CPU 基准:benchmarks/client-nav 的跨框架确定性基准设计

TanStack Router 客户端导航 CPU 基准:benchmarks/client-nav 的跨框架确定性基准设计 TanStack Router 客户端导航 CPU 基准benchmarks/client-nav 的跨框架确定性基准设计【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/routerbenchmarks/client-nav是 TanStack Router 仓库中面向客户端路由核心能力的 CPU 基准套件在 jsdom 环境内对tanstack/react-router、tanstack/solid-router、tanstack/vue-router三个框架适配层的生产构建进行混合导航压测并通过 CodSpeed 在 CI 中持续跟踪回归。读懂这套基准你就能掌握如何为路由库设计可归因、可复现的客户端性能基准的完整方法论从场景拆分、确定性约定到 harness 同步机制与 Nx/Vitest/CodSpeed 的工程接线。基准的适用范围与定位该基准只覆盖**独立客户端路由standalone client router**部分即路由在纯客户端完成导航、loader 分发、参数与 search 解析等工作的开销。TanStack Start 的客户端侧服务端渲染文档的水合、流式 payload 消费、客户端发起 server-function 调用等不在覆盖范围内Start 的服务端侧则由另一个基准目录 benchmarks/ssr 单独覆盖。这一范围划分在 README 开头的 Scope 说明中明确界定阅读基准结果时需要注意这一前提它衡量的是客户端路由引擎本身的每导航成本而不是全栈框架的端到端延迟。基准的运行环境是 jsdomNode 中的 DOM 模拟针对的是真实应用的 production 构建产物应用先被 Vite 打包为dist/app.js基准文件再 import 这个构建后的 bundle因此测到的是生产包构建 生产 JSX 输出的开销而非开发态的 transform 开销。CI 中由 CodSpeed 以 simulation 模式跟踪。目录结构与场景应用布局套件顶层由三个框架基线应用与共享配置组成react/、solid/、vue/— 基线基准混合导航循环 各自的 Vitest 配置vitest.react.config.ts、vitest.solid.config.ts、vitest.vue.config.ts— 按框架聚合的配置先跑基线、再跑各场景项目。例如 React 的聚合配置只有一行 projects 声明projects: [./react/vite.config.ts, ./scenarios/*/react/vite.config.ts]即把基线与全部 React 场景纳进同一个 Vitest projects 运行scenarios/harness.ts — 共享场景运行器挂载、link 点击步骤、onRendered同步scenarios/scenario/shared.ts— 与框架无关的场景定义工作负载数据、步骤序列、断言、bench 选项scenarios/scenario/framework/— 相互隔离的场景应用。每个场景应用是一个完整的小型 Vite 应用布局如下与常规用户应用一致使用tanstack/router-plugin的文件路由 生成的routeTree.gen.tsscenarios/scenario/framework/ vite.config.ts speed.bench.ts speed.flame.ts setup.ts project.json tsconfig.json src/ main.tsx routeTree.gen.ts routes/以 scenarios/loaders/react/vite.config.ts 为例场景配置注册了tanstackRouter插件target: react从src/routes生成src/routeTree.gen.ts构建产物为dist/app.jsminify: false测试部分指定environment: jsdom并复用根目录的 vitest.setup.ts。这种一个场景一个独立应用的设计是刻意为之让每个场景的路由树规模与 router 配置彼此隔离一个场景的变化不会串扰另一个场景的数值而基线应用与 bench 名称保持稳定保证 CodSpeed 的历史数据连续性。场景职责矩阵每个场景隔离一项客户端职责套件的核心思路是每个场景隔离一项客户端职责使基准数值的变化可以归因到具体的功能区域。以下是 README 中的完整场景矩阵场景隔离的客户端职责react/、solid/、vue/基线应用混合导航循环path 参数、search 参数、route context以及useParams/useSearch/useLoaderData的 selector 订阅async-pipeline路由器的异步管线用计数的 0ms 定时器跳数timer hops驱动异步 loadertransition 持有的导航、异步beforeLoadcontext、并行嵌套异步 loader。组件级Await/Suspense 被排除在外React 19 对 Suspense 揭示有约 300ms 的墙钟节流天然不可确定无法用于基准control-flowloader 抛出的redirect含两跳链式跳转、带notFoundComponent的notFound()、带errorComponent的 loader 错误以及恢复导航时的边界重置head每次导航的HeadContent工作嵌套路由head()求值、跨匹配项的 title/meta/link 去重、导航期间的 head 标签 DOM 更新historyhistory 的 push/replace/back/forward 遍历、location masking、已注册但从不阻塞的useBlocker以及useCanGoBack/useLocation订阅links约 200 个已挂载Link的每导航成本link prop 构建、activeOptions各变体下的 active 状态重算、activeProps切换以及useMatchRoute探测刻意避开MatchRoute组件从源码结构看vue-router 的实现每次渲染会泄漏一个订阅因此基准改用 hook 形式loaders客户端 loader 分发始终过期的重跑staleTime: 0、缓存回访由invalidate步保证每圈重跑一次、loaderDeps键控缓存、router.invalidate()、useLoaderDataselectormount冷启动createRouter路由树处理 首次渲染 初始router.load() 卸载每次挂载都使用全新 router。是唯一测量路由创建成本的场景nested-params深度嵌套8 层动态段每层params.parse/stringify、跨匹配项的beforeLoadcontext 累积、每层useParams/useRouteContext订阅。参数值包含需要百分号编码的字符route-tree-scale同样如此使每次导航都会走到 segment 编解码路径preload来自 hover 事件的 intent 预加载、编程式router.preloadRoute、确定性的预加载缓存行为defaultPreloadStaleTime: 0以及 commit 时的缓存维护rewrites组合式客户端 location 重写routerbasepath加上一对 locale 输入/输出 rewrite在每次 href 构建与 location 解析时运行SSRrewrites场景的客户端对应物route-tree-scale在约 40 条路由的宽树上做路由匹配与 link 目标解析混合静态、动态、前缀参数、splat、pathless-layout 与 route-group 路径并开启autoCodeSplitting使导航还需解析懒加载路由块search-paramsvalidateSearch执行、search middlewaresretainSearchParams/stripSearchParams、函数式 search updater、结构性共享structural sharing与useSearchselector 订阅这套矩阵覆盖了客户端路由引擎的主要工作区匹配与解析、数据管线、head/history 副作用、链接渲染与预加载、路由树规模。基准约定让数值可归因的工程纪律README 的 Conventions 一节定义了让基准可信的一整套约定这些约定在 harness 与基线代码中都有对应实现生产构建测量应用以NODE_ENVproductionminify: false构建为dist/app.js基准 import 构建产物测量的是生产包构建与生产 JSX 输出而非开发转换。minify: false的选择是为了保留可读的函数名/结构以便火焰图分析。行为贴近真实用户应用导航通过向真实 anchor 元素派发Link点击事件完成除非场景专门测量命令式 APIscrollRestoration启用。固定循环步骤 逐步同步每次基准迭代推进一段固定的环形步骤序列每个步骤都等待路由器的onRendered事件渲染工作被完整计入、步骤之间不会重叠。相邻两步不得指向同一 location同 location 点击不会触发onRendered且序列最终回到初始路由。热身圈 可观测断言setup 阶段跑完一整圈步骤作为热身并对每一步的可观测输出做断言如document.title。这样某个场景悄悄停止干活的情况会以失败暴露而不是报告一个虚快的时间。确定性不使用墙钟定时器异步工作用已解决的 Promise 或计数的setTimeout(0)跳数表达staleTime/gcTime只取0或等效无穷大被测循环内禁用Math.random/Date.now。scrollRestoration使用getScrollRestorationKey: (location) location.pathname使缓存保持有界默认随机键会让缓存随 push 导航无限增长。mount场景则完全关闭scrollRestoration并在卸载时销毁 history —— 这两者都会注册页面级全局状态若不处理会在它的挂载/卸载循环间泄漏。jsdom history 特性只有 push 的圈次会让 jsdom 的 session-history 条目列表持续增长jsdom 从不裁剪当前索引之前的条目由于单步成本是 O(1) 不影响计时只有history场景需要深度静止的圈次并且它确实提供了这样的圈次。校准目标为每个场景选择单迭代步数使vitest bench一次运行大致落在 8 到 30 秒之间——足够长以平均化波动又足够短以适合 CI。关于 history 模式还有一处值得注意的实现细节从 harness.ts 的注释看浏览器 history 仅保留给显式测量 history 遍历的场景history场景historyMode: browser其余场景默认使用全新的 memory history。README 中路由使用默认浏览器 history的表述应理解为行为模式模拟真实浏览器而实际接线以 harness 源码为准——这是阅读该目录时文档与源码的一处偏差。共享运行器 harness.ts 的实现剖析scenarios/harness.ts 是所有场景共用的执行引擎暴露createScenarioSetup与createMountLoopSetup两个入口。它的设计要点值得逐条看步骤类型系统ScenarioStep。步骤分为两类改变路由器 location 的步骤click/navigate/back/forward/go通过onRendered事件同步做后台工作的步骤hover/preload/invalidate通过固定数量的 0ms 定时器跳数同步保持循环确定性。其中hover步骤只派发单个mouseover事件——注释解释了跨框架的等价性React 从mouseover合成mouseEnterSolid/Vue 则挂原生mouseover预加载处理器因此单事件派发在三个适配器中都恰好触发一次 intent 预加载。计步器用setImmediate而非setTimeout(0)。timerHop()的实现是一个setImmediatePromise。源码注释给出了量化动机两者都确定性地让出事件循环但一次 timer 跳大约需要 3–4 次系统调用timerfd epoll而 immediate 只需约 1 次CodSpeed 从测量中排除系统调用时间但超出阈值时会不一致地导致跳数多的基准被标记为 skipped所以循环必须保持系统调用精简。零延迟超时重定向patchZeroDelayTimeouts。jsdom 通过两层嵌套的window.setTimeout(0)任务投递 history 遍历事件而 Node 会把零延迟定时器钳制为 1ms 墙钟时间在事件循环无其他任务时循环会阻塞在epoll_wait上直到定时器到期这段阻塞墙钟会被记录为高方差跨运行 5–26ms的系统调用时间足以让 CodSpeed 对 history 基准发出 skip 警告。harness 因此在基准期间把window.setTimeout的零延迟调用重写为setImmediate负数 id 避免与真实 jsdom 定时器 id 冲突非零延迟原样放行——因为被测代码本就约定禁用真实延迟定时器零延迟超时没有 immediate 无法满足的排序语义。滚动恢复监听器捕获captureScrollRestorationListeners。滚动恢复会安装页面生命周期的监听器document上的scroll、globalThis上的pagehide而路由器没有公开的 dispose 接口。harness 在 router 创建期间拦截这些addEventListener调用并记录下来在 teardown 时逐一removeEventListener使按调用创建的 router 在拆卸后不再可达——这是防止挂载循环场景产生监听器泄漏的关键手段。热身圈即校验圈。before()中挂载应用、订阅onRendered、等待router.load()、用waitForRequiredLink确认所有步骤涉及的 link 均已渲染然后跑一整圈步骤并调用assertAfterStep做断言最后finishBatch()固定 4 个跳数收尾把状态定格回初始路由再进入测量。以 scenarios/loaders/shared.ts 为例它定义了 8 步点击序列fresh/cached/deps 六条 link invalidatego-home并为每步准备了精确的标记文本断言如depsMarkerText(2)任何一步输出不符预期都会立即抛错——这正是场景悄悄停工会失败而非报快约定的落地。冷启动运行器createMountLoopSetup。mount场景不复用上面的循环器而是每个 tick 新建容器与 memory history、挂载应用含createRouter、等待初始渲染提交以readyTestId出现为准、执行可选断言然后在finally中卸载、销毁 history、移除容器。这使createRouter的路由树处理成本被完整计入每次迭代。基线应用混合导航循环的具体形态基线应用如 react/app.tsx展示了一个真实用户应用长什么样。React 基线路由树为根路由10 个useParams订阅 10 个useSearch订阅 4 组 link 面板、/items/$id带自定义params.parse/stringify与onEnter/onStay/onLeavenoop 钩子、其子路由details、/searchvalidateSearch归一化 search middleware loaderDeps键控 loader staleTime/gcTime60s与/ctx/$idbeforeLoad产生 context。各页面内再叠加 6 个useParams/useSearch/useLoaderData/useLoaderDeps/useRouteContext订阅组件。所有订阅组件都调用runPerfSelectorComputation——一个固定的 40 轮线性同余生成器循环——把 selector 计算变成一个非平凡但完全确定的 CPU 工作负载。link 面板刻意覆盖activeOptions的变体exact、includeSearch: true/false、activeProps/inactiveProps切换、函数式 search updater 与 children-as-function 形态的Link。react/setup.ts 展示了步骤循环的接线before()挂载应用后订阅onRendered定义click向data-testid锚点派发MouseEvent(click, { bubbles: true, cancelable: true, button: 0 })并等待下次onRendered与navigate两种步骤随后按取模方式环形推进一个 10 步序列4 次 link 点击 嵌套详情往返 search 翻页 context 页。react/speed.bench.ts 中基准体每次迭代执行 10 个 tick配置warmupIterations: 100、time: 10_000。文件内有一段耐人寻味的注释vitest benchtinybench会忽略beforeAll/afterAll套件钩子所以 setup 走 tinybench 的setup/teardown选项但 CodSpeed 会绕过setup/teardown直接调用基准函数只支持beforeAll/afterAll——因此代码里看似重复地挂载了两套钩子实际上每个环境只会执行一套。工程接线package.json、Nx 图与 CodSpeedpackage.json 定义了全部入口脚本build:react/build:solid/build:vueNODE_ENVproduction vite build --config ./framework/vite.config.ts产出dist/app.jstest:perfNODE_ENVproduction vitest bench --config ./vitest.config.ts三框架聚合test:perf:react|solid|vue按框架聚合配置分别运行test:flame:react|solid|vueNODE_ENVproduction flame run --md-formatdetailed --delaynone --node-options--stack-size65500 ./framework/speed.flame.ts即基于platformatic/flame的 10 秒循环火焰基准test:types对三套基线 tsconfig 跑tsc --noEmit。依赖声明同样说明了测量对象tanstack/react-router、tanstack/solid-router、tanstack/vue-router、tanstack/router-core、tanstack/history全部以workspace:^引用即基准测的是本仓库当前工作区构建的路由库而非 npm 发布版本。Vitest 配置中还有两个统一细节define把process.env.NODE_ENV固化为production确保打包进 bundle 的代码走生产分支CodSpeed 插件仅在VITEST与WITH_INSTRUMENTATION环境变量同时存在时启用!!(process.env.VITEST process.env.WITH_INSTRUMENTATION) codspeedPlugin()即本地裸跑vitest bench时不注入插桩只有 CodSpeed CI 环境才激活。execArgv: cpuSimulationExecArgv()来自仓库根的 cpu-simulation.ts为 CI 提供模拟的 CPU 运行参数。Nx 侧package.json的nx.targets块把依赖边显式写成图test:perf依赖三个build:framework每个build:framework又依赖对应路由库项目的build目标以及该框架下全部 12 个场景项目的build:client目标benchmarks/client-nav-scenario-framework命名。所有 perf/flame 目标均声明cache: false保证每次运行都重建、重测。运行指南以下命令全部来自 README通过 Nx 执行以确保依赖构建被纳入任务图运行全部基准CI1 NX_DAEMONfalse pnpm nx run benchmarks/client-nav:test:perf --outputStylestream --skipRemoteCache按框架运行基线 全部场景CI1 NX_DAEMONfalse pnpm nx run benchmarks/client-nav:test:perf:react --outputStylestream --skipRemoteCache CI1 NX_DAEMONfalse pnpm nx run benchmarks/client-nav:test:perf:solid --outputStylestream --skipRemoteCache CI1 NX_DAEMONfalse pnpm nx run benchmarks/client-nav:test:perf:vue --outputStylestream --skipRemoteCache手动运行单个场景应用需先经 Nx 构建CI1 NX_DAEMONfalse pnpm nx run benchmarks/client-nav-scenario-framework:build:client --outputStylestream --skipRemoteCache cd benchmarks/client-nav NODE_ENVproduction vitest bench --config ./scenarios/scenario/framework/vite.config.ts火焰图基准10 秒循环platformatic/flame采集强制NODE_ENVproduction# 基线 CI1 NX_DAEMONfalse pnpm nx run benchmarks/client-nav:test:flame:react --outputStylestream --skipRemoteCache CI1 NX_DAEMONfalse pnpm nx run benchmarks/client-nav:test:flame:solid --outputStylestream --skipRemoteCache CI1 NX_DAEMONfalse pnpm nx run benchmarks/client-nav:test:flame:vue --outputStylestream --skipRemoteCache # 场景 CI1 NX_DAEMONfalse pnpm nx run benchmarks/client-nav-scenario-framework:test:flame --outputStylestream --skipRemoteCache对基准源码做类型检查基线 场景CI1 NX_DAEMONfalse pnpm nx run benchmarks/client-nav:test:types --outputStylestream --skipRemoteCache小结benchmarks/client-nav的价值不只是有一组性能数字而是一套可复用的客户端路由基准方法论职责隔离12 个场景各测一块职责回归可归因基线 bench 名保持稳定以延续 CodSpeed 历史数据确定性优先无墙钟、无随机、缓存参数只取 0 或无穷大、跳数化的异步同步配合setImmediate与零延迟超时重写压制系统调用噪声失败优于虚快热身圈逐步断言可观测输出静默失效的场景会直接报错工程闭环Nx 任务图保证先构建库 → 再构建场景 → 后运行基准的顺序CodSpeed 插桩仅在 CI 环境激活火焰图与微基准共用同一生产 bundle。若要进一步深入可以从 scenarios/harness.ts 入手读同步机制再看 scenarios/loaders/shared.ts 这类框架无关场景定义 三框架应用的具体实例最后到 react/app.tsx 理解基线负载的构成服务端侧对应物则见 benchmarks/ssr。【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表