🤖
AI审核中

849 KB 的字体一个字没用上:博客冷加载流量减半

博客默认主题换成流萤后,用无头 Chrome 在手机尺寸下做冷加载实测:首页 1495 KB 里有 849 KB 是一个字体文件,页面上却没有一个字用它。本文记录这一轮排查:预加载让按需下载的字体变成必下,CSS 背景图当最大内容时浏览器发现得太晚,评论照片显示 220px 却下载 1080px,改 srcset 时撞上同一段 HTML 被规范化两遍,以及打字机副标题折行导致的首屏跳动。改完四个页面的冷加载流量减少 42% 到 68%。

文章摘要

博客默认主题换成流萤后,用无头 Chrome 在手机尺寸下做冷加载实测:首页 1495 KB 里有 849 KB 是一个字体文件,页面上却没有一个字用它。本文记录这一轮排查:预加载让按需下载的字体变成必下,CSS 背景图当最大内容时浏览器发现得太晚,评论照片显示 220px 却下载 1080px,改 srcset 时撞上同一段 HTML 被规范化两遍,以及打字机副标题折行导致的首屏跳动。改完四个页面的冷加载流量减少 42% 到 68%。

博客的默认主题换成流萤之后,上一轮测的是页面动起来卡不卡:滚动、动画、明暗切换。这一轮看的是页面来得快不快:手机尺寸、不带缓存,第一次打开要下载多少东西,最大的那块内容什么时候画出来,首屏有没有东西跳来跳去。

先放结果,四个页面的冷加载流量:

页面 改前 改后
首页 1495 KB 638 KB
文章页 1606 KB 749 KB
归档 1266 KB 409 KB
留言板 2599 KB 1508 KB

下面按发现的顺序记录:一个没人用的字体,一张发现得太晚的大图,一组尺寸不对的评论照片,以及改照片时撞上的一个渲染问题。

一、怎么测

用本机 Chrome 的无头模式,通过 CDP 控制。视口设成 390×844、3 倍屏,开触摸模拟,禁用缓存,打开页面后等 15 秒,收集四类数据:

  • 每个请求的传输字节数,按资源类型和域名汇总;
  • 最大内容绘制(LCP)和布局偏移(CLS),都带上对应的元素;
  • head 里阻塞渲染的样式表和脚本;
  • 页面上每张图的原始宽度和显示宽度。

字节数来自 Network 域的事件,LCP 和 CLS 用页面里的 PerformanceObserver 记录(节选):

// 每个请求结束时记下实际传输的字节数
if (x.method === 'Network.loadingFinished') {
  const r = reqs.get(x.params.requestId);
  if (r) r.bytes = x.params.encodedDataLength;
}

// 页面里:最大内容绘制和布局偏移,都带上元素
new PerformanceObserver((list) => list.getEntries().forEach((e) => {
  lcp.push({ t: Math.round(e.startTime), size: e.size, el: e.element && e.element.className });
})).observe({ type: 'largest-contentful-paint', buffered: true });
new PerformanceObserver((list) => list.getEntries().forEach((e) => {
  if (!e.hadRecentInput) cls.push({ v: e.value, src: e.sources.map((s) => s.node && s.node.className) });
})).observe({ type: 'layout-shift', buffered: true });

有一点要先说明:从这台电脑连服务器,首字节时间在 2.6 到 5.1 秒之间跳,前后两次测出来的 LCP 毫秒数没法直接比。字节数和请求数不受网络波动影响,下面主要看它们。

二、849 KB 的字体,页面上一个字都没用

首页一共 44 个请求、1495 KB,其中字体 4 个、887 KB。单是一个 lxgwwenkaiscreen-site-22cb6c14a1.woff2 就有 849 KB,占了整页的一半以上。

这是霞鹜文楷的站点子集,是原主题“云笺”的正文字体,按全站实际用到的字裁过,3637 个码点。五套新主题共用一个页头片段,里面照搬了云笺的预加载:

<link rel="preload" href="https://niu.hqxiaozou.top/blog-static/fonts/lxgwwenkaiscreen-site-22cb6c14a1.woff2"
      as="font" type="font/woff2" crossorigin="anonymous">

可新主题都有自己的字体栈,把正文字体的变量换掉了。到底还有谁在用它?我在每个主题的首页、文章页和留言板上逐个检查有文字的元素,看 getComputedStyle 算出来的字体栈第一项是不是文楷:

主题 首页 文章页 留言板
画报 0 0 0
格致 0 0 0
星河 0 0 0
流萤 0 0 0
纸墨 85 298 136

只有纸墨还拿文楷当正文字体,另外四套一个字都没用到。

这里的关键在预加载本身。用 @font-face 声明的字体本来是按需下载的:页面上有文字要用它,浏览器才去拉,没人用就不下。预加载把这一步变成了无条件下载。所以光留着 @font-face 声明并不费流量,真正的问题是这条预加载。

改法是加个条件,只给纸墨。顺手把那个写满 unicode-range 的 @font-face 样式表(未压缩 24.8 KB,阻塞渲染)也一起加上条件:

<th:block th:if="${blogTheme == 'paper'}">
    <link rel="preload" href="https://niu.hqxiaozou.top/blog-static/fonts/lxgwwenkaiscreen-site-22cb6c14a1.woff2"
          as="font" type="font/woff2" crossorigin="anonymous">
</th:block>
<link rel="stylesheet" th:if="${blogTheme == 'paper'}"
      th:href="@{/fonts/lxgw-wenkai-screen/lxgwwenkaiscreen.css(v=site-20260924a)}">

改完首页的字体从 887 KB 降到 38 KB,剩下的是两个图标字体子集和一个等宽字体。开头那张表里,首页、文章页、归档各少了 857 KB,基本都是它。

三、最大内容是一张 CSS 背景图

文章页、归档和留言板的 LCP 元素都是 div.ff-banner-img,也就是首屏那张大图。它不是 <img>,而是写在 style 上的 CSS 背景,电脑和手机各用一个尺寸:

<div class="ff-banner-img" style="--ff-bg-lg:url(…/thumbnail/1920x/…);--ff-bg-sm:url(…/thumbnail/900x/…)"></div>
.ff-banner-img { background-image: var(--ff-bg-lg); }
@media (max-width: 767px) {
    .ff-banner-img { background-image: var(--ff-bg-sm, var(--ff-bg-lg)); }
}

浏览器有个预加载扫描器,HTML 还没解析完就会先扫出里面的 <img>、<link>、<script>,提前开始下载。CSS 背景图它看不见:要等样式表下载完、样式算完,浏览器才知道这个元素要用哪张图。而首页 head 里有 13 个阻塞渲染的样式表。

改法是在大图前面放两条图片预加载,断点和 CSS 一模一样,同一时刻只会命中一条:

<link rel="preload" as="image" th:href="${commons.bannerSmall(image)}" media="(max-width: 767px)" fetchpriority="high">
<link rel="preload" as="image" th:href="${commons.banner(image)}" media="(min-width: 768px)" fetchpriority="high">

更顺手的写法是 imagesrcset 加 imagesizes,让浏览器自己挑,但这里不能用。CSS 是按视口宽度切换的,不看像素密度:390 宽的 3 倍屏,CSS 用的是 900 宽那张;imagesrcset 却会按 390×3=1170 去挑 1920 宽那张。结果是预加载一张、背景再下一张,同一张图下两遍。所以预加载的条件必须照抄 CSS 的媒体查询。

改完实测,每个视口只发出一个大图请求,手机是 900 宽,电脑是 1920 宽。

四、显示 220px 的照片,下载的是 1080px

留言板是最重的页面:198 个请求、2599 KB,其中图片 1407 KB。

留言里的照片在手机上最多显示 220px 宽,竖图还要再被 260px 的最大高度压一下,实际只有 117px 宽;电脑上最多 300 到 320px。下载的却是 1080 宽的缩略图,单张 128 到 138 KB。

这些地址是服务端渲染时统一补的七牛处理参数,只有 1080 这一档。改成给浏览器三档,让它按屏幕挑:

<img src="…/a.jpg?imageMogr2/thumbnail/1080x/quality/78/format/webp/ignore-error/1"
     srcset="…/a.jpg?imageMogr2/thumbnail/480x/quality/78/format/webp/ignore-error/1 480w,
             …/a.jpg?imageMogr2/thumbnail/720x/quality/78/format/webp/ignore-error/1 720w,
             …/a.jpg?imageMogr2/thumbnail/1080x/quality/78/format/webp/ignore-error/1 1080w"
     sizes="(max-width: 768px) 240px, 320px">

有两个地方要留意。

src 留着 1080 宽。点开照片看大图的预览,读的是 img.src,而 src 返回的永远是属性里写的地址,不是浏览器从 srcset 里挑中的那张(那张在 currentSrc 里)。所以列表里加载小图,点开照样是 1080 宽。

邮件不能带 srcset。有人回复时,评论正文也会发进邮件。邮件那边会把图片下载下来作为内嵌附件,测试要求 HTML 里不能留下图床地址,srcset 里的地址会漏过去。所以只给网页那条渲染路径加。

改完手机 3 倍屏挑的是 720 宽,电脑 1 倍屏挑的是 480 宽,点开大图仍然是 1080 宽。

改完一看,srcset 没了

单元测试都过了,本地起服务一看留言板的源码,照片的 src 带上了处理参数,srcset 却一个都没有。单独调那个规范化函数,输出里明明有 srcset。

原因是评论正文在列表查询里被规范化了两遍,分页查询内部一次,外面又一次,代码注释里写着“同一批记录可能被处理两次”。规范化的第一步是安全重建:把 <img> 标签拆开,只保留 src、alt、style 三个属性重新拼。于是第二遍时,srcset 被当成不认识的属性丢掉了;随后补 srcset 的那一步看到 src 已经带了参数,按“已经处理过的不动”跳了过去。

阶段 src srcset
库里存的 原图地址 没有
第一遍 补上参数 补上
第二遍重建 带着参数 丢了
第二遍补 srcset 带着参数 跳过

修法是让这一步幂等:src 结尾恰好是自己加的那串默认参数时,按原图重新生成一遍。

// 第二遍时 src 已带默认参数、srcset 已经丢了:去掉默认参数,按原图重新生成
String bare = url.endsWith(PARAMS) ? url.substring(0, url.length() - PARAMS.length()) : url;

再补一条测试,规范化两遍和一遍的结果必须完全一样。

改完留言板从 2599 KB 降到 1508 KB,其中图片从 1407 KB 降到 1174 KB。图片省得不算多,因为再往下的照片本来就是懒加载,首屏附近只有 4 张换成了小图。

五、首屏上下跳

最后一个问题不在流量上。首页大标题下面有一行打字机效果的副标题,在 390 宽的手机上会折成两行。打字时它一会儿一行一会儿两行,而大图里的内容是垂直居中的,于是上面的标题和下面的按钮跟着上下跳。15 秒里记到 84 次布局偏移,来源就是它们:

元素 位置变化
标题 h1 302 → 313
按钮组 453 → 464

改法是先量一遍每句话完整打出来时这一行有多高,取最大值写成最小高度,窗口尺寸变了再量一次:

function reserve() {
    var keep = el.textContent, max = 0;
    line.style.minHeight = '';
    list.forEach(function (text) {
        el.textContent = text;
        max = Math.max(max, line.offsetHeight);
    });
    el.textContent = keep;
    line.style.minHeight = max + 'px';
}

手机上这一行固定成两行高,标题和按钮不再跳。累计布局偏移从 0.054 降到 0.021,剩下的是居中的文字逐字变长时自身的位移。

总结

问题 根因 改法
字体白下 849 KB 预加载照搬旧主题 只给纸墨
大图发现得晚 CSS 背景图 同断点预加载
照片下载过大 只有 1080 一档 srcset 三档
srcset 消失 规范化跑两遍 改写做成幂等
首屏上下跳 副标题折行 预留最高一行

这一轮的几条经验:

预加载是强制下载。@font-face 没人用就不下,预加载不管用不用都下。多个主题共用的片段尤其要回头查一遍,当初为谁加的。

背景图当最大内容时,浏览器发现得晚。可以预加载,但条件要和 CSS 的切换条件完全一致,不然会下两遍。

加 srcset 之前,先想清楚还有谁在读 src。预览读 src,邮件不能带 srcset。渲染链路里可能有一段会跑两遍,改写要做成幂等的。

网络波动大的时候,看字节数和请求数,比看毫秒稳。

0 条评论

如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧

评论区 0

头像
 0 条评论