对博客而言,加载速度和页面大小就是第一体验,排版再精致,打开要转两三秒,读者大概率直接退回。今年我把博客从某通用平台迁到了自己用某静态站点生成器搭建的站点上,排版确实清爽了,但文章里的图一多,自己刷新都觉得卡:手机上图片一张张蹦,滚动时还能看到占位白块。于是我把“博客的加载速度和大小的优化、优化再优化”这件事认真做了一遍,从体检、砍体积、调传输链路,再到控制优化成果不回退,都有完整记录。这篇文章适合自己搭博客、图文内容多、还没系统做过性能优化的人参考,里面所有的操作都可以直接照着做。
1. 先别急着优化:用数据找出博客变慢的元凶
很多人拿到“博客慢”这个问题,第一反应是上网搜各种优化技巧,然后一股脑全加上。我一开始也这样干过,结果是折腾半天,速度没有本质提升,反而把页面搞复杂了。后来我学乖了:慢在哪,要用数据说话。
1.1 一次真实体检:首屏数据是怎么来的
体检方法很简单,用浏览器隐身窗口打开博客首页,按F12进入开发者工具,切到“网络”面板,勾选“禁用缓存”,然后刷新页面。记录下面几个数字:
- DOMContentLoaded:页面 HTML 解析完成的时间;
- Load:页面所有资源加载完成的时间;
- 请求总数与总传输大小:这次刷新总共下载了多少字节;
- 首屏加载时间(Largest Contentful Paint):用户看到的第一个主要内容出现的时间。
我当时的体检结果如下:
| 指标 | 实测值 |
|---|---|
| 首屏时间 | 2.7 秒 |
| 总传输大小 | 9.4 MB |
| 总请求数 | 138 个 |
| 最大单个资源 | 4 MB 的未压缩截图 |
这个结果放在今天任何博客平台上都是不合格的。但真正有价值的不是“它慢”,而是数字背后的分布。9.4MB 里面,图片占了 7.3MB,字体占了 1.2MB,剩下的是 JS、CSS 和 HTML。 这就是第一轮优化的方向:先把体积最大的部分砍下去,而不是先去调什么 CDN、改什么缓存。
1.2 四个瓶颈检查点:从数据里读出问题
网络面板里每个请求都列在那里,但我建议不要只看单个请求耗时,而是从四个维度去看整体状况:
- 请求数是否过多。我当时的 138 个请求里,光是外部站点就占了近 40 个——评论系统、统计脚本、字体服务、分享组件、图片托管,每个都要去建一次连接、发一次请求。请求多了之后,即使每个资源都不大,也架不住排队等待。
- 页面总体积是否过大。图片是绝对大户。做图文教程的博客尤其严重,编辑器里直接粘贴截图,一张就是 1MB 起步。
- 关键资源链路是否太长。我当时的 CSS 和 JS 都是同步加载,浏览器要等它们全部就绪才开始渲染,期间用户看到的就是白屏或空骨架。
- 缓存是否缺失。禁用缓存刷新时几乎把所有资源重新下载了一遍,说明我之前的加载完全没有把“回访用户”考虑进去。
这四个点不是并列关系,而是有优先级的。我的判断是:先解决“体积”,再解决“请求次数”,最后解决“浏览器渲染路径”。 体积决定了带宽占用,带宽占用直接决定加载时间的下限,所以先啃最难啃的骨头。
1.3 别被性能评分骗了:该盯的指标只有一个
在体检过程中,我还用浏览器自带的性能评分面板跑了一次,得分 62 分,面板给了一堆建议:什么“减少未使用的 JavaScript”“正确设置图片尺寸”等等。这类建议对不对?对。但对我的博客来说,最致命的问题就是图片体积太大,其他问题都是次要矛盾。
我想说的是:性能评分是一个综合指标,它不代表真实体验。 你真正应该追的,是下面几个可验证的数字:
- 首次打开时,首屏内容出现的累计等待时间;
- 首屏传输体积(关键资源的总字节数);
- 回访用户二次打开时,有多少资源直接命中缓存。
把这三个数字盯住,优化方向就不会跑偏。节省时间的唯一办法,是不要做无效优化。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 接着砍体积:图片、字体、代码的逐个击破
确定“体积”是主要矛盾之后,我开始按资源类型逐个处理。这一轮做完,全站总传输从 9.4MB 降到了 2.3MB,效果立竿见影。下面是每类资源的具体做法。
2.1 图片:博客体积最大头,先从格式和压缩下手
图片问题的根源有三类,几乎每个自建博客都能对上号:
- 从文档或聊天工具里直接粘贴截图,保存下来往往是一张 1MB 以上的 PNG,里面还有大块空白区域;
- 从相机或手机导出的照片,宽 1920px 甚至更大,但博客正文栏最大展示宽度才 700px;
- 透明背景 PNG 和带复杂渐变的图片,保存成传统格式后体积巨大。
我当时的处理流程是:
- 把每张图片的最大可用宽度定为 800px,超过的先在本地改尺寸;
- 用开源命令行批量压缩工具跑一遍,质量控制在 80,肉眼几乎看不出差别;
- 对照片类图片转成体积更小的现代格式,截图类图片用无损压缩模式;
- 给所有非首屏图片加上懒加载属性,让首屏优先加载。
效果最夸张的是某一篇教程:12 张截图,从 7.2MB 降到 1.3MB,其中最大的一张从 4MB 降到了 320KB。注意:先裁尺寸,再压缩,顺序不能反。 我见过很多人只压缩不裁尺寸,一张 1920px 宽的图压到 70% 质量,还是比裁好尺寸的图大好几倍。
2.2 字体:藏在浏览器里的体积黑洞
中文博客换网络字体非常吃亏:中文字体动辄 1MB 起步,一个字体文件包含几千个常用汉字,而一篇文章实际用到的可能只有几百个。当时我给博客配了一套正文和标题字体,两个字重加一起快 2.4MB,这个数字比整个 CSS 和 JS 加起来都大。
我做三件事:
第一,字体子集化。 用开源字体子集化工具,按页面实际出现的字符生成精简字体文件。比如一篇文章只用到 300 多个不同的汉字,那就只打包这几百个字符,字体文件从 1.2MB 降到 40KB 左右。不同页面按需加载自己需要的子集,全站字体体积直接缩小一个数量级。
第二,限制字重。 原来我同时引入了 400、600、700、900 几个字重,最终正常文章只用 400,标题用 700。把用不到的字重全部删掉,文件数量直接减半。
第三,设置字体显示策略。 用 CSS 的 font-display: swap,让文字先以系统字体显示,字体文件加载完成后自动替换。这样字体文件慢一点也不会造成白屏。
这里有个注意点:子集化后的字体不支持动态出现的字符。 如果你的页面里有用户评论区,大家会输入各种生僻字,子集可能缺字。我的方案是:针对渲染正文内容用子集字体,评论区走通用的系统字体,保证兜底。
2.3 代码体积:压缩只是及格线,拆包才能真正见效
图片和字体砍完之后,全站最大的尾大不掉就是 JavaScript 和 CSS。当时我引入了一个第三方评论组件,它自己带了完整的前端框架,首屏光这一个组件的 JS 就有 300KB 多。我的处理思路是:
- 构建阶段压缩。所有 JS/CSS 压缩后再上线,这是基础,不需要多解释;
- 拆包与按需加载。把首屏真正需要的代码单独打包成一个小文件,其余功能全部异步加载。例如评论模块只在用户滚动到底部时才拉取;
- 砍掉冗余依赖。那个评论组件实际只用了一个核心功能,我换成了更轻量的方式,把附带的统计、弹窗、动画全部去掉。
优化后,首屏必须加载的 JS 从 320KB 降到了 25KB 左右,CSS 从 280KB 降到 110KB。关键是首屏路径上非必需的代码全部移除了,这一步几乎立竿见影地缩短了白屏时间。
到这里,第一轮“体积优化”结束。我汇总一下前后对照:
| 资源类型 | 优化前 | 优化后 | 说明 |
|---|---|---|---|
| 图片 | 7.3 MB | 1.5 MB | 裁尺寸 + 压缩 + 现代格式 + 懒加载 |
| 字体 | 2.4 MB | 80 KB | 子集化 + 限字重 |
| JS | 1.8 MB | 620 KB | 压缩 + 拆包,首屏只加载 25KB |
| CSS | 280 KB | 110 KB | 压缩 + 去重,首屏关键路径内联 |
| 页面总传输 | 9.4 MB | 2.3 MB | 整体下降 75% |
3. 传输层的确定性收益:缓存、压缩与连接复用
体积砍完一轮,首屏时间从 2.7 秒下降到 1.5 秒左右。但回访用户依然感觉不够快,因为完全没有缓存策略,每次打开都在重新下载已经看过的资源。这一轮的处理重点是“传输层”的确定性优化——不是靠碰运气,而是实打实把机制弄对。
3.1 缓存策略:让回访用户真正感受到“秒开”
搞缓存之前必须分清两种资源:
- HTML 页面:内容会变,不能长期缓存;
- 带内容哈希的静态资源:文件名里带一串指纹,内容一变文件名就变,可以放心长期缓存。
我的配置是:HTML 走 no-cache + ETag 协商缓存,浏览器每次请求都会跟服务器确认是否有更新,没有更新就返回 304,几乎不消耗带宽。静态资源带上 max-age=31536000,一年内直接用本地缓存,连请求都不发。
为什么内容哈希之后可以放心用强缓存? 因为缓存是跟文件名绑定的。比如 app.8f3a2b.js 这个文件,只要代码不变,文件名永远不变,浏览器永远走本地缓存;哪天代码改了,构建后生成 app.c71d9e.js,浏览器发现是个新文件,自然重新下载。这就是“内容寻址”的思路,简单可靠。
实际操作上,要看你用的托管服务支持什么配置。大多数静态托管平台都有自定义响应头功能,在后台加几条规则就能完成。我当时配的规则如下:
| 资源类型 | 缓存策略 | 说明 |
|---|---|---|
| HTML | no-cache + ETag |
每次校验,内容变化立即生效 |
| 带哈希的静态资源 | public, max-age=31536000, immutable |
一年强缓存,文件名变则自动更新 |
| 图片 | public, max-age=31536000 |
更新时改名或加版本参数 |
| 字体 | public, max-age=31536000, immutable |
子集化后按页面加载 |
配置完成后,回访用户打开页面时,绝大多数资源直接命中本地缓存,几乎没有网络开销。这一项优化对“再次打开速度”的提升,比压缩体积还要明显。
3.2 压缩算法:gzip 换成 brotli,文本资源再瘦一圈
文本类资源(HTML、CSS、JS、JSON)在上线前再做一轮压缩,是常规操作。但大多数托管平台的默认配置只开了 gzip,而 brotli 算法对文本资源的压缩率通常比 gzip 高 10% 到 25%,体积越大的文本收益越明显。
我实测过两个版本:某个 CSS 文件 gzip 后 12KB,brotli 后 9KB;某 HTML 首页 gzip 后 18KB,brotli 后 14KB。整体文本资源大约又瘦了 15%,看起来不多,但在带宽有限的场景下,每 1KB 都是收益。
开启方式看平台:有些静态托管平台直接在后端开启 brotli;有些平台需要你自己在构建阶段预生成 .br 文件,服务器根据请求头自动下发。我的做法是后者,构建时用压缩工具批量生成 .br 版本,并保证原文件仍然存在作为回退。
这里要提醒一句:不要对图片再开文本压缩。 图片本身已经压缩过,再跑一遍 gzip/brotli 几乎没有体积收益,纯属浪费服务器算力。只对文本类资源做压缩就够了。
3.3 连接复用:HTTP/2 与预连接
回到 138 个请求这个数字。在 HTTP/1.1 时代,每个域名下浏览器通常只能并发 6 个左右的连接,其他请求都在排队。138 个请求意味着要等很多轮,即使每个请求只有几十毫秒,累积起来也是几百毫秒甚至上秒的延迟。
升级到 HTTP/2 后,最大的变化是多路复用:一个连接可以同时处理无数个请求,不再有“并发数限制”造成的排队。如果托管平台默认支持 HTTP/2,什么都不用做就能获得这一轮收益。我升级完之后,同一个页面的 138 个请求总耗时明显下降,尤其是那些大量小资源的页面,效果格外明显。
另外两个小技巧:
- 对站外资源用
preconnect提示浏览器提前建立连接,比如某评论服务、字体服务、统计服务。浏览器在解析到页面之前就先握手,等到请求真正发出时连接已经就绪; - 对首屏关键资源(自托管的主字体、首屏大图)用
preload提升优先级,让它们不要排在一堆不重要的资源后面。
但要注意,preload 不能滥用。把所有资源都标记成高优先级,等于没有优先级,反而可能抢占关键资源的带宽。我最后只对 3 个资源做了 preload:一个字体子集、首屏的一张关键图、一个首屏必需的 CSS 文件。
传输层优化结束后,我又做了一次完整体检,结果如下:
- 首次访问总传输 2.1MB(图片懒加载后按实际滚动逐步拉取);
- 回访访问总传输 210KB(绝大多数资源命中缓存);
- 首屏时间 1.2 秒;
- 请求数从 138 降到 96。
4. 让浏览器少干点活:关键渲染路径的梳理
体积和传输层都搞定了,最后一块硬骨头是“渲染路径”。有时候网络明明不慢,但浏览器半天画不出内容,原因就出在渲染阻塞资源上。这一轮的目标是:让浏览器在拿到 HTML 之后,能以最短路径完成首屏渲染。
4.1 渲染阻塞资源:样式和脚本的顺序问题
浏览器有两条铁律:
- CSS 不加载完,浏览器不敢渲染。因为渲染出的样式可能是错的,所以所有 CSS 默认都是渲染阻塞资源;
- 普通 JS 会阻塞 HTML 解析。浏览器遇到
<script>时会停下来,先下载并执行脚本,再继续解析后面的内容。
这就导致了一个常见问题:页面 HTML 加载得很快,但 CSS、JS 一个接一个阻塞在关键路径上,白屏时间被无限拉长。
我的处理方式是把 CSS 分成两部分:
- 首屏关键样式:只包含首屏真正用到的布局和样式,直接内联进 HTML;
- 其余样式:全部放在一个独立的 CSS 文件里,用异步方式加载,等首屏渲染完成后再补上。
JS 这边更简单:凡是首屏不需要的脚本,全部加上 defer 属性,让它们执行顺序延后,不再阻塞解析。
我给自己立了一条规矩:任何新脚本进场之前,先回答一个问题——它值不值首屏 100KB?
这条规矩后来帮我挡掉了不少“看起来有用但并非必需”的第三方功能。
4.2 内联与预加载的取舍:别把好东西玩坏
内联首屏 CSS 的思路很容易被执行得过头。有人把整个站点几千行的 CSS 全部内联进 HTML,结果 HTML 自己有几百 KB,反而比原来的方式更慢。
内联要克制。首屏关键样式控制在 10KB 左右比较合适,超过这个量就考虑拆分,只内联真正影响首屏外观的部分:顶部导航、文章标题、正文排版、首屏背景。评论区样式、代码高亮样式、侧边栏结构这些完全不需要内联。
同样是预加载,也要分轻重。我前面提到了 preload,这里补充一下它的适用场景:
- 首屏指定的字体子集文件:
preload,因为首屏文字渲染依赖它; - 首屏唯一的全宽大图:
preload,如果不提前拉取,可能被其他小资源挤到后面; - 第二屏及以后的图片、评论脚本、统计脚本:一律不加
preload,让它们在后台自然加载。
我记得有一次为了让页面看起来“性能很好”,一次性加了 8 个 preload 标签,结果首屏传输体积反而暴增,加载速度还变慢了。删到只剩 3 个之后立竿见影。预加载的本质是重新分配带宽优先级,只有把资源让给真正关键的少数节点,才有正向收益。
4.3 布局抖动与图片解码:容易被忽略的隐性成本
传输层的优化做得再好,如果浏览器在渲染时反复重排布局,体验依然会打折扣。我遇到过两个典型现象:
第一个是图片没有固定尺寸。 图片加载完成后,浏览器才知道这张图占多少高度,于是页面发生一次跳跃,已经读完的文字被顶下去,用户不得不重新定位。这就是累积布局位移。解决方法很简单:给每张图片预留宽高比,比如在 HTML 里直接写 width="800" height="450",或者用 CSS 的 aspect-ratio 锁定比例。
第二个是大图解码耗时。 在低端手机上,一张 2MB 的 JPEG 解码可能需要一两百毫秒,这个时间甚至比下载还长。把图片压缩并从传统格式换成更高效的现代格式之后,解码开销明显下降。这里再次印证了一个观点:压缩图片省下的不光是带宽,还有内存和解码时间,是双重收益。
懒加载方面也要注意时机。很多人的做法是直接给所有图片加 loading="lazy",但这会导致首屏图片也被延迟加载,出现“图片慢慢从下往上钻”的糟糕体验。我最后采用的方案:
- 首屏内图片不加懒加载;
- 距离当前视口较近的图片用低阈值触发加载,提前一点拉资源;
- 距离远的图片用更高的阈值延迟加载,不占用首屏带宽。
这些调整单独看都微不足道,但合在一起,首屏的流畅度和图片渐次出现的节奏都改善了不少。
5. 别让优化成果悄悄回退:建立防回退机制
优化做完最怕什么?最怕三个月后回头看,各项指标又打回原形。我身边不止一个朋友经历过这种“优化了个寂寞”:加了一个插件、换了一套字体、往文章里塞了一批大图,速度一夜回到解放前。所以最后一轮,我给自己建了一套简单的防回退机制。
5.1 定一个可执行的性能预算
没有预算的优化是不可持续的。我给自己定了几条硬性指标:
- 单篇文章首屏传输体积不超过 400KB;
- 全页面总传输不超过 2.5MB;
- 总请求数不超过 100 个;
- 首屏时间在普通网络环境下小于 1.5 秒。
定预算的原则是“够用且留余量”,不是越严格越好。如果你的网络环境和多数读者不一样,先用统计工具看看读者的真实网络状况,再确定合理阈值。预算确定之后,每次新增内容发布前,我都对照着检查一遍。
5.2 自动化检查:把体检变成日常动作
手动体检依赖毅力,不可靠。我在构建流程里加了一个体积检查脚本,在每次发布之前自动跑一遍。如果超出预算,构建直接失败,强制我回头处理新引入的资源。
脚本逻辑不复杂,核心就是扫描构建输出目录,统计总大小和指定文件的体积,与预值对比。下面是一个简化的版本:
javascript复制// check-size.mjs
import { readdir, stat } from 'fs/promises';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const root = join(dirname(fileURLToPath(import.meta.url)), 'site');
const limitMB = 4; // 预算:整个静态输出目录不超过 4MB
async function getSize(dir) {
let total = 0;
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const full = join(dir, entry.name);
total += entry.isDirectory() ? await getSize(full) : (await stat(full)).size;
}
return total;
}
const totalMB = (await getSize(root)) / 1024 / 1024;
console.log(`当前静态资源总大小:${totalMB.toFixed(2)}MB`);
if (totalMB > limitMB) {
console.error('超出性能预算,请检查新增资源');
process.exit(1);
}
这个脚本很简单,但非常有效。它把“优化回退”从感觉问题变成了构建异常,想偷懒都不行。
5.3 定期抽查与版本管理
自动检查负责“报警”,定期抽查负责“找原因”。我建议给自己设置一个低频提醒,比如每三个月花半小时打开几篇代表性页面刷一遍,看看浏览器网络面板里的数字有没有反弹。
回退的常见原因:
- 为了加一个分享按钮,引了一整包第三方脚本,顺带还拉取了字体和统计代码;
- 文章配图直接从手机上导出原图,忘了走压缩流程;
- 换了一款新的网络字体,忘记检查字体子集化;
- 为了视觉效果,加了一段全屏动效,间接引入了重型 JS 库。
发现回退之后,我最常用的定位方式是看版本管理提交记录。因为每次发布都走版本管理,所以哪一次提交引入了大文件,一拍一个准。保留干净的构建记录,是优化可持续的隐形前提。
我还养成了一个习惯:不追求性能分数一次性拉满,也不为单次分数波动焦虑。关键是让“正常打开一篇文章”这个场景保持稳定,让关键指标长期不越线。
最后的体会是,博客优化很像整理房间——不是一次大扫除就能一劳永逸,而是在每次写文章、装新功能时顺手维护一下。我现在每写一篇带图教程,发布前都会跑一遍批量压缩,然后用开发者工具看一次首屏传输体积,确认没有反弹才敢发。别太执着于把某个指标做到极致,把首屏体积和加载时间稳定在人人都能感知的“流畅线”以内,持续保持住,就已经是很了不起的成果了。加载速度和大小优化这件事,能一直做下去,本身就是最优解。
