首页
/ Astro 基准测试中的 @benchmark/timer 适配器:用毫秒计时器替代页面输出,精确度量服务端渲染耗时

Astro 基准测试中的 @benchmark/timer 适配器:用毫秒计时器替代页面输出,精确度量服务端渲染耗时

2026-09-05 23:55:02作者:齐冠琰

@benchmark/timer 是 Astro 官方仓库中 benchmark/ 基准测试套件的一部分,它实现了一个"像 @astrojs/node 但不返回页面 HTML、而是返回页面渲染耗时(毫秒)"的专用适配器,仅供内部性能基准测试使用。本文以该包的 README 说明为骨架,结合其完整源码与基准测试驱动脚本,讲清楚这个计时适配器的注册机制、计时实现、在 astro build/astro preview 流程中的位置,以及统计口径(每页 100 次采样的均值、标准差与最大值),帮助读者理解 Astro 是如何量化"每次请求的服务端渲染时间"的,并借鉴其测量方法。

它是什么:一个只输出耗时的适配器

benchmark/packages/timer/README.md@benchmark/timer 的全部官方定义只有三行:

Like @astrojs/node, but returns the rendered time in milliseconds for the page instead of the page content itself. This is used for internal benchmarks only.

即:行为上类似 @astrojs/node 适配器,但对每个页面的响应体不再是渲染后的 HTML,而是一个纯文本数字——这次渲染所花费的毫秒数。该包被明确标注为"仅用于内部基准测试"(used for internal benchmarks only),这也与其 package.json 中的 "private": true"version": "0.0.0"keywords: ["astro-adapter"] 一致:它不会发布到 npm,只在 Astro monorepo 内通过 workspace:* 协议被 benchmark/package.json 引用。

整个 benchmark/ 目录是 Astro 的主基准测试套件,benchmark/README.md 指出它暴露了 astro-benchmark CLI 命令(astro-benchmark --help 可查看全部命令)。在该体系中,@benchmark/timer 承担的角色是"计时探针":先用它构建并预览一个标准测试项目,再通过 HTTP 请求逐页采集渲染耗时,最后做统计分析。

包结构:两个入口文件与一个适配器

package.jsonexports 字段声明了四个出口:

{
  "exports": {
    ".": "./dist/index.js",
    "./server.js": "./dist/server.js",
    "./preview.js": "./dist/preview.js",
    "./package.json": "./package.json"
  }
}
  • .(即 dist/index.js,来源 src/index.ts):在用户的 astro.config.* 中作为 integration 引入,负责注册适配器;
  • ./server.js(来源 src/server.ts):SSR 服务入口,真正的"计时 handler";
  • ./preview.js(来源 src/preview.ts):astro preview 使用的预览服务器入口。

构建脚本为 "build": "astro-scripts build \"src/**/*.ts\" && tsc",依赖上只有 server-destroy(预览服务器关闭时用于销毁 socket),并通过 peerDependenciesdevDependencies 声明了对 astro: workspace:* 的依赖——再次印证它只服务于 monorepo 内部的 Astro 版本,这也是使用它的前提:必须能解析到工作区内的 astro 包,外部项目无法直接安装复用

集成与适配器注册:src/index.ts 的实现细节

src/index.ts 对外导出两个东西:一个 getAdapter() 工厂和一个默认的 createIntegration() 工厂。

import type { AstroAdapter, AstroIntegration } from 'astro';

export function getAdapter(): AstroAdapter {
	return {
		name: '@benchmark/timer',
		serverEntrypoint: '@benchmark/timer/server.js',
		previewEntrypoint: '@benchmark/timer/preview.js',
		exports: ['handler'],
		supportedAstroFeatures: {
			serverOutput: 'stable',
		},
	};
}

export default function createIntegration(): AstroIntegration {
	return {
		name: '@benchmark/timer',
		hooks: {
			'astro:config:setup': ({ updateConfig }) => {
				updateConfig({
					vite: {
						ssr: {
							noExternal: ['@benchmark/timer'],
						},
					},
				});
			},
			'astro:config:done': ({ setAdapter, config }) => {
				setAdapter(getAdapter());

				if (config.output === 'static') {
					console.warn(`[@benchmark/timer] \`output: "server"\` is required to use this adapter.`);
				}
			},
		},
	};
}

可以从源码中读出三个关键设计点:

  1. 适配器元信息AstroAdapter 声明了 serverEntrypoint: '@benchmark/timer/server.js'previewEntrypoint: '@benchmark/timer/preview.js',与上面 exports 字段一一对应;exports: ['handler'] 表明该 server 入口只向 Astro 运行时暴露一个 handler 函数;supportedAstroFeatures.serverOutput: 'stable' 声明它支持服务端输出能力。
  2. astro:config:setup 钩子。通过 updateConfig@benchmark/timer 加入 Vite 的 ssr.noExternal,让该包在 SSR 构建时走打包而非外部 require。从源码结构看,这是因为它依赖工作区内的 astro 产物,需要与 Astro 核心一起被打进 bundle,避免在 SSR 运行期按 Node 模块解析失败。
  3. astro:config:done 钩子与前置条件。在配置完成后调用 setAdapter(getAdapter()) 完成注册;同时检查 config.output === 'static',并打印警告 `output: "server"` is required to use this adapter。这正是 README 中"像 @astrojs/node"的具体体现:计时逻辑运行在服务端渲染路径上,静态输出模式没有请求级渲染过程,自然无从计时。

计时核心:src/server.ts 如何用 performance.now() 包络一次渲染

真正的测量逻辑只有十几行,位于 src/server.ts

import type { IncomingMessage, ServerResponse } from 'node:http';
import { createApp } from 'astro/app/entrypoint';
import { createRequest } from 'astro/app/node';

const app = createApp();

export async function handler(req: IncomingMessage, res: ServerResponse): Promise<void> {
	const start = performance.now();
	await app.render(
		createRequest(req, {
			allowedDomains: app.manifest.allowedDomains,
		}),
	);
	const end = performance.now();
	res.write(end - start + '');
	res.end();
}

调用链是:astro/app/entrypointcreateApp() 创建应用实例 → 收到 Node IncomingMessage 后用 astro/app/nodecreateRequest() 转换为 Astro 内部 Request(并透传 app.manifest.allowedDomains 作为 fetch 允许的域名)→ await app.render(...) 完成一次完整的服务端渲染 → 用 performance.now() 差值得到毫秒数 → res.write(end - start + '') 把这个数字作为纯文本响应体写出。

由此可以明确它的测量口径:计时范围是 app.render() 这一次请求在服务器进程内的执行时间,不包含客户端网络往返、浏览器解析与前端框架水合;由于基准脚本从本机发起请求(见下文),网络开销也基本被排除在统计之外。

配套的 src/preview.ts 则实现了 astro preview 所需的 CreatePreviewServer:动态 import(serverEntrypoint) 拿到 handler,用 node:httpcreateServer(ssrHandler) 创建服务器并 listen(port, host),再通过 server-destroyenableDestroy(server) 支持 stop() 时销毁服务器;closed() 返回的 Promise 在服务器 close 时 resolve、error 时 reject。这与 @astrojs/node 等正式适配器的预览服务形态一致,所以 README 才会说它"like @astrojs/node"。

基准套件如何驱动它:生成项目、百次采样、统计分析

生成带 timer 的测试项目

benchmark/make-project/render-default.js 负责"造项目":它写入一套标准页面/组件文件后,动态生成一份 astro.config.js

import { defineConfig } from 'astro/config';
import timer from '@benchmark/timer';
import mdx from '@astrojs/mdx';

export default defineConfig({
  integrations: [mdx()],
  output: 'server',
	adapter: timer(),
});

注意这里 adapter: timer() 调用的是 integration 的默认导出——即上一节 createIntegration() 返回的对象,由它内部的 astro:config:done 钩子再调用 setAdapter(getAdapter())。同时 output: 'server' 也正好避开了前面提到的静态模式警告。

构建、预览与采集

benchmark/bench/render.jsrun() 编排了完整流程:

  1. 在项目目录执行 astro buildastroBinbenchmark/bench/_util.js 解析为工作区内 astro 包的 bin/astro.mjs,保证测的是当前源码而非发布版本);
  2. 执行 astro preview --port 4322waitUntilBusy(port) 等待服务就绪;
  3. 调用 benchmarkRenderTime() 采集数据;
  4. SIGTERM 终止预览进程,把结果写入 JSON 文件并打印 Markdown 表格。

benchmarkRenderTime() 的统计口径是:对每个页面连续发起 100 次请求,每次通过 fetchRenderTime() 读取响应体(把 timer 返回的纯文本毫秒值用 +data 转成数字)并压入数组;随后交给 calculateStat() 计算三项指标:

const avg = numbers.reduce((a, b) => a + b, 0) / numbers.length;
const stdev = Math.sqrt(
	numbers.map((x) => Math.pow(x - avg, 2)).reduce((a, b) => a + b, 0) / numbers.length,
);
const max = Math.max(...numbers);
return { avg, stdev, max };

最终通过 markdownTable 输出 Page / Avg (ms) / Stdev (ms) / Max (ms) 四列表格,均值与标准差保留两位小数。也就是说,@benchmark/timer 提供的每次"单请求耗时"读数,最终被汇总成可跨版本对比的统计量。

小结:可借鉴的计时探针模式

把 README 的一句话结论展开后,@benchmark/timer 的完整图景是:一个私有 Astro 适配器,用 performance.now()app.render() 两侧打点、以纯文本返回耗时;通过 astro:config:done 钩子注册、以 output: 'server' 为硬性前提;由 benchmark/ 套件的"生成项目 → build → preview → 每页 100 次采样 → avg/stdev/max 统计"流水线驱动,产出可重复对比的渲染耗时数据。其限制也很明确:private: true 且依赖 workspace:* 的 astro,只能在 Astro 仓库内部使用;它度量的是服务端单次请求渲染耗时,不包含网络、浏览器端行为。如果你需要在自己的项目中做类似的 SSR 耗时测量,"在渲染调用两侧用 performance.now() 打点 + 多次采样求均值与极值"这一模式可以直接参照实现。

补充一点仓库内的观察:同级的 benchmark/packages/adapter 包 README 标题也写作 # @benchmark/timer(可视为文档笔误),但其 integration 内部声明的名称是 @benchmark/adapter、server 入口为 @benchmark/adapter/server.js,与本文介绍的 @benchmark/timer 是两个不同的包,引用时请注意区分。

登录后查看全文
热门项目推荐
相关项目推荐