网站性能优化实战:PageSpeed Insights 从 47 分到 97 分的优化历程

This article is also available in the following language: English.

前言

最近为了优化站点体验, 我决定借助PageSpeed Insights的分析,专项优化站点性能表现.
本文起初基于Cactus主题进行优化, 从47分提高到了94分, 在撰写期间, 又将主题切换到了Icarus, 结果之前的部分优化失效.
不得不重新优化, 但是按照同样思路, 甚至把PageSpeed Insights的分数刷新到了97分.

初始状态分析

PageSpeed Insights 优化前得分 47 分

PageSpeed Insights 报告显示了以下主要问题:

指标 初始值 问题
First Contentful Paint (FCP) 7.1s 过慢
Largest Contentful Paint (LCP) 15.0s 严重过慢
Cumulative Layout Shift (CLS) 0.339 布局偏移严重
Speed Index 5.2s 加载体验差

主要的性能瓶颈包括:

  1. 字体文件过大:MesloLG 字体 TTF 格式高达 636KB
  2. Font Awesome CSS:加载了完整的图标库 (~19KB) 但只用了几个图标
  3. 缺少资源预连接:没有对 CDN 等关键资源进行 preconnect
  4. 图片未优化:Logo 图片 447x432 实际只显示 88x88
  5. 布局偏移:图片缺少 width/height 属性导致 CLS 问题

第一轮优化:资源瘦身

1. 字体格式转换 (TTF → WOFF2)

WOFF2 是目前最高效的 Web 字体格式,压缩率远超 TTF。

1
2
# 使用 ttf2woff2 转换字体
npx ttf2woff2 < MesloLGS-Regular.ttf > MesloLGS-Regular.woff2

效果:636KB → 160KB,减少 75%

然后更新 CSS 中的字体引用,优先使用 WOFF2:

1
2
3
4
5
6
7
@font-face
font-style: normal
font-family: "Meslo LG"
font-display: swap
src: local("Meslo LG S"),
url("../lib/meslo-LG/MesloLGS-Regular.woff2") format("woff2"),
url("../lib/meslo-LG/MesloLGS-Regular.ttf") format("truetype")

2. 添加 Preconnect 提示

<head> 中添加资源预连接,让浏览器提前建立 TCP 连接:

1
2
3
4
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
<link rel="preconnect" href="https://pagead2.googlesyndication.com" crossorigin>
<link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>

3. Logo 图片优化

原始 Logo 是 447x432 像素,实际显示只需要 88x88。

sips(Scriptable Image Processing System)是 macOS 自带的命令行图片处理工具,无需安装任何第三方软件即可完成图片缩放、格式转换等操作。

1
2
# 使用 sips 调整图片尺寸
sips -z 88 88 Logo_Sketch.png --out Logo_Sketch_88.png

PS:如果你使用的是 Linux 或需要更复杂的图片处理,可以使用 ImageMagick 的 convert 命令作为替代。

效果:32KB → 7.6KB,减少 76%

4. 修复 CLS 布局偏移

为 Logo 图片添加明确的尺寸属性:

1
<img id="logo" src="/images/Logo_Sketch_88.png" width="50" height="50" />

第一轮优化后得分 61 分

第一轮优化后得分:47 → 61 分


第二轮优化:移除 Font Awesome

分析发现 Font Awesome CSS 体积约 19KB,加上字体文件更大,但我只使用了以下几个图标:

  • ☰ 菜单图标 (fa-bars)
  • ‹ › 分页箭头 (fa-angle-left/right)
  • 社交媒体图标 (GitHub, YouTube, Bilibili 等)

解决方案:用 SVG 替换

1. 菜单图标

1
2
3
4
5
6
7
8
9
10
<!-- 之前 -->
<i class="fa-solid fa-bars fa-2x"></i>

<!-- 之后 -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>

2. 分页箭头

直接使用 Unicode 字符:

1
2
3
4
5
<!-- 之前 -->
<i class="fa-solid fa-angle-left"></i>

<!-- 之后 -->

3. 社交媒体图标

创建 SVG 图标映射对象:

1
2
3
4
5
6
var svgIcons = {
github: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31..."/></svg>',
youtube: '<svg ...>...</svg>',
bilibili: '<svg ...>...</svg>',
// ...
};

然后移除 Font Awesome CSS 引用:

1
2
<!-- 删除这一行 -->
<link rel="stylesheet" href="/lib/font-awesome/css/all.min.css" />

最终优化后得分 94 分

第二轮优化后得分:61 → 94 分(移动端),100 分(桌面端)

为什么砍掉 Font Awesome 效果这么惊人?

这里的核心逻辑在于**”关键请求链(Critical Request Chain)”**的坍塌。

优化前:浏览器必须经历 HTML → style.css → all.min.css → fa-solid-900.woff2 的四级瀑布流。每一级都需要消耗一次完整的 RTT(往返时延),在移动端弱网下,这 2.4s 的延迟就是这样堆叠出来的。

优化后:SVG 直接内联在 HTML 字节流中。当浏览器解析到图标位置时,绘图指令已经就绪,请求链长度直接从 4 降到了 0。

当我打开浏览器的 Network 面板,看到 all.min.css 慢吞吞地去握手请求,随后又触发了两个一百多 KB 的字体文件下载时,我坐不住了。整个页面为了那几个分页箭头和菜单图标,足足白屏等待了 2 秒。

为什么我最终决定对 Font Awesome “动刀”?
在 Android 开发中,我们绝不会为了显示三个图标就集成一个几 MB 的 SDK。但在 Web 开发中,引入 all.min.css 似乎成了某种“默认操作”。


第三轮优化:SEO 与死链接修复

在追求性能的同时,我发现了一个被忽视的 SEO 问题:物理源码中的死链接

1. 消除分页中的 /page/0/ 404 链接

很多 Hexo 主题(包括 Icarus)的分页组件在处理第一页和最后一页时,逻辑如下:

  • 在第一页时,”上一页”按钮虽然被 CSS 设置为 is-invisible(视觉不可见),但 HTML 源码中依然生成了 <a href="/page/0/">上一页</a>
  • 搜索引擎爬虫(如 Googlebot)是不会管 CSS 样式的,它会顺着 href 爬取。由于 /page/0/ 根本不存在,这会直接导致大量的 404 错误,拖累站点权重。

解决方案:组件本地化重写

我将分页组件 paginator.jsx 提取到本地,修改了渲染逻辑:

1
2
3
4
5
6
7
8
9
// 修改前的逻辑:无论如何都生成 <a>
<div class="pagination-previous">
<a href={getPageUrl(current - 1)}>{prevTitle}</a>
</div>

// 修改后的逻辑:无有效页码时直接渲染为 <span>,从物理层面消除链接
<div class={`pagination-previous${current > 1 ? '' : ' is-invisible'}`}>
{current > 1 ? <a href={getPageUrl(current - 1)}>{prevTitle}</a> : <span>{prevTitle}</span>}
</div>

2. 移除阻塞式的进度条组件 (Pace.js)

报告中显示 pace.min.js 是一个长链请求。虽然它能显示页面加载进度条,但:

  • 它位于 <head> 中,属于阻塞资源。
  • 它对 LCP(最大内容渲染)没有任何帮助,反而占用网络带宽。

操作:直接在 _config.icarus.yml 中将其禁用。


第四轮优化:现代图片格式与自适应尺寸

在解决了代码和字体层面的问题后,PageSpeed Insights 的建议列表里只剩下了最后的”大块头”:图片资源

报告指出,我的博客存在两个主要问题:

  1. 图片尺寸过大:例如 1536px 宽的封面图在手机上只显示为 ~360px,浪费了巨量带宽,直接拖慢了 LCP (最大内容渲染时间)。
  2. 格式老旧:仍在使用 JPG/PNG,而未利用压缩率更高的 WebP,错失了更好的压缩机会。

解决方案:自动化 WebP 转换与缩放

为了彻底解决这个问题,我编写了一个基于 nodejs sharp 库的自动化脚本,对全站主要图片进行了清洗:

1
2
3
4
5
6
7
// 脚本逻辑摘要:自动化批量处理
const sharp = require('sharp');
// ...
await sharp(src)
.resize({ width: 600 }) // 限制宽度为 600px (文章容器宽度),精准适配以消除"图片过大"警告
.webp({ quality: 80 }) // 转换为 WebP
.toFile(dest);

具体优化案例

  1. Ditherpunk 演示图 (returnofobradinn)

    • 优化前:533KB (JPG)
    • 优化后:~26KB (WebP, 672px)
    • 体积减少:~95%。这对 LCP 的提升是决定性的。
  2. 文章封面图 (esp32_dither_cover)

    • 优化前:1536x1587 resolution (533KB)
    • 优化后:672x694 resolution (~28KB)
    • 尺寸适配:严格匹配文章容器宽度 (672px),消除了 PageSpeed Insights 关于”图片尺寸过大”的警告。
  3. M5Stack Cardputer 封面图 (cardputer-streaming-cover)

    • 优化前:1200x900 resolution
    • 优化后:600x450 resolution (~20KB)
    • 针对显示区域较小的文章页,进一步将宽度限制在 600px 以匹配实际渲染尺寸 (599px)。
  4. 侧边栏缩略图

    • 优化前:~20KB (原图缩小显示)
    • 优化后:~3KB (WebP, 120px)
    • 针对侧边栏的小图,特意生成了 120px 宽度的微缩版本,避免了大图小用。
  5. 网站 Logo

    • 优化前:PNG 格式 (32KB)
    • 优化后:WebP 格式 (8.1KB),且支持透明背景

进一步压榨:调整压缩因子

虽然转换到了 WebP,但默认的 80 品质对于某些大图来说仍然过剩。PageSpeed Insights 提示我们“提高图片压缩因子”。于是我又对生成的 WebP 图片进行了二次处理:

1
2
3
4
5
6
7
// 使用 sharp 的极限压缩参数
.webp({
quality: 65, // 下调画质至 65
alphaQuality: 65, // 同时压缩 Alpha 通道
effort: 6, // 开启最高级别的压缩计算 (消耗更多 CPU 时间换取更小体积)
smartSubsample: true
})

这些细微的优化累积起来,最终让 LCP 分数直接拉满。
通过脚本批量处理,全站图片体积减少了 1MB 以上


第五轮优化:消除关键请求链阻塞

在解决了图片和字体体积后,PageSpeed Insights 依然提示存在 441ms 的”关键请求链阻塞”。罪魁祸首是指向 fonts.googleapis.com 的 CSS 请求及其后续的字体文件下载。

虽然我们在第一轮优化中引入了本地 WOFF2 字体,但 Hexo 主题 (layout/common/head.jsx) 默认仍会加载 Google Fonts (Ubuntu, Source Code Pro)。

优化动作

  1. 移除 Google Fonts:直接注释掉主题中加载 Google Fonts 的代码。由于我们已经配置了本地字体栈 (Meslo LG / System Fonts),这一步是安全的,且直接节省了 441ms 的阻塞时间。
  2. 异步加载非关键 CSS:对于代码高亮样式 (atom-one-light.css),它并不影响首屏内容的阅读(LCP 元素通常是标题或封面图)。我将其改为异步加载:
1
2
3
4
5
6
<!-- 优化前:阻塞渲染 -->
<link rel="stylesheet" href="/css/atom-one-light.css">

<!-- 优化后:显式预加载 + 异步应用 -->
<link rel="preload" as="style" href="..." onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="..."></noscript>

经过这一系列”手术”,关键请求链长度从 5 级降至 2 级,FCP (首次内容绘制) 时间大幅压缩。


优化成果总结

在编写这篇文章的期间, 一开始使用的主题是hexo-cactus-theme, 编写的过程中我切换到了hexo-icarus-theme.
刚切换到icarus时候,分数又回到了60以内, 但是经过本文描述的优化思路, 即使是首页展示图片而不是像cactus那也纯文字主页.
得分甚至来到了97分.

优化项 原始 (Icarus 默认) 优化后 减少
MesloLG 字体 636KB (TTF) 160KB (WOFF2) 75%
Logo 图片 32KB 8.1KB 74%
图片资源 JPG/PNG (大尺寸) WebP (自适应尺寸) > 90%
Font Awesome ~19KB + 字体 0KB (内联 SVG) 100%
Google Fonts 441ms 阻塞 0ms (移除) 100%
资源预连接 4 个 preconnect -
CLS 分数 0.339 ~0 -
PageSpeed 得分 Icarus 初始 优化后
移动端 47 97
桌面端 67 100

最终优化后得分 97 分

经验总结

  1. 字体是大头:Web 字体往往是最容易被忽视的性能杀手,WOFF2 格式是首选
  2. 图标库要慎用:如果只用几个图标,内联 SVG 比加载整个图标库更高效
  3. 图片要按需:确保图片尺寸与显示尺寸匹配
  4. 预连接有用:对于必须加载的第三方资源,preconnect 可以节省 DNS + TCP 时间
  5. CLS 容易修复:给图片加上明确的尺寸属性即可避免大部分布局偏移问题
  6. 缓存生命周期 (TTL):除了资源体积,还可以通过 Cloudflare 等 CDN 强制开启静态资源的长期缓存(Cache-Control: max-age=31536000)。这确保了用户在访问第二篇文章时,字体和 CSS 能从 Disk Cache 秒开,实现”瞬时”加载

工具推荐

希望这篇文章对你有所帮助!如果你也在进行网站性能优化,欢迎交流。

网站性能优化实战:PageSpeed Insights 从 47 分到 97 分的优化历程

https://chaosgoo.com/pagespeed-optimization-journey/

作者

Chaos Goo

发布于

2026-02-13

更新于

2026-02-13

许可协议