#frontend#performance#ssr#images

Violet 与 Yggdrasil 性能实测:首屏速度与长文并发

Mizuki_Miru
Mizuki_Miru
2026年10月24日6 次阅读

HTTP 返回 200、HTML 很快收到、首批文字出现、首屏最大内容绘制完成,是四个不同的判断。这次对 Violet 与 Yggdrasil 做了两组测量:用真实浏览器观察显示速度,再用有界 HTTP/2 请求观察小并发下的响应变化。

结果没有给出一个全面领先的站点:Yggdrasil 首页的 LCP 更早,同文长文则是 Violet 的 LCP 更早;但长文的 LCP 元素不同,而且两站文章链路都出现了需要继续定位的现象。

测什么,怎样测

文章样本选两站共有的《逐像素弯曲时空:singularity-rs 黑洞渲染器深度解析》:Violet 转载页与 Yggdrasil 原文页。两站使用同一篇原文,但转载页没有原文相同的封面与可运行代码示例,因此是同文对照,不是完全相同的页面工作量。

浏览器使用独立、未登录、无代理的 Chromium 156,DPR 为 1,按下列条件逐页加载,不与并发请求测量重叠:

条件桌面移动模拟
CSS 视口1440 × 900390 × 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 首页331601,132212
Yggdrasil 首页802648961,088
Violet 长文175552552272
Yggdrasil 长文132480664468

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。部分跨域图片没有开放详细计时,因此可能低估实际传输量。

页面FCPLCP可统计传输量长任务累计
Violet 首页1,228ms4,788ms1,079 KiB400ms
Yggdrasil 首页1,060ms1,696ms672 KiB248ms
Violet 长文1,924ms1,924ms1,377 KiB2,016ms
Yggdrasil 长文1,428ms5,712ms868 KiB1,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 并发 P953 并发 P955 并发 P95实际请求数
Violet 首页34.9ms58.5ms90.4ms90
Yggdrasil 首页170.5ms323.0ms201.5ms90
Violet 长文195.7ms623.5ms1,410.2ms90
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 百分位。

下一轮优化应保持样本、限速、窗口和缓存分类一致,同时回答两件事:读者是否更早看到需要的内容,以及被延后的工作是否让第一次交互变得更重。

评论将在接近时加载。