Violet 与 Yggdrasil 性能实测:首屏速度与长文并发
HTTP 返回 200、HTML 很快收到、首批文字出现、首屏最大内容绘制完成,是四个不同的判断。这次对 Violet 与 Yggdrasil 做了两组测量:用真实浏览器观察显示速度,再用有界 HTTP/2 请求观察小并发下的响应变化。
结果没有给出一个全面领先的站点:Yggdrasil 首页的 LCP 更早,同文长文则是 Violet 的 LCP 更早;但长文的 LCP 元素不同,而且两站文章链路都出现了需要继续定位的现象。
测什么,怎样测
文章样本选两站共有的《逐像素弯曲时空:singularity-rs 黑洞渲染器深度解析》:Violet 转载页与 Yggdrasil 原文页。两站使用同一篇原文,但转载页没有原文相同的封面与可运行代码示例,因此是同文对照,不是完全相同的页面工作量。
浏览器使用独立、未登录、无代理的 Chromium 156,DPR 为 1,按下列条件逐页加载,不与并发请求测量重叠:
| 条件 | 桌面 | 移动模拟 |
|---|---|---|
| CSS 视口 | 1440 × 900 | 390 × 844 |
| 网络 | 不限速 | 下载 1.6Mbps,配置延迟 150ms |
| CPU | 不降速 | 降速 4 倍 |
| 缓存与重复次数 | 每页冷、暖缓存各 3 次 | 每页冷缓存 3 次 |
| 导航后观察窗口 | 8 秒 | 15 秒 |
一共完成 36 次浏览器页面加载。下文的浏览器数字取每组 3 次的中位数,LCP 取观察窗口内最后一个候选;移动条件是模拟,不是真机测试。冷缓存只清浏览器 HTTP 资源缓存,不清 CDN 或服务端 SSR 缓存,DNS 与已建立的传输连接可能复用。
几个指标需要分开读:
- TTFB:从导航开始到收到响应首字节,包含当前访问路径上的等待,不等于服务端 CPU 时间。
- FCP:页面首次绘制文字、图片等内容。它回答“什么时候开始有内容”,不是“正文已经完整可读”。定义见 First Contentful Paint。
- LCP:视口中最大内容元素的绘制时刻,不是整篇文章、全部资源或所有交互都已完成。定义见 Largest Contentful Paint。
桌面:先出内容与主要内容出现,不是同一个排名
单位为毫秒,越低越好。
| 页面 | 冷缓存 TTFB | 冷缓存 FCP | 冷缓存 LCP | 暖缓存 LCP |
|---|---|---|---|---|
| Violet 首页 | 33 | 160 | 1,132 | 212 |
| Yggdrasil 首页 | 80 | 264 | 896 | 1,088 |
| Violet 长文 | 175 | 552 | 552 | 272 |
| Yggdrasil 长文 | 132 | 480 | 664 | 468 |
Violet 首页的 HTML 和首批内容更早到达,但最大的首屏内容更晚呈现。长文则是 Yggdrasil 更早出现首批内容,Violet 的 LCP 更早。只选其中一个指标,会得到不同的“谁更快”。
Yggdrasil 的暖缓存首页样本没有整体变快。这里的“暖”只指浏览器资源缓存,HTML 响应仍会受到服务端 SSR 缓存状态与访问路径波动影响。其响应携带 Cache-Control: public, no-cache,浏览器缓存变暖不能直接解释成 HTML 无须网络访问。
桌面数字保留了当时的网络失败现场:8 次导航中,jsDelivr 上 Maple Mono 的 4 个字重请求发生连接重置,合计 32 次 ERR_CONNECTION_RESET。后续桌面轮次恢复,移动轮次没有资源网络失败。这些失败没有因为页面最终显示成功而被剔除。
移动慢网:文字先出现,封面可能还要等
传输量按 Resource Timing 可见信息汇总,单位为 KiB。部分跨域图片没有开放详细计时,因此可能低估实际传输量。
| 页面 | FCP | LCP | 可统计传输量 | 长任务累计 |
|---|---|---|---|---|
| Violet 首页 | 1,228ms | 4,788ms | 1,079 KiB | 400ms |
| Yggdrasil 首页 | 1,060ms | 1,696ms | 672 KiB | 248ms |
| Violet 长文 | 1,924ms | 1,924ms | 1,377 KiB | 2,016ms |
| Yggdrasil 长文 | 1,428ms | 5,712ms | 868 KiB | 1,249ms |
长任务是单次超过 50ms 的主线程任务,表中累计导航后 15 秒观察窗内的任务耗时。它不是连续冻结时长,也不是 INP;定义可参见 PerformanceLongTaskTiming。
这组对照最需要看的是 LCP 对应什么元素。Violet 长文对应正文文字,Yggdrasil 长文对应封面图片。后者的标题、摘要此前已经出现,不能把封面的时刻改写为“整篇文章一直白屏到这时”。
请求时序也解释了为什么只看文件大小不够。一轮移动样本中,Yggdrasil 的 WASM 在导航后约 4.66 秒下载完成,清晰封面直到约 5.05 秒才发起请求。封面仅约 10.5 KiB,传输耗时约 0.22 秒。主要等待发生在请求发起之前,而不是这张图本身太大。
Violet 首页的三个移动样本,最终 LCP 都落在 GitHub 头像。HTML 已经返回、部分文字已经出现,并不意味着头像与标题等首屏主体也在同一时刻完成呈现。
另一项不能被 LCP 排名掩盖的结果是主线程成本:Violet 长文的 LCP 更早,长任务累计却更多。显示主要正文和后续执行是否轻量,需要分别判断。
有界并发:全部 200,也不能叫作全程稳定
并发阶段只请求 HTML,不批量加载页面子资源。每个 URL 复用一条持久 HTTP/2 连接,依次测试 1、3、5 个并发流,每档目标 30 次请求,每轮至少间隔 1 秒,最多每秒发起 5 个请求。
保护规则是出现非 200、请求超时或明显慢响应后停止升压。本次实际触发的是超过 3 秒的响应耗时保护线。
下表统计从请求发起到整份 HTML 接收完成的 P95,不包含连接建立与 TLS 握手,也不是浏览器渲染时间。
| 页面 | 1 并发 P95 | 3 并发 P95 | 5 并发 P95 | 实际请求数 |
|---|---|---|---|---|
| Violet 首页 | 34.9ms | 58.5ms | 90.4ms | 90 |
| Yggdrasil 首页 | 170.5ms | 323.0ms | 201.5ms | 90 |
| Violet 长文 | 195.7ms | 623.5ms | 1,410.2ms | 90 |
| Yggdrasil 长文 | 样本不足,保护停压 | 未执行 | 未执行 | 3 |
273 次请求全部返回 HTTP 200,没有 429、5xx 或请求超时,但文章页有两类风险:
Violet 长文在小并发下出现长尾放大。 5 并发档中,首字节等待的 P95 为 768.5ms,整份 HTML 接收完成还要更晚。需要结合服务端记录与传输路径拆分耗时,不能直接把全部等待归到数据库、CPU 或 SSR 的某一个环节。
Yggdrasil 长文触发了保护停压。 三次串行请求分别耗时约 177ms、210ms、3,469ms;第三次仅首字节等待就用了约 3,338ms。因此没有继续执行该文章的 3、5 并发档,3 个样本也不足以给出有意义的 P95 判断。这是一次确实发生的慢响应,不是服务器容量上限的证明。
缓存头给出了另一项差别:并发阶段 Yggdrasil 首页 88/90 次返回 X-SSR-Cache: HIT,文章所测 3 次均为 MISS。这与 CDN 是两层状态;这些请求的 EO-Cache-Status 都是 MISS,不能把首页 SSR 命中改称为 CDN 命中。
优化意见
以下是依据本轮观察给出的排查与改进建议,尚未作为代码改动实施,也没有优化后的性能数字。
Violet:首页头像、字体与主包
先检查首屏主体为什么晚呈现。 对照头像请求、图片解码、首屏可见性与水合时序,确认是否存在图片交接、数据回填或动画门槛。移动样本的最终 LCP 是 GitHub 头像,但这还不足以断言网络下载是唯一原因。
降低首屏对外部字体的依赖。 本轮确实发生过 jsDelivr 连接重置。可评估本地托管 Maple Mono,先加载首屏实际使用的字重,保留合适的系统回退字体;不要让字体失败阻止必要文字出现。
按必要性缩减首屏资源。 四个 Maple Mono 字重合计约 298 KiB,首页主 JS 压缩后约 410 KiB。先核对哪些依赖必须在首屏执行,再决定是否拆分;播放器、非首屏交互等能力是否适合延后,应以它们的真实使用路径为准。
验证方式:沿用相同移动限速条件,比较首屏头像与标题的实际绘制时刻、字体失败时的可读性、主包下载与执行时序。不要只以 HTML TTFB 不变或总文件体积变小作为完成依据。
Yggdrasil:让首屏清晰封面尽早可发现
优先解决请求发现延迟。 既然文件很小,而请求发起已经很晚,第一步应检查清晰图是否依赖客户端初始化后才设置真实图片地址,避免先去压缩一个已经很小的文件。
首屏清晰图可在服务端输出浏览器可发现的 src、srcset 与 sizes,保留尺寸或宽高比,避免以 JavaScript 回填地址作为显示前提。确认它确实是首屏关键图后,再评估 fetchpriority="high" 或预加载;非首屏图片继续按需加载,不把全站图片都提升优先级。
如果需要模糊占位,允许占位图与清晰图的浏览器加载同时推进,而不是等 WASM 初始化完成才开始下载清晰图。水合依赖关系仍需进一步核对,当前证据能直接确认的是请求发起晚。
验证方式:在同一限速条件下检查清晰封面是否能在 HTML 解析阶段附近发起请求,并同时观察 LCP、文字出现时刻与布局稳定性。资源发现与优先级的原则可参见 web.dev 的 LCP 优化指南。
两站文章链路:解释长尾与缓存差异,再提高负载
Violet 先拆分文章响应耗时。 对照请求并发、服务端排队、正文处理、模板或 SSR、压缩与发送时长,找出随并发放大的阶段。当前客户端结果能说明等待变长,不能替代这些环节的服务端测量。
Yggdrasil 先核对慢请求与 SSR 缓存。 排查文章页的缓存适用条件、键、失效策略与页面体积限制,解释为什么本轮文章样本均为 MISS;这些只是待核对的方向,不是已经确认存在的缺陷。首页命中率不能代替文章页的缓存有效性。
缓存优化必须保持匿名页与用户私有内容的边界,并保留发布、更新与撤回后的失效语义,不能为了命中率让旧正文或私有内容被错误复用。
验证方式:把缓存 HIT 与 MISS 的耗时分别统计,在相同网络路径与页面版本下重复小规模测量。确认慢响应原因、取得服务端监控并设置停止条件后,再安排更高负载;本轮的每秒 5 次发起上限不能换算成服务器峰值 QPS。
长文主线程:保留早显示,也削减后续工作
按长任务追踪实际执行路径。 区分 HTML 解析、样式与布局、数学公式、代码高亮、框架初始化以及非首屏增强的成本,再决定哪些工作适合延后。不要因为正文已经较早成为 LCP,就忽略其后的主线程占用。
能由服务端输出稳定内容的部分,避免客户端无条件重做;需要按需增强的部分,保留未增强时可读的正文与代码。移出首屏的工作仍然需要在首次滚动、目录跳转与返回时验证,不能只把成本转移到下一次交互。
验证方式:除首屏绘制外,检查长任务分布、首次滚动、跨章节目录跳转、代码与公式第一次进入视口时的行为。本轮没有测量 INP,不把长任务累计当作用户交互延迟的替代数字。
这次数据能支持的判断
两站的首页与文章不能合并成一个速度排名。TTFB 更早,不保证首屏主体更早;文字成为 LCP,也不代表整篇文章的后续工作已经结束。封面晚显示时,资源发现时刻可能比继续压缩图片更值得先查;全部 HTTP 200,也可能同时存在秒级慢响应和并发长尾。
这是单客户端、低流量的线上基线,而不是极限容量测试或真实用户分布。HTTP/2 并发测量中,Violet 走 IPv4 直连,Yggdrasil 走 IPv6 CDN;网络路径、缓存状态和页面工作量都不同。因此,这组数字不能用于宣布 Go 与 Rust 的后端性能高下,也不能把模拟条件下 3 次的中位数当作线上 Web Vitals 的第 75 百分位。
下一轮优化应保持样本、限速、窗口和缓存分类一致,同时回答两件事:读者是否更早看到需要的内容,以及被延后的工作是否让第一次交互变得更重。