没有产品在购物车中。
TTFB过高怎么办?本文从实际案例出发,解析CDN缓存策略如何有效降低首字节时间,包含缓存配置、回源减少及性能优化技巧,帮助提升网站速度与用户体验。
在网络性能优化中,TTFB(Time to First Byte,首字节时间)几乎是所有性能问题里最容易被忽略、但影响最大的指标之一。大部分团队第一次做优化,都会优先盯着后端接口或服务器性能,实际测试的时候却发现即便在本地压测时源站响应已经压到100ms以内,但从海外实际访问时,TTFB依然可以高达800ms甚至1秒以上。 Google 研究报告显示,当 TTFB 超过 200ms 时,用户感知延迟会明显上升;一旦超过500ms,LCP(Largest Contentful Paint)基本很难达标。问题往往不在“服务器慢”,而在于:请求没有被正确地缓存,导致大量流量仍然在回源。
在实际项目中我们发现,即便已经接入CDN,很多站点却只把它当作“静态资源分发工具”,忽略了现代CDN边缘节点在连接复用、协议优化以及动态内容缓存方面的能力。这也是为什么,同样使用CDN,有的网站TTFB可以稳定在200ms以内,而有的却长期维持在800ms以上。今天我们就来通过一次真实测试,拆解如何通过优化CDN缓存策略,将TTFB降低50%以上。无论你是在管理全球分布的电商平台、优化高并发的 API 接口,还是实时互动的竞技游戏,这些策略都将帮助你突破性能瓶颈,为用户提供丝滑的响应体验。
TTFB全称Time To First Byte,简单说就是:从浏览器发出请求,到收到服务器返回的第一个字节的时间。它直接反映了用户第一次“感知到响应”的速度。你点了一个链接,浏览器开始转圈,屏幕还是白的。等到页面开始出现第一个文字或者第一个背景颜色的时候,TTFB早就已经结束了。用户感受上的“白屏时间”其实包含TTFB + 后续的下载和渲染。所以TTFB是白屏时间的重要组成部分,但不是全部。
实际排查中,TTFB高一般是这几个因素导致的:
DNS解析:如果用户用的DNS递归服务器慢,或者你的域名权威DNS响应慢,这一步就可能花掉几十甚至上百毫秒。但这一步通常归到DNS lookup,严格来说不算TTFB内部,但WebPageTest等工具会把DNS时间单独列出来。
TCP/TLS握手:一个HTTPS请求在发数据之前,要先完成TCP三次握手和TLS协商。如果是复用连接会快很多,但首次访问或者连接池失效时,这部分时间会显著拖累TTFB。我就曾遇到过因为TLS证书链配置,导致额外往返,TTFB凭空多了200ms。
CDN节点处理时间:CDN节点收到请求后,要查缓存、构造响应头、可能还要做一些边缘计算。大部分CDN节点的处理时间在1-5ms,但如果缓存规则复杂或者节点负载高,也可能到10-20ms。
回源延迟:如果缓存没命中,CDN节点必须向源站发起请求。源站可能在另一个洲,跨洲网络RTT 150-300ms很正常,加上源站自己的处理时间(PHP执行、数据库查询等),很容易就超过500ms。
TTFB高,本质上就是“回源太多 + 缓存命中率太低”。很多人一看到TTFB高,第一反应就是源站太慢了,然后去优化数据库、加Redis、升级PHP版本。这些虽然有用,但治标不治本,如果大部分请求还是要回源,那源站再快也快不过CDN边缘节点直接返回。所以只要缓存命中率足够高,TTFB就能稳定在边缘节点到用户之间的网络RTT + CDN节点处理时间,通常在50-150ms。源站再慢也影响不到。

本次测试基于一套实际线上运行的CDN缓存配置进行,以YewSafe的边缘缓存策略为参考架构,还原真实业务环境中的缓存命中与回源行为。
源站:部署在新加坡(AWS ap-southeast-1),一台c5.large实例,Nginx 1.24 + PHP-FPM 8.2。后端有一个MySQL数据库和一个Redis缓存。网站是一个典型的Magento 2电商站,产品页是动态生成的,但内容更新频率不高(每天改几次价格和库存)。
CDN边缘节点:使用了一组边缘节点配置,主要测试区域放在亚洲:东京、新加坡、孟买、法兰克福(覆盖部分欧洲用户)。
协议:HTTPS,TLS 1.3。证书由CDN统一管理,源站到CDN之间使用内部证书。
curl:我最信赖的工具。命令很简单:curl -w “@curl-format.txt” -o /dev/null -s “https://example.com/product/123”。curl-format.txt里可以定义输出变量,包括time_namelookup、time_connect、time_starttransfer等。time_starttransfer基本上就是TTFB。
WebPageTest:用来验证多地区表现。我在新加坡、东京、孟买、伦敦四个测试节点,跑9次测试取中位数。
Chrome DevTools:本地调试和肉眼验证。比如确认Cache-Control头是否正确下发,确认CDN有没有返回X-Cache: HIT。
我们选了一个产品详情页URL:/product/12345。这个页面大概200KB,包含HTML、6个JS、15个CSS、20多张图片。后端生成这个HTML平均需要180ms(包含数据库查询和模板渲染)。
针对同一个URL,依次应用三种缓存策略。每种策略先清空CDN缓存(确保冷启动),然后连续发起20次请求,取后10次的数据平均值。前几次可能经历缓存预热,取平均值会偏低。后10次基本稳定在稳态表现。
配置方法:在CDN上将该域名的缓存开关关闭,或者源站返回Cache-Control: no-store, no-cache, private。我用的后面这个,毕竟它更接近真实生产中的误配置场景。
实测结果(东京节点访问新加坡源站):
平均TTFB:923ms
最低值:780ms(凌晨网络空闲时)
最高值:1340ms(晚间高峰)
回源比例:100%
缓存命中率:0%
每次请求都从东京CDN节点回源到新加坡。网络RTT大约90-110ms,TLS握手另加一轮往返(如果复用连接会好一些,但首请求无法避免)。源站PHP处理180ms,加上数据库查询50ms,再加上CDN节点与源站之间的传输,最终900多ms其实已经算正常了。用户体感是点一下链接,接近1秒钟页面没有任何反应。
配置方法:这是大多数CDN的默认行为,对/static/、/media/、/js/、/css/等路径设置长缓存(max-age=31536000),但对HTML保持Cache-Control: no-cache。也就是说,每个HTML请求还是要回源验证。
这种方式的实测结果如下:
平均TTFB:712ms
最低值:610ms
最高值:1030ms
回源比例:约65%(因为HTML每次都回源,静态资源首次回源后续命中)
静态资源命中率:约88%
HTML命中率:0%
静态资源缓存比完全不缓存好了一点,但幅度有限。TTFB仍然受制于HTML回源。虽然静态资源从边缘返回可以加快后续加载,但对TTFB几乎没有帮助,毕竟TTFB只关心第一个字节,而第一个字节来自HTML响应头。只要HTML要回源,TTFB就降不下来。
配置方法:对HTML也设置缓存。一定要注意:动态内容不能乱缓存,否则用户可能看到别人的购物车。我是基于URL + 部分Cookie做Cache Key,并且设置合理的TTL。
具体配置:
Cache-Control: public, max-age=300, stale-while-revalidate=60
CDN边缘TTL:600秒
Cache Key包含:完整URL(包括query参数)、currency Cookie、store Cookie。忽略utm_*参数和PHPSESSID。
实测结果:
平均TTFB:287ms
最低值:142ms(东京节点命中缓存)
最高值:510ms(缓存过期后首次回源)
回源比例:约18%
缓存命中率:约82%
数据对比表(后10次平均值)
| 策略 | 平均TTFB | P95 TTFB | 回源比例 | 命中率 |
|---|---|---|---|---|
| 无缓存 | 923ms | 1250ms | 100% | 0% |
| 静态资源缓存 | 712ms | 980ms | 65% | 0% (HTML) |
| 全页面缓存 | 287ms | 410ms | 18% | 82% |
注:静态资源策略下,静态资源命中率约88%,但HTML始终回源,因此TTFB仍较高。
全页面缓存让TTFB直接砍掉了将近70%。注意P95从1250ms降到了410ms,长尾问题改善更明显。缓存命中时,CDN边缘节点几乎瞬时返回响应,不再受跨洲网络波动影响。
在实际测试中,不同CDN在缓存策略灵活性上的差异非常明显。 有些CDN只允许设置全局TTL,不能按路径区分,更不支持Cache Key定制。YewSafe 支持路径级规则和缓存键定制的方案,在降低回源请求和稳定TTFB方面有很大的优势。

很多人一看到 TTFB 高,第一反应就是后端太慢。但是你的源站在硅谷,用户在东京。光信号在光纤里跑一个来回,啥都不干,物理延迟就吃掉 150 到 200 毫秒。这还没算 TCP 三次握手、TLS 密钥交换,这些“握手税”在高延迟链路上跑起来,每一轮都得等一个 RTT。所以有时候源站 CPU 才用了 5%,用户那边已经等了半秒多了。
CDN 缓存能干的事,说白了就是用边缘节点的存储空间,换用户的时间。具体怎么换的呢?
第一,拉近物理距离:把内容推到用户所在城市的边缘节点上,请求就不用漂洋过海了。原本跨太平洋的传输,变成同城机房能够一下直接返回。我见过一个做游戏下载的客户,光是这部分优化,TTFB 就砍掉了 100 多毫秒,物理定律谁都绕不过,但你可以绕过距离。
第二,省掉握手往返时间:一次完整的 HTTPS 握手,在高延迟线路上来回跑好几趟,时间相当可观。CDN 节点离用户近,握手能秒完。而且 CDN 跟源站之间会维持一条长连接,相当于事先把水管铺好了。缓存失效需要回源的时候,CDN 直接拿现成的管子接水,不用重新铺。
第三,后端逻辑静默化:很多 API 接口(比如商品配置、公告列表)其实不需要每次实时算。把它们缓存在边缘节点上,哪怕只缓存几秒钟,原本要去撞数据库、跑代码的请求,直接从边缘内存里吐出来。对用户来说,TTFB 就是几十毫秒的事。
这就跟在你家楼下开个便利店一样。买瓶水下楼就行,不用每次都开车去郊区工厂。把路径从公里级压到米级,TTFB 降 50% 以上一点都不夸张。
那么具体怎么优化呢?下面这些策略是我过去几年在多个项目中验证过的,切实可行的优化策略
Cache-Control头是CDN行为的指令来源。源站返回什么,CDN通常就遵守什么(除非CDN强制覆盖,但我不建议这么做)。
静态资源(JS/CSS/图片/字体):
Cache-Control: public, max-age=31536000, immutable
我习惯用一年。immutable是个好东西,告诉浏览器和CDN这个资源永远不会变,连协商验证都不要做。更新时通过文件名hash来刷新。
HTML页面(非个性化):
Cache-Control: public, max-age=300, stale-while-revalidate=60
5分钟新鲜期,过期后60秒内允许用stale内容,同时CDN后台异步回源更新。这个策略既能保证内容不会太旧,又能避免缓存过期瞬间的请求全部回源。
API响应(可缓存的读接口):
Cache-Control: public, s-maxage=60, max-age=0
s-maxage只对CDN等共享缓存生效,max-age=0告诉浏览器不要缓存。这样CDN可以缓存60秒,但每次浏览器请求都会到CDN验证(如果CDN有缓存则直接返回,没有就回源)。
个性化内容(用户登录后的首页、购物车接口):
Cache-Control: private, no-store
完全不缓存。CDN必须透传到源站。如果一定要缓存,要基于用户ID做Cache Key。
同一个域名下,不同路径的更新频率和个性化程度不一样。我习惯在CDN控制台里配置类似下面的规则:
| 路径模式 | 边缘TTL | Cache-Control | 说明 |
|---|---|---|---|
| /static/* | 1年 | public, max-age=31536000, immutable | 构建产物 |
| /media/catalog/product/* | 7天 | public, max-age=604800 | 产品图片,很少变 |
| /product/* | 1小时 | public, s-maxage=3600, max-age=0 | 产品详情页,价格库存变化频率中等 |
| /category/* | 5分钟 | public, max-age=300, stale-while-revalidate=60 | 分类列表页,内容变化快 |
| /customer/* | 无缓存 | private, no-store | 用户中心,必须实时 |
| /api/v1/prices | 10秒 | public, s-maxage=10 | 价格接口,需要较高实时性 |
这样进行精细控制,既保证性能又避免缓存错误。
Cache Key是CDN用来判断“两个请求是否相同”的依据。默认情况下,CDN会用完整的URL(包括query参数)加上一些请求头作为Cache Key。但很多时候,URL里带着无关参数,导致明明相同的内容却被缓存成多份,命中率直线下降。
去年我们就遇到一个这样的情况:一个网站的首页URL被加上各种追踪参数 ?utm_source=facebook&utm_campaign=summer,每个campaign不同,结果首页在CDN里缓存了上千个副本,命中率不到10%。TTFB经常回源,平均800ms。
这种情况我们只需要在CDN配置中忽略那些不影响响应内容的参数就可以解决,那么CDN配置中可以忽略的参数有哪些呢?
应该忽略的参数:
utm_source, utm_medium, utm_campaign, utm_term, utm_content
fbclid, gclid, msclkid
ref, referrer
_ga, _gl
各种A/B测试参数(除非测试版本确实改变页面内容)
应该保留的参数:
id, sku, product_id
page, offset, limit(分页参数)
sort, order(如果改变排序)
currency, store(多币种/多门店场景)
除了query参数,Cookie也可能污染Cache Key。有些CDN默认把整个Cookie头纳入Cache Key,这样只要用户带了个PHPSESSID,每个用户都会产生独立的缓存副本。正确做法是只将影响响应内容的Cookie字段纳入Cache Key,比如currency、store_code,而忽略PHPSESSID、_ga等。
在实际优化中,Cache Key设计直接影响命中率。而YewSafe支持高级缓存控制,可以灵活定义缓存键规则,忽略特定查询参数、自定义包含哪些请求头、选择性纳入Cookie中的特定字段,从而避免动态参数导致缓存失效。
TTL(Time To Live)是缓存的有效期。设置太短,回源频繁;设置太长,内容更新不及时。
下面是我的一些经验值,大家可以根据业务进行调整:
| 资源类型 | 推荐TTL | 为什么 |
|---|---|---|
| 带hash的静态资源(JS/CSS/字体) | 1年 | 内容不变,更新靠改文件名 |
| 用户头像、普通图片 | 1个月 | 偶尔变,变了影响不大 |
| 产品主图、品牌banner | 7天 | 更新频率中等 |
| 博客文章内容 | 1天 | 如果不常更新的话 |
| 产品详情页 | 1小时 | 价格库存变化频率中等 |
| 首页、分类页 | 5分钟 | 聚合内容变化较快 |
| 实时数据接口(库存、价格) | 1分钟以内 | 需要较高实时性 |
另外,善用stale-while-revalidate。比如设置max-age=300, stale-while-revalidate=60,表示缓存过期后60秒内仍然可以返回旧内容,同时异步去源站拉取新内容。这样用户永远不会因为缓存过期而等待回源,TTFB保持稳定。合理设置边缘缓存TTL可以显著降低回源频率。
第二个是定时预热:有些站点每天的访问量波动特别明显,比如白天是高峰、晚上没人,或者特定时间段流量暴增。这种情况,我们就可以在流量波峰来之前,提前预热那些热门页面,比如早上9点前预热一遍,等用户上班后集中访问,缓存已经准备好了,TTFB就能一直很稳。
第三个是智能预热:根据网站的访问日志,分析预测哪些URL可能会成为热点,比如某个活动页面、某个爆款产品页,提前把这些URL推送到全球主要的边缘节点。不用我们手动判断,靠数据说话,能精准覆盖潜在的高访问量页面。

当你把基础的缓存策略做到位后,TTFB可能已经降到了200-300ms。如果想进一步压到100ms以内,还可以尝试以下进阶手段。
不同地区的用户对内容新鲜度的要求可能不同。比如,针对欧美市场(主站流量区)可以设置较短TTL确保内容及时更新,而针对新兴市场(如南美、非洲)可以设置更长TTL以减少回源,因为那些地区的网络延迟本身就高。可以在CDN配置中按区域(Region)或国家设置不同的TTL和缓存规则。一些CDN支持通过Edge Logic实现条件判断。
单一CDN服务商不可能在全球所有地区都是最快的。比如,某CDN在北美很强,但在东南亚可能丢包率高;另一个CDN在欧洲不错,但在南美节点少。
多CDN策略是指同时接入两家或以上CDN,通过DNS智能解析或客户端测速,实时将用户调度到当前性能最好的CDN上。这能显著降低P95 TTFB,并提高整体可用性。但多CDN会增加运维复杂度(配置同步、日志聚合、成本分摊)。一般只有对性能要求极高的大型网站才会采用。
把一些轻量级的逻辑从源站搬到CDN边缘节点,可以避免不必要的回源。
比如:
边缘重定向:根据User-Agent判断设备类型,直接在边缘返回302重定向到移动版URL,不用回源。
边缘身份验证:JWT验证在边缘完成,无效请求直接拒绝,不回源。
边缘数据聚合:组合两个后端API的响应,减少客户端请求次数。
通过边缘计算,可以进一步降低那些原本必须回源的请求的TTFB。因为边缘节点离用户更近,且处理能力足够快。
讲完了怎么优化,得说说哪些坑我踩过,希望我的经验能给你避免一些不必要的麻烦。
记得我刚入行没多久的时候给一个客户做配置,当时图省事,想着“全页面缓存效果这么好,那就全上吧”,结果忘了某个页面里藏着用户的昵称和购物车。过了半小时,客服过来说“好多用户反映看到别人的账号”。我一下就慌了,赶紧回滚配置。后来但凡跟用户身份沾边的URL,要么直接 Cache-Control: private, no-store,要么非得缓存的话,Cache Key 里必须带上用户ID。
我见过有人给首页设了30秒TTL,还特别得意说“我们缓存了”。这样30秒过期,一天要回源将近3000次,命中率不到20%,TTFB当然下不来。还有人给产品价格接口设了1小时TTL,结果库存卖光了页面还显示有货,运营气得直拍桌子。切记:能接受多久的延迟,就设多久的TTL。拿不准就先设短一点,观察命中率,再慢慢往上调。别想一口吃成胖子。
假设你的产品页TTL都是一小时,那每到整点,所有产品页同时过期。下一波请求涌进来,CDN发现缓存都没了,于是一窝蜂全去回源。源站瞬间被几百上千个并发打满,轻则响应变慢,重则直接502。那这该怎么办呢?一是给TTL加随机偏移量,比如3600到3900秒之间随机;二是看CDN支不支持“请求合并”(Request Collapsing),也就是同一个资源过期后,只让第一个请求回源,其他的排队等着。
有些CDN产品看着便宜,用起来才发现根本不能按路径设不同TTL,Cache Key只能全URL匹配,甚至不支持 stale-while-revalidate。你前面做的那些精细配置,根本施展不开。选择CDN一定要专门测试一下:能不能忽略query参数、能不能按路径配规则、支不支持请求合并。这些能力直接决定了TTFB优化的天花板。
TTFB优化的关键,不在于单纯提升源站性能,而在于减少回源请求。通过合理的CDN缓存策略(尤其是全页面缓存与缓存键优化),在实际环境中实现30%-50%的TTFB下降是完全可行的。
问:TTFB到底是网络时间还是服务器时间?
两者都有影响。具体比例取决于是否命中缓存。命中时主要是网络RTT + CDN处理;未命中时还要加上回源网络和源站处理。
问:动态页面能不能缓存?
能。通过Cache Key区分不同状态(如货币、门店),同时设置合理的TTL。完全个性化内容(如购物车)不建议缓存。
问:Cache-Control里的s-maxage和max-age有什么区别?
s-maxage只对CDN等共享缓存生效,max-age对浏览器和CDN都生效。通常给CDN设置s-maxage,浏览器用max-age=0避免浏览器缓存。
问:为什么用了CDN但TTFB还是高?
检查命中率。如果命中率低,大概率是HTML没有缓存,或者Cache Key设计不合理导致缓存碎片化。
问:如何测试CDN缓存是否生效?
用curl查看响应头:curl -I https://example.com,看是否有X-Cache: HIT或CF-Cache-Status: HIT(取决于CDN厂商)。同时对比加缓存前后的TTFB数值。
为了方便大家快速落地,我整理了一份Nginx源站配置 + CDN规则配置模板。你可以根据实际业务调整:
源站Nginx配置(/etc/nginx/sites-available/example.com):
server {
listen 443 ssl http2;
server_name example.com;
# 静态资源(带hash)
location ~* \.(js|css|woff2?|ttf|eot|ico)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 图片资源
location ~* \.(jpg|jpeg|png|gif|webp|svg)$ {
expires 30d;
add_header Cache-Control "public";
}
# HTML页面 - 动态生成,但允许CDN缓存
location / {
# 默认HTML缓存策略
add_header Cache-Control "public, max-age=300, stale-while-revalidate=60";
try_files $uri $uri/ /index.php?$args;
}
# API接口 - 短缓存
location /api/ {
add_header Cache-Control "public, s-maxage=60, max-age=0";
try_files $uri $uri/ /index.php?$args;
}
# 用户中心 - 不缓存
location /customer/ {
add_header Cache-Control "private, no-store";
try_files $uri $uri/ /index.php?$args;
}
# PHP处理
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
CDN缓存规则配置建议
| 路径 | 边缘TTL | 缓存键规则 |
|---|---|---|
| /static/* | 31536000秒(1年) | 忽略查询参数 |
| /images/* | 604800秒(7天) | 忽略追踪参数 |
| /products/* | 3600秒(1小时) | 保留ID参数,忽略排序/分页参数 |
| /api/* | 60秒 | 保留关键参数 |
| / | 300秒 | 忽略所有查询参数 |
Cache Key优化规则:
忽略的查询参数:utm_source、utm_medium、utm_campaign、utm_term、utm_content、fbclid、gclid、ref、_ga等追踪和分析参数
保留的查询参数:id、slug、page、category等影响内容的关键参数
不纳入Cache Key的请求头:User-Agent(除非内容需要针对设备类型差异化)