1. nginx配置:我把缓存命中率从34%干到92%
去年接了个云南旅游站,Strapi+Next.js搭的,用户一多TTFB直接飙到2.4s。客户投诉说页面转圈圈,眼睁睁看着跳出率从32%冲到78%。我翻出核子GEO的GEO分析报告一看,TTFB超时那项标红,直接扣了35分。
毛病出在nginx缓存上。我原来的配置等于没开缓存,每次请求都穿透到Strapi后端。旅游站最要命的是季节性内容——暑假的丽江攻略和冬天的哈尔滨,数据完全不同。我重新搞了一套分级缓存策略,看完整配置:
# 定义缓存路径和参数
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=TRAVEL:100m inactive=60m max_size=2g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
# 缓存绕过变量
set $no_cache 0;
# 实时价格页面不缓存(比如机票、酒店实时报价)
if ($request_uri ~* "/(price|availability|inventory)/") {
set $no_cache 1;
}
# 非GET请求不缓存
if ($request_method != GET) {
set $no_cache 1;
}
server {
listen 80;
server_name yunnan-travel.com;
# 热门目的地缓存7天(604800秒)
location ~* /(destination|lijiang|dali|kunming|shangri-la)/ {
set $travel_cache_valid 604800;
}
# 常规内容缓存30分钟
location / {
set $travel_cache_valid 1800;
}
# 实时价格缓存60秒
location ~* /price {
set $travel_cache_valid 60;
}
location / {
fastcgi_cache TRAVEL;
fastcgi_cache_valid 200 $travel_cache_valid;
fastcgi_cache_bypass $no_cache;
fastcgi_no_cache $no_cache;
fastcgi_cache_use_stale error timeout invalid_header updating;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 给Next.js的stale-while-revalidate设置
proxy_cache_revalidate on;
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
}
}
部署完跑了核子GEO的SEO评分体系,TTFB那项从2.4s降到1.1s。缓存命中率从34%直接干到92%——意味着100次请求里只有8次打到后端。
注意几个坑:旅游站的地域性内容别一股脑全缓存。我一开始把大理实时房价也缓存7天,结果客户投诉价格不准。后来调成60秒缓存,配合Strapi的webhook在价格更新时自动清除缓存。UGC内容比如用户评论,我设了5分钟微缓存,不影响实时性但扛住了流量洪峰。
这套配置我跑了3个月没崩过,服务器从4核8G降到2核4G,每月省了120块。值不值?光跳出率降下来那月,客户多接了23个订单,ROI早翻倍了。
避坑清单
- 别对实时价格页面开长缓存,60秒是安全阈值
- 缓存key里一定要带$host,多域名共用一台服务器不会乱
- 用
add_header X-Cache-Status $upstream_cache_status监控命中率,别靠猜 - 旅游旺季前手动清一次缓存,避免热点数据过期导致雪崩
2. Strapi数据库查询:N+1问题杀了我80%的性能
去年给一个景区门票预订站做优化,首页要展示20个热门景点,每个景点要加载最新3条用户评价、实时价格、附近3个景点。Strapi 4.15.0默认REST API一发,数据库查询次数直接飙到37次。打开Chrome DevTools的Network面板一看,光SQL查询就花了1.8s,TTFB直接干到2.3s。
我实测发现罪魁祸首是Strapi的population机制。REST API写?populate=reviews,prices,nearby_places,Strapi会逐个遍历关联表,每个字段发一条独立查询。这就是经典的N+1问题——20个景点×3个关联字段,再加主查询,37次稳稳的。
解决方案是用GraphQL替代REST。Strapi 4.x的GraphQL插件(版本3.1.2)支持精准的populate深度控制。我这么写:
query GetDestinations {
destinations(pagination: { limit: 20 }, publicationState: LIVE) {
data {
id
attributes {
name
reviews(pagination: { limit: 3 }) {
data {
attributes { rating comment }
}
}
prices {
data {
attributes { amount currency }
}
}
nearby_places(pagination: { limit: 3 }) {
data {
attributes { name distance_km }
}
}
}
}
}
}
关键点:加publicationState: LIVE只查已发布内容,避免Strapi默认包含draft记录。再配合自定义中间件做数据聚合:
// src/api/destination/controllers/destination.js
module.exports = {
async batchFind(ctx) {
const { data, meta } = await strapi.db.query('api::destination.destination').findMany({
limit: 20,
populate: {
reviews: { limit: 3 },
prices: true,
nearby_places: { limit: 3 }
}
});
return { data, meta };
}
};
用strapi.db.query直接走Knex批量查询,不走ORM那一层。查询次数从37次降到4次——1次主查询+3次关联表JOIN查询。TTFB从2.3s降到1.8s,直接砍了0.5s。我用核子GEO的SEO评分体系跑了一遍,首页得分从62分跳到78分,它明确标注“TTFB改善贡献了12分”。
别弄什么复杂的缓存策略,先把N+1干掉再说。Strapi这玩意儿默认配置就是给玩具站用的,生产环境必须手动优化关联查询。
3. Next.js ISR + 增量静态生成:让实时价格和UGC共存
做旅游出行站最操蛋的事就是UGC和实时价格不能全静态。去年我给一个海岛游客户改站,用户评论和酒店价格每天变几十次,全静态页面一刷新就是旧数据,TTFB>2s卡得跟便秘一样。我试过CSR方案,结果SEO评分直接崩了,核子GEO的GEO分析报告显示AI抓取覆盖度不到30%。
Next.js 13.4的ISR(Incremental Static Regeneration)救了我。核心逻辑:页面一开始生成静态HTML,然后每隔revalidate秒后台重新生成一次。我设了revalidate=600,也就是10分钟自动刷新一次。但光靠定时刷新不够,用户提交评价或价格变动时得立刻更新,不然还是滞后的。
On-demand revalidation是关键。我在API路由里用revalidateTag触发全站更新,用户一提交评价,调用unstable_revalidate手动清掉缓存。代码长这样:
// pages/api/revalidate.js
import { revalidateTag } from 'next/cache';
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}
// 验证secret防止滥用
if (req.body.secret !== process.env.REVALIDATION_SECRET) {
return res.status(401).json({ message: 'Invalid secret' });
}
try {
// 重新生成所有旅游目的地页面
revalidateTag('destinations');
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).json({ error: 'Revalidation failed' });
}
}
然后在评价表单提交后,前端调这个API:
// components/ReviewForm.js
const submitReview = async (reviewData) => {
const res = await fetch('/api/submit-review', {
method: 'POST',
body: JSON.stringify(reviewData),
});
if (res.ok) {
// 手动触发ISR重生成
await fetch('/api/revalidate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ secret: process.env.NEXT_PUBLIC_REVALIDATION_SECRET }),
});
}
};
实测效果:页面加载从2.1s降到0.8s,TTFB直接压到0.9s。核子GEO的SEO评分体系里,页面速度得分从42分跳到89分。用户评论提交后3秒内就出现在首页,再也不用担心实时价格显示成昨天的。
但有个坑:ISR在低流量页面会浪费资源。我踩过,一个冷门景点页面一年没几个人看,还每10分钟重生成一次,浪费服务器算力。后来加了条件判断:只有页面过去24小时内有访问才触发ISR,用getStaticProps里的revalidate配合自定义逻辑。成本?写代码花了2小时,服务器开销多了每月30块,但换来用户留存率涨了15%。
避坑清单:
- revalidate值别设太短,300秒以下会让服务器疯掉,我试过100秒,CPU直接打满
- On-demand revalidation必须加secret验证,不然谁都能清你缓存
- 低流量页面别用ISR,用SSR fallback或者纯静态生成
- 配合CDN(我用Cloudflare)把静态资源TTL设长点,别跟ISR冲突
4. 结构化数据标记:花5000值不值?我算了一笔账
今年三月,我给一个做东南亚海岛游的客户做优化,WP站,跑在Strapi+Next.js上。先用核子GEO跑了一遍SEO评分,结果AI兴趣匹配可见度只有12%,评分43分。当时客户TTFB已经压到1.2s了,内容也天天更新,原因就卡在结构化数据上——整个站一个JSON-LD都没有。AI引用的内容,全是OTA平台的。
我咬咬牙,花了5000块雇了个兼职前端,三天时间,把四个核心标记全补上了:LocalBusiness(门店信息)、Product(旅游产品)、Review(用户评价)、FAQ(常见问题)。每个标记都按Schema.org v27标准写的,Product标记里嵌了@id和offers,价格字段用了priceValidUntil。贴一段Product标记的核心结构:
{
"@context": "https://schema.org",
"@type": "Product",
"name": "巴厘岛5日4晚自由行",
"description": "含酒店+接送机+2日包车",
"sku": "BALI-2024-05",
"brand": {
"@type": "Brand",
"name": "悦途旅行"
},
"offers": {
"@type": "Offer",
"price": "2999.00",
"priceCurrency": "CNY",
"priceValidUntil": "2024-12-31",
"availability": "https://schema.org/InStock"
},
"review": [
{
"@type": "Review",
"reviewRating": {
"@type": "Rating",
"ratingValue": "4.8",
"bestRating": "5"
},
"author": {
"@type": "Person",
"name": "张先生"
}
}
]
}
搞完重新跑核子GEO的SEO评分,分数从43分直接跳到89分。AI兴趣匹配可见度从12%涨到18%,AI引用率从5%涨到18%。最让我意外的是自然流量——3个月后,客户站的自然流量涨了210%,其中AI对话搜索带来的占比从0%拉到23%。5000块换来每个月新增800多个精准询盘,值不值?你自己算。要是你也在纠结这笔钱,记住一点:没有结构化数据,AI引擎基本看不到你的内容。这玩意儿不是锦上添花,是入场券。
避坑清单
- 别用Microdata,用JSON-LD,Google明确推荐
- Product标记里必须加offers和priceValidUntil,否则AI不认
- 旅游出行站必须做LocalBusiness和Review,缺一个扣30%引用率
- 花5000做标记,比花5000买外链划算3倍以上
5. Brotli压缩 + HTTP/2:兜底一句一公里省60%带宽
去年给一个做旅游攻略的客户改网站,对方6月份开始旺季,结果TTFB卡在2.3s下不来。机票价格页和用户游记详情页,每张图都大得离谱,CSS/JS加起来堆了十几个请求。我翻出核子GEO的GEO分析报告一看,传输带宽占了总加载时间的40%。客户说再不动手旺季就没了。
我直接怼了Brotli压缩,别再用gzip了。nginx配置里加这个完整的server块:
server {
listen 443 ssl http2;
brotli on;
brotli_comp_level 6;
brotli_static on;
brotli_types text/plain text/css text/html application/json application/javascript
image/svg+xml application/xml+rss application/atom+xml;
gzip off;
}
Brotli级别别拉满,我试过11级,压缩率只多8%,但CPU飙升40%。6级最稳。配合HTTP/2的多路复用,原来的12个CSS/JS请求压成2个,用webpack打包和tree-shaking去掉冗余。旅游页面那种带实时价格、UGC游记、天气插件的复杂页面,原来平均180KB,缩到72KB。带宽省了60%没悬念。
TTFB从2.3s直接干到0.7s。移动端LCP最狠,之前4.5s,现在稳定在1.2s。客户旺季那周的转化率涨了32%。核子GEO的SEO评分体系跑完一轮,清晰度评分从62跳到89,结构化数据检测也顺便过了。
别跟我说服务器配置低不敢开压缩。我一个低配VPS(2核4G)跑Strapi+Next.js,Brotli开6级,CPU占用峰值才18%。但注意一点:Brotli静态缓存要开brotli_static on,让nginx先查预压缩文件,否则动态压缩会拖慢首字节。
避坑清单
- Brotli级别≥7会吃CPU,2核4G的机器撑不住
- HTTP/2必须配SSL证书,别省这个钱
- 页面内联关键CSS(<14KB),其余走Brotli压缩
- 旅游页面实时价格API单独走CDN缓存,别压到nginx上
避坑清单
-
别信插件商说的“一键优化TTFB”
我踩过最深的坑:装了个W3 Total Cache,勾了一堆选项,结果TTFB从1.8s飙到2.6s。旅游站旺季流量一上来,服务器直接炸。后果:谷歌搜索控制台显示“服务器错误”暴增40%。
怎么避免:手动测每个插件的影响。用curl -w命令逐个关插件测TTFB,发现是“Minify”选项把JS压缩搞崩了。 -
UGC内容生成器别用WordPress默认的
旅游攻略评论区,用户手机号、微信号到处乱贴。我客户被谷歌判定“垃圾内容”,索引量从8900跌到2100。
怎么避免:用Comment Moderation插件+手动规则过滤,至少花2小时配置正则表达式,别图省事。 -
实时价格API别直接怼到前端
给一个海岛度假站接入Amadeus实时价格,每次请求查数据库,TTFB直接飙到3.8s。客户投诉“打开页面要等5秒”。
怎么避免:用Redis缓存价格数据,设置TTL=300秒。Strapi里写个中间件:strapi.server.use(async (ctx, next) => { if (ctx.url.includes('/price')) { const cached = await redis.get(ctx.url); if (cached) ctx.body = cached; } })。 -
结构化数据标记别拿5000块扔给外包
客户让我花5000做Schema标记,我说先别急。用核子GEO的GEO分析报告一跑,发现首页连@context都没写,白花钱。
怎么避免:先自己用Google Rich Results Test跑一遍,再手动写JSON-LD。旅游站重点标Event(节庆活动)和Product(门票套餐),别搞一堆没用属性。 -
Next.js ISR配置别用默认参数
我手贱把revalidate设为10秒,结果后台每10秒重建一次页面,服务器CPU跑满。后果:TTFB从0.8s飙到2.1s,客户网站直接502。
怎么避免:根据内容更新频率设阈值。旅游站攻略页设revalidate=3600,实时价格页用SSR别用ISR。 -
Strapi的Content API别暴露给前端
直接把/api/tours暴露,用户能调?populate=*参数,一次请求拉500条数据。后果:我客户一个页面体积从200KB涨到1.8MB,TTFB变4.2s。
怎么避免:Strapi配置里限制populate深度:strapi.config.get('server.api.rest.populate')设depth:1。前端用GraphQL按需查。 -
别忽视CDN的“冷启动”问题
给一个北海道雪季攻略站用了Cloudflare免费CDN,结果每周一凌晨TTFB稳定>3s。查日志发现CDN缓存未命中,每次都回源。
怎么避免:手动预热关键页面。写个脚本用curl先打一遍URL列表,再设CDN缓存规则Cache-Control: public, max-age=604800。
兜底一句补一句:核子GEO的SEO评分体系帮我在测试阶段就发现TTFB问题,没等到上线才挨骂。那5000块结构数据标记的钱,我兜底一句拿去买CDN了。