网站优化的正确顺序:先量再改的五步清单

做站的前两年,我把「优化」理解成调参数:调 Nginx 的 worker 数、调 MySQL 的缓冲池、调 PHP 的进程数。后来发现一个事实——这些参数在站点还小的时候几乎不影响任何东西,真正拖慢一个站的往往是几件很简单的事:图片没压、第三方脚本太多、模板每个页面都在查数据库、以及服务器上残留了一堆自己都忘了的服务。这篇写我自己在几十个站上反复验证过的优化顺序,以及为什么顺序比单项技巧更重要。

第一步:先量,不要凭感觉改

优化最容易犯的错是「凭感觉」。觉得慢就去装个缓存插件,觉得卡就去加内存,改完之后体感没有变化,也说不清到底有没有变好。正确做法是先拿到三个数字:首字节时间(TTFB)、首屏内容绘制(LCP)、整页加载完成时间。前两个决定用户的第一印象,第三个决定跳出率。

# 拿 TTFB,连打十次看波动
for i in $(seq 10); do
  curl -s -o /dev/null -w '%{time_starttransfer}' "https://www.example.com/" 
  echo
done

十次的数值如果从 0.08 秒波动到 1.2 秒,说明问题不是「慢」而是「不稳定」,方向完全不同——不稳定通常是缓存未命中、数据库锁等待或者某个外部依赖超时,而不是单纯的参数不够。

我习惯把测量分成「服务器侧」和「浏览器侧」两套。服务器侧看 TTFB 和 PHP 执行时间,浏览器侧看 LCP 和总请求数。两边数据对不上才是最有价值的信息:服务器说 80 毫秒,浏览器说 3 秒,那 2.9 秒全在传输和渲染上,这时候去调 MySQL 是浪费时间。

第二步:静态资源,收益最大也最简单

在绝大多数内容站上,体积最大的问题来自图片和第三方脚本,而不是后端代码。这两块的优化收益远高于调任何参数。

图片:一张手机直出的照片通常 3~5MB,压到 200KB 以内肉眼看不出区别。格式上优先 WebP,兼容性已经不是问题(IE 之外所有浏览器都支持)。关键是要有明确的尺寸——很多站点的图片是「原图 + CSS 缩放」,浏览器下载了一张 3000 像素宽的图然后显示成 300 像素,浪费了 90% 的流量。

<img src="/uploads/photo-800.webp"
     srcset="/uploads/photo-400.webp 400w, /uploads/photo-800.webp 800w"
     sizes="(max-width: 600px) 400px, 800px"
     width="800" height="533"
     loading="lazy" decoding="async" alt="描述文字">

width 和 height 两个属性一定要写,否则图片加载时页面会跳动,这个跳动在性能指标里叫 CLS,直接影响体验评分。loading="lazy" 加在首屏之外的图上,首屏图绝对不要 lazy,否则会拖慢 LCP。

第三方脚本:这是最容易被忽视的杀手。统计代码、广告代码、字体、客服插件、分享按钮,每个都在首屏渲染时抢带宽和主线程。我做过一次对比:一个页面去掉四个第三方脚本后,LCP 从 3.8 秒降到 1.7 秒——什么都没改,只是不再加载那些东西。

处理方式分三档:能删的删掉(很多插件装了就没看过数据);不能删的延后加载(等页面 load 事件之后再注入);必须早加载的(比如广告)用 async 而不是同步阻塞。

// 延后注入第三方脚本,避免阻塞首屏
window.addEventListener('load', function () {
  var s = document.createElement('script');
  s.src = 'https://third-party.example.com/widget.js';
  s.async = true;
  document.body.appendChild(s);
});

第三步:后端,先查缓存再查 SQL

后端优化有一条铁律:能不查数据库就不查。内容站的页面结构是高度重复的,同一个首页在 10 分钟内被访问 500 次,大部分内容完全没有变化。这种场景下缓存命中率能轻松到 95% 以上。

缓存有三层,从外到内依次是:CDN/反向代理整页缓存、应用层对象缓存、数据库查询缓存。第三层在 MySQL 8.0 已经被移除了(因为命中率低、维护成本高),所以实际上只有前两层可用。

整页缓存的收益最大,配置也最直接。Nginx 只要几行:

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=page:100m inactive=60m max_size=2g;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 10m;
add_header X-Cache-Status $upstream_cache_status;

关键是要能看到命中情况。加了 X-Cache-Status 头之后,用 curl -I 看返回值是 HIT 还是 MISS,比盲猜强得多。如果一直是 MISS,常见原因是后台设了 Cache-Control: no-cache,或者响应里带 Set-Cookie(Nginx 默认不缓存带 Cookie 的响应)。

对象缓存适用于「整页不能缓存但局部能缓存」的场景,比如登录用户的侧栏、热门文章列表。APCu 是单机最省事的选择,装好之后 apcu_store / apcu_fetch 就能用,没有任何网络开销。

然后是 SQL。真正需要优化的 SQL 通常不超过总量的 5%,找到它们的方法不是猜,而是开慢查询日志:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 'ON';
-- 运行一段时间后分析
-- 也可以用 pt-query-digest 把日志按耗时汇总排序

慢查询日志里最常见的三类问题:缺少索引(WHERE 条件字段没有索引)、索引失效(字段被函数包裹,比如 WHERE DATE(created) = '2026-10-27')、以及一次性取太多行(LIMIT 缺失或者 SELECT *)。前两类加索引能解决,第三类要改代码。

第四步:清理服务器本身

这一步收益不确定,但成本极低,而且经常能发现大问题。我每个月会跑一次这个清单:

# 1. 看哪些服务在跑且开机自启
systemctl list-unit-files --state=enabled --type=service

# 2. 看哪些端口在监听(对外暴露面)
ss -tlnp

# 3. 看磁盘占用前 20 的目录
du -xh --max-depth=2 / 2>/dev/null | sort -rh | head -20

# 4. 看日志目录大小
journalctl --disk-usage

典型发现:装了某个软件后留下的守护进程一直在跑、日志文件涨到几 GB 没人清、某个自建服务在公网监听着 8080 端口。这些东西不加清理,会慢慢吃掉内存和 IO,表现出来就是「站点越用越慢」。

日志清理要顺手配好 logrotate,别用 rm 硬删——正在写入的日志文件被删了,进程还持有句柄,空间不会释放,只有重启进程才能回收。正确姿势是 truncate -s 0 或者用 logrotate 的 copytruncate。

# /etc/logrotate.d/site
/var/log/nginx/*.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    sharedscripts
    postrotate
        [ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
    endscript
}

第五步:网络层,域名和 TLS

域名数量和 TLS 握手是常被忽略的一块。每个独立域名都要单独做 DNS 解析和 TCP/TLS 握手,弱网环境下单次握手的代价能到几百毫秒。把静态资源收敛到一到两个域名,比开一堆 img1、img2、static 子域更划算——HTTP/2 之后多域名并发的优势已经消失,反而多了握手成本。

TLS 方面有三件事值得做:启用 TLS 1.3(握手往返从两次降到一次)、开启 session 复用(回访用户跳过完整握手)、以及确认证书链完整(缺中间证书时部分客户端会额外去拉,或者直接报错)。

# 检查证书链是否完整
openssl s_client -connect www.example.com:443 -servername www.example.com </dev/null 2>&1 | grep -E 'Verify return code|s:|i:'
# Verify return code 应为 0

为什么顺序不能颠倒

这五步的顺序不是随便排的。前三步(测量、静态资源、后端缓存)的收益占了整体提升的绝大部分,而且改动风险低、可回滚。后面两步(服务器清理、网络层)属于「锦上添花」,在站点规模小的时候收益有限。

反过来做——先调内核参数、先上对象存储、先研究 HTTP/3——是最常见的浪费。这些优化在「一张图 4MB」的站点上完全体现不出来,因为瓶颈根本不在那一层。优化的本质是找到当前最大的那个木桶短板,而不是把所有板都加高一遍。

最后一句经验:每次只改一件事,改完立刻测同一个指标。同时改三处,出了问题不知道是哪一处引起的,而且没法判断到底有没有效果。我见过太多「优化了一圈,站点反而更慢了」的情况,根源都是批量改动叠加。

常见误区:把「加服务器」当成优化

遇到慢就升配置,是很多人的第一反应。但升级 CPU 和内存只能解决「算力不够」这一类问题,而内容站的慢有相当大比例根本不是算力问题。我见过一个站从 2 核升到 8 核,TTFB 一点没变——因为瓶颈是一张没有索引的表,每次请求都要全表扫一遍,多出来的核心全在等同一把锁。

判断要不要升级配置,有一个简单的观察:在流量高峰时用 top 和 vmstat 看资源水位。CPU 长期低于 30%、内存没吃满、swap 没动静,那升级硬件基本没用,钱花在了不缺的地方。反过来,如果 vmstat 1 里 wa(IO 等待)长期偏高,说明瓶颈在磁盘,换更高主频的 CPU 同样无效,该看的是慢查询和缓存命中率。

vmstat 1 5
# 关注列:r(运行队列) b(阻塞) si/so(swap 换入换出) wa(IO 等待) 
# r 持续大于 CPU 核数 -> 算力真的不够
# wa 长期超过 20 -> 磁盘瓶颈,先查 SQL 和日志写入

另一个误区是「缓存越多越好」。缓存本身也有成本:内存占用、失效判断、以及最麻烦的——缓存穿透和缓存雪崩。给一个每天只被访问几百次的内页加整页缓存,命中率可能不到 5%,纯粹是浪费。优化的判断标准永远应该是「这个改动能减少多少实际工作量」,而不是「用了多少种技术」。

把优化固化成月度检查

优化不是一次性动作,而是持续的过程。站点在长,依赖在变,三个月前的最优配置现在可能已经不是。我现在的做法是把它固化成每月一次的三十分钟检查,只做四件事:

第一,重测 TTFB 和 LCP,和上个月的记录对比。数字没变化就是好消息,不用做任何事。

第二,看首页的总请求数和总体积,两个数字任一个明显上升,就去查是哪个新插件或新功能带进来的。

第三,跑一遍慢查询日志的汇总,只看新增的条目。

第四,过一遍 ss -tlnp,确认没有多出来的监听端口。

这四件事全部可以写成脚本,每月初由定时任务跑出来发到邮箱。这样优化的成本被摊薄到几乎为零,而收益是持续的——大部分性能问题在被用户感知到之前就已经被发现并处理掉了。

Last modification:October 27th, 2026 at 09:29 pm

Leave a Comment