TTFB(Time To First Byte,首字节时间)指的是从浏览器发出请求到收到服务器返回的第一个字节所经历的时间。它直接决定用户感知的「卡不卡」,也是 Core Web Vitals 中 LCP(最大内容绘制)的重要组成,同时被百度、Google 纳入排序参考。很多站长优化性能只盯着图片压缩和 CSS 合并,结果 TTFB 还是 800ms 以上——因为问题根本不在前端。这篇文章讲一条从测量到定位、再到逐层优化的完整路径。
一、先学会准确测量 TTFB
优化的前提是测量。curl 是最快的测量工具,一条命令就能拆出各个阶段的时间:
curl -o /dev/null -s -w "DNS: %{time_namelookup}s
TCP: %{time_connect}s
TLS: %{time_appconnect}s
TTFB: %{time_starttransfer}s
总耗时: %{time_total}s
" https://www.example.com/这里 time_starttransfer 就是 TTFB。建议测 10 次取中位数,单次结果受网络抖动影响很大。浏览器端可以用 DevTools 的 Network 面板看 TTFB 指标,也可以用 WebPageTest 从多个地域测,判断是不是地域性问题。拿到数据后先做一次粗定位:如果 DNS、TCP、TLS 加起来就占了大头,问题在网络链路或 CDN 节点;如果这三个都很快、TTFB 还是很高,那问题基本就在服务器后端处理上,也就是下面要讲的重点。
为了把「网络因素」和「服务器处理时间」彻底分开,建议在服务器本机再测一次:curl -o /dev/null -s -w "%{time_starttransfer}" http://127.0.0.1/。本机请求不经过公网,测出来的 TTFB 就是纯后端处理时间。如果本机很快、公网访问慢,说明瓶颈在网络回程或 CDN 节点,跟服务器配置无关;如果本机也慢,那就可以放心地把精力全部投入到数据库、PHP、Nginx 这三层优化上。这个「本机测一遍」的习惯能帮你少走一半弯路,很多站长在公网反复测了半天,最后发现慢的是跨国线路,服务器本身毫无问题。
二、数据库层:慢查询是 TTFB 的头号元凶
PHP/MySQL 架构的网站,后端处理时间绝大部分花在数据库上。一条没走索引的查询,在数据量上来之后轻松几百毫秒。第一步是打开慢查询日志,把「嫌疑人」抓出来:
# my.cnf slow_query_log = ON slow_query_log_file = /var/log/mysql/slow.log long_query_time = 1
重启 MySQL 后,跑几天再分析日志,重点看两类问题:一是出现频率高的慢查询,二是单次耗时极长的查询。用 mysqldumpslow -s c /var/log/mysql/slow.log 可以按出现次数排序。拿到慢查询 SQL 后,用 EXPLAIN 看执行计划:
EXPLAIN SELECT * FROM posts WHERE category_id=5 ORDER BY created_at DESC LIMIT 10;
看 type 字段:如果是 ALL(全表扫描)或 index,说明索引没建好;ref 和 range 才是健康状态。常见的修复手段:给 WHERE 和 ORDER BY 涉及的列建联合索引、避免在索引列上做函数运算、分页查询用延迟关联代替 OFFSET 大跳页。另外别忘了开查询缓存之外的杀手锏——MySQL 8.0 的 innodb_buffer_pool_size 要够大,热数据全在内存里,查询根本不用碰磁盘。
三、PHP 层:OPcache 与 PHP-FPM 参数调优
数据库没问题了,下一个瓶颈在 PHP 进程本身。第一件事是确认 OPcache 开着,它能把编译后的 PHP 字节码缓存起来,省掉每次请求重新编译的开销。检查 php -i | grep opcache.enable,如果是 Off,在 php.ini 里开启:
opcache.enable=1 opcache.memory_consumption=128 opcache.max_accelerated_files=10000 opcache.revalidate_freq=60
然后是 PHP-FPM 的进程管理模式。这是个人站长最常调错的配置,默认的 dynamic 模式在高并发下会频繁创建、销毁进程,反而拖慢响应。先看看当前每个 PHP-FPM 进程占多少内存:
ps aux | grep php-fpm | awk '{print $6/1024" MB"}'假设平均每个进程占 80MB,服务器可用内存 4GB,留出 1GB 给系统和 MySQL,那么 pm.max_children 可以设为 35 左右。生产环境我推荐用 static 模式,进程数固定,避免动态伸缩的抖动:
pm = static pm.max_children = 35
如果确实要开 dynamic,至少把 pm.start_servers、pm.min_spare_servers、pm.max_spare_servers 设成合理区间,别用默认值。改完 systemctl reload php8.x-fpm 生效。这里有个铁律:pm.max_children 宁可小一点也别超内存,进程数超过物理内存会触发 swap,整台服务器卡死,TTFB 直接飙到几秒。
四、Nginx 层:fastcgi 与 keepalive 优化
Nginx 作为前端代理,有几个配置直接影响 TTFB。第一是 fastcgi 缓冲,默认开启,但如果被显式关闭了,PHP 的输出会一点一点往浏览器挤,TTFB 和首屏都会变慢,确认配置里没有 fastcgi_buffering off 即可。第二是 upstream keepalive,让 Nginx 与 PHP-FPM 之间复用长连接,省去每次握手:
upstream php_fpm {
server unix:/run/php/php8.1-fpm.sock;
keepalive 32;
}
server {
location ~ \.php$ {
fastcgi_pass php_fpm;
fastcgi_keep_conn on;
include fastcgi_params;
}
}第三是 HTTP 层面的传输优化:开启 gzip 压缩 HTML 和文本资源,开启 HTTP/2(配合 TLS 1.3),静态资源设置长缓存。这些不能让 TTFB 变快多少,但能显著缩短总加载时间,配合 TTFB 一起改善用户体验。
五、CDN 与回源:把压力挡在边缘
网站上了 CDN 之后,用户访问的是边缘节点,TTFB 里网络部分会大幅改善。但要注意 CDN 掩盖不了源站慢:如果源站处理要 800ms,CDN 缓存没命中时用户照样慢。所以 CDN 优化要盯两件事:一是缓存命中率,静态资源命中率应该做到 95% 以上,动态页面按需做分片缓存;二是回源协议,源站和 CDN 之间保持 HTTP/2 或至少 HTTP/1.1 keepalive,避免每次回源都重新建连。用 CDN 服务商的控制台看「回源统计」,如果回源请求里出现大量慢请求,把源站的 TTFB 优化好,CDN 才能发挥全部价值。
六、引入 Redis 缓存层:把重复查询挡在应用之外
慢查询日志清干净之后,还有一个更彻底的思路:让热门数据根本不再打到数据库。个人网站 90% 的流量集中在少数页面(首页、热门文章、分类页),这些页面的查询结果是高度重复的。在 PHP 和 MySQL 之间加一层 Redis 缓存,命中时直接返回,TTFB 能再降一个量级。
以最常见的 Typecho/WordPress 类站点为例,缓存策略一般分两级:对象缓存和页面缓存。对象缓存缓存数据库查询结果,适合数据更新频繁、需要实时性的场景;页面缓存直接缓存渲染好的 HTML 片段,适合首页、文章列表这类变化慢的内容。用 Redis 做页面缓存的最小示例:
// PHP 伪代码:页面级缓存
$key = 'page:' . md5($_SERVER['REQUEST_URI']);
$html = $redis->get($key);
if ($html === false) {
// 生成页面(查库 + 渲染)
$html = render_page();
$redis->setex($key, 300, $html); // 缓存 5 分钟
}
echo $html;注意缓存键里带上 URI,不同 URL 的页面不会串数据;setex 的过期时间要根据内容更新频率定,文章发布后要主动删除对应缓存键,否则读者会看到旧内容。这里有一个非常常见的坑:Redis 默认只能存字符串,用 JSON 序列化数组时中文会被转成 \uXXXX,读取后记得 json_decode 还原,别直接拼字符串。
Redis 本身也要注意两件事:一是内存上限 maxmemory 和淘汰策略 allkeys-lru 要配好,否则缓存数据无限增长会把服务器磁盘写满;二是如果 Redis 只服务本机网站,绑 127.0.0.1 并设置密码,Redis 默认配置是裸奔的,直接暴露公网等于把服务器送给别人当肉鸡,这个教训每年都在发生。
七、优化效果如何验证
每改完一层,都回到第一步重新测量,用数据说话。我把 4GB 内存、单核 CPU 的小服务器完整走了一遍这套流程:慢查询日志抓出 3 条没走索引的列表查询并加了联合索引,PHP-FPM 从 dynamic 默认值改成 static 35 进程,确认 OPcache 开启,Nginx 加上 upstream keepalive,再给热门列表页套上 Redis 页面缓存。优化前首页 TTFB 平均 820ms,优化后稳定在 180ms 左右,LCP 从 3.2s 降到 1.4s,百度统计里的跳出率也明显下降。
最后补充几个优化过程中的常见疑问:TTFB 高但 CPU 和内存都空闲,优先查数据库慢查询和外网回程;单次请求慢、其余都快,多半是某条 SQL 或某个接口的问题,用 slow.log 和 Nginx 的 $request_time 字段交叉定位;CDN 开了之后 TTFB 反而更高,检查是否把动态页面也缓存了错误内容,或者回源链路走了跨地域线路。优化没有银弹,但每条路径都有指标可查。
总结一下排查顺序:先测量拆分,再查数据库慢查询,然后调 PHP 缓存和 FPM 进程,接着优化 Nginx 和 CDN 回源,最后用 Redis 把重复查询挡在应用之外。TTFB 优化没有玄学,每一步都有日志和指标可查,按这条路径走,任何一台配置不算太差的服务器都能把首字节时间压进 300ms 以内。