← 返回岛志
技术

给 Astro 博客做性能优化

从 Lighthouse 审计出发,记录响应式图片、组件样式裁剪与 Waline 延迟加载,并说明哪些资源级数据可以证明收益。

给 Astro 博客做性能优化的文章封面

这是我第一次认真用 Lighthouse 审计自己的博客。此前我会凭感觉压缩图片、删一点 CSS,但很难回答一个更具体的问题:访问首页和文章页时,浏览器到底在把时间和流量花在哪里?

第一次按移动端冷加载审计首页和一篇代表性文章时,我没有把某一次性能分数当作唯一目标。单次 Lighthouse 分数会随网络、CPU 节流、缓存和页面内容变化;更有价值的是报告给出的诊断:图片下载尺寸偏大、组件库有未使用 CSS、Waline 的脚本/样式/评论字体在首屏就开始请求,以及少量文字对比度不足。

下面按 Lighthouse 报告提示的顺序,记录这次如何逐项判断:哪些应该修、哪些需要换一种实现、哪些在当前页面不该为了分数而动。文中的数字只用于说明具体资源变化,不把单次 Lighthouse 分数当作成绩单。

从 Lighthouse 建立优化边界

我按影响和判断方式整理报告中的建议:

Lighthouse 信号判断最终处理
图片传输浪费直接影响移动端下载量实测后发布 AVIF/WebP 候选图,提供 srcset 和准确的 sizes
未使用的组件库 CSS不影响视觉设计,但会增加首屏样式体积改为只保留实际使用组件的样式
Waline 资源首屏加载评论在页面底部,首屏不需要它接近评论区时才加载客户端、样式和评论字体
图片缺少固有尺寸有潜在布局偏移输出正确的 width / height,并回归检查特殊插画
链接与页脚对比度直接影响无障碍审计调整文字与链接颜色
首页入场与漂浮动画会影响首屏观感,但也是页面体验的一部分保留,不以移除动画换取分数

这张表之后,才形成了本次的边界:

  • 首页的入场动画和漂浮动画保留;
  • ClientRouter 保留;
  • 外链图片不在构建阶段改写;
  • 评论区可以延迟加载,但进入视口前必须正常可用。

换句话说,Lighthouse 是这次优化的起点和验证工具,而不是一份可以机械照抄的待办清单。性能优化不是把一切都变成 display: none,而是处理真实的传输浪费、渲染阻塞和生命周期问题。

一、图片:从原图直出改为按显示尺寸下载

博客中最明显的浪费来自图片。文章封面、首页卡片和页面场景常常以原尺寸下载,但移动端实际只显示几百像素宽。CSS 的 max-width: 100% 只能缩小显示尺寸,不能缩小已经下载的文件。

因此构建流程新增了一个基于 Sharp 的派生图生成器:

public/assets/**
    ├── 原图(保留并提交)
    └── responsive/**
        └── 带内容指纹的 AVIF/WebP 候选图(构建生成,不提交)

生成器不会放大原图,但也不会假定“尺寸更小就一定更省”。它会先在临时目录尝试 320 / 480 / 768 / 1024 / 1440 等不超过原图宽度的候选,再测量真实字节数:WebP 必须比原图至少节省 8 KiB 才发布;AVIF 会与同宽 WebP 比较,只有不大于 WebP 时才保留。若任一宽度的 AVIF 没有收益,则整组 AVIF 都不发布,以免浏览器因稀疏 srcset 选到过大的候选。这样小图通常直接保留原图,而大型原生 WebP 仍可在有收益时获得更合适的尺寸候选。

以首页狸猫插画为例,浏览器从原本约 93 KiB 的 WebP 改选 1024w AVIF 后,实际下载约 31 KiB。这种变化比“格式更先进”更有说服力:它确实少下载了不需要的像素。

这个判断很重要。比如带透明通道、纹理或不同缩放比例的 WebP,较小尺寸不一定比大尺寸更小;不能只根据像素宽度删除候选,也不能为了格式“先进”而强行生成 AVIF。

页面组件统一通过 ResponsiveImage 输出:

<ResponsiveImage
  src="/assets/blog-images/example/cover.jpg"
  alt="文章封面"
  sizes="(max-width: 760px) calc(100vw - 40px), 720px"
  loading="lazy"
/>

最终 HTML 是 <picture>:支持 AVIF 的浏览器优先从 AVIF srcset 中选择合适候选,其次使用 WebP;原图仍放在 <img src> 中作为最终回退。组件还会写入准确的 widthheight,让浏览器在图片下载前就预留布局空间,降低 CLS。

Markdown 也不能漏掉

只改页面组件还不够。博客最常增加图片的位置是 Markdown 正文,所以内容处理器会自动转换本地 /assets/... 图片:

![海边的傍晚](/assets/blog-images/example/photo.jpg)

现有的本地 HTML 图片同样会被转换,样式类仍然保留:

<img class="full-width" src="/assets/blog-images/example/photo.jpg" alt="海边的傍晚">

外链保持原样。它们不应该由站点构建过程猜测尺寸或下载到仓库中。

CSS 背景是另一条路径

首页大背景不是 <img>,因此不能把 <picture> 嵌进 background-image:CSS 背景没有 <source>srcsetsizes。它使用生成的 CSS token;支持 image-set() 的浏览器在同一 token 内选择 AVIF 或 WebP。若某个尺寸没有达到收益门槛、因而没有生成 token,样式才回退到原图 URL;媒体查询负责在小屏与大屏之间引用足够宽的 token。

只有 home-bg 这类真正作为大型 CSS 背景使用的图片才会写入 token 文件。若为每张图片、每个尺寸都写 CSS 变量,主 CSS 会携带一批从未被背景使用的 URL 和声明;普通文章图片继续由 <picture> 处理即可。

二、收紧首屏资源,而不是牺牲页面体验

Lighthouse 同时提示了未使用 CSS 和评论资源的首屏请求。两者的处理方向一致:只保留当前页面真正需要的资源。

组件库不再引入全量样式,而是保留本站实际使用组件的样式;页面原有的 Card、Tag、Button 等视觉效果不变。主 CSS 因此从约 36 KiB 降到约 21 KiB。Waline 则延后到评论区即将进入视口时,再加载客户端、基础样式和评论字体。初始文章页的首屏请求已经说明了它的必要性:评论相关资源约 825 KiB,其中绝大部分是评论字体。这样首屏不必为页面底部的交互付出下载成本,用户滚到评论区时仍能直接留言。

这里的重点不是“动态 import”本身,而是资源的出现时机:首屏应该优先服务阅读,评论区则在用户接近它时准备好。

实现细节

上面的判断最终落成了三条独立但能一起工作的构建链路。

1. 图片在构建前生成,在页面中按需选择

图片生成接入 predevprebuildprecheck。它扫描 public/assets/,生成派生图清单;构建结束后再检查清单中的候选图是否都存在。清单记录原图的 sourceHash、编码规则的 pipelineHash、候选的真实字节数与指纹 URL;两种哈希都未变化且文件存在时直接复用。新增、替换或删除一张图片只会生成或清理那一组候选,调整全局尺寸、质量或门槛才会使所有图片重新评估。

候选 URL 带内容指纹,因此原图或编码规则变化后,页面会引用新地址,浏览器不会把旧派生图当成新版本继续使用。生成器先完成候选与清单校验,再发布新清单;旧清单管理、但新清单不再引用的候选随后才会清理。原图和 AVIF/WebP 候选图的关系由一个统一组件读取,所以首页、卡片、文章封面和页面场景不会各自维护一套 srcset

内容层也走同一套规则:Markdown 的 ![](/assets/...) 与本地 HTML <img> 在构建时改写为响应式图片;外链不动。这样新文章只需要按普通 Markdown 写图,不需要记住额外的组件语法。由于图片清单变化时 Markdown 源文件本身未必变化,开发和生产构建会强制刷新 Astro 内容层缓存,避免页面继续保留已清理的旧候选 URL。

2. 样式按“基础 + 已使用组件”组织

组件库的通用基础样式保留一份,本地再显式汇入当前确实使用的组件样式,例如:

@import '…/Button/button.module.css';
@import '…/Card/card.module.css';
@import '…/Tag/tag.module.css';

实际项目还为这份清单设置了校验:构建前检查页面中使用的组件是否都有对应样式。这让“缩小 CSS 体积”和“页面组件不失样式”成为两个可以同时验证的条件,而不是二选一。

3. 评论区由可见性触发加载

Waline 容器先作为普通的占位区域输出,客户端脚本再建立观察器:

评论区距离视口约 600px

加载 Waline 客户端与基础 CSS

挂载评论区,并异步补充评论字体

不支持 IntersectionObserver 时直接加载,保证兼容性。站内切换时先销毁旧实例;重新进入评论页时再检查样式与字体是否已经就绪。这样既避免首屏请求评论资源,也不把评论区变成“第一次能用、切换后偶发失效”的黑盒。

三、不要盲目提高图片优先级

fetchpriority="high" 很有用,但它不是图片优化的默认开关。本次审计中真实 LCP 是文本区域,而不是封面或狸猫插画。给多张图片都标高优先级,只会让它们和关键 CSS、字体与正文竞争网络带宽。

正确顺序应当是:

  1. 先通过 srcsetsizes 减少图片传输;
  2. 在生产环境重复测量,确认真正的 LCP 元素;
  3. 只有当某页唯一的 LCP 图片稳定出现时,才为那一张图添加高优先级。

性能指标必须服务于真实页面,而不是猜测。

验证清单

这次没有只依赖一次 Lighthouse 结果,而是把验证拆成多层:

  • 完整构建与类型检查:确认内容、字体、图片和样式产物都完整;
  • npm run images:ensurenpm run images:verify:确认候选的收益门槛、字节数、尺寸和 manifest 一致;
  • 浏览器 Network:确认移动端选取较小候选图、响应式图片走 AVIF → WebP → 原图的正确回退链、CSS 背景引用带指纹的 token URL,且首屏不请求评论资源;
  • 浏览器人工回归:首页动画、文章封面、归档场景和留言页均保持正常。

最后一项尤其重要。性能改动常常跨越构建、缓存、网络、DOM 生命周期和样式层叠;单一分数不能证明它们在用户真实的浏览路径上都正确。

实现时的五个提醒

采用类似方案时,以下五点值得提前注意:

  • 给图片补尺寸后要回归检查特殊布局。 widthheight 能减少布局偏移,但绝对定位的插画、裁切封面等旧样式应明确保持正确的宽高比例。
  • 候选格式必须以实测收益决定。 不要因为源图已经是 WebP 就放弃大图的响应式候选,也不要因为 AVIF 更新就为小图和无收益图增加编码、解码与维护成本。
  • CSS 背景不要照搬 <picture> 它需要独立的 image-set()/token 回退链;只为确实作为大型背景使用的资源生成 token,避免主 CSS 膨胀。
  • 按需引入组件库时,主题变量不能替代组件 CSS。 若移除了全量入口,要同时保留实际使用组件的样式规则,否则 Card、Tag、Button 等会退回为未样式化元素。
  • 带页面切换的站点要把动态资源当作有生命周期的 DOM 节点。 动态加载的评论样式可能在路由切换后被替换;重新进入评论页时,应确认样式和字体确实仍在页面中,而不是只复用一次已经完成的加载 Promise。

结语

这次优化最有价值的部分并不是少了多少 KB,而是建立了一种更可靠的判断方式:先用 Lighthouse 找出值得怀疑的资源,再结合真实的页面体验决定如何处理。

保留动画并不意味着放弃性能。只要资源优先级、图片候选、缓存指纹和页面生命周期被正确处理,博客依然可以有一点缓慢登场的仪式感,同时不把不必要的负担交给每一次访问。

岛民留言

聊聊这篇

欢迎留下想法。邮箱不会公开,只用来识别你的留言身份。

留言区正在靠岸…

回到顶部