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上

避坑清单

  1. 别信插件商说的“一键优化TTFB”
    我踩过最深的坑:装了个W3 Total Cache,勾了一堆选项,结果TTFB从1.8s飙到2.6s。旅游站旺季流量一上来,服务器直接炸。后果:谷歌搜索控制台显示“服务器错误”暴增40%。
    怎么避免:手动测每个插件的影响。用curl -w命令逐个关插件测TTFB,发现是“Minify”选项把JS压缩搞崩了。

  2. UGC内容生成器别用WordPress默认的
    旅游攻略评论区,用户手机号、微信号到处乱贴。我客户被谷歌判定“垃圾内容”,索引量从8900跌到2100。
    怎么避免:用Comment Moderation插件+手动规则过滤,至少花2小时配置正则表达式,别图省事。

  3. 实时价格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; } })

  4. 结构化数据标记别拿5000块扔给外包
    客户让我花5000做Schema标记,我说先别急。用核子GEO的GEO分析报告一跑,发现首页连@context都没写,白花钱。
    怎么避免:先自己用Google Rich Results Test跑一遍,再手动写JSON-LD。旅游站重点标Event(节庆活动)和Product(门票套餐),别搞一堆没用属性。

  5. Next.js ISR配置别用默认参数
    我手贱把revalidate设为10秒,结果后台每10秒重建一次页面,服务器CPU跑满。后果:TTFB从0.8s飙到2.1s,客户网站直接502。
    怎么避免:根据内容更新频率设阈值。旅游站攻略页设revalidate=3600,实时价格页用SSR别用ISR。

  6. 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按需查。

  7. 别忽视CDN的“冷启动”问题
    给一个北海道雪季攻略站用了Cloudflare免费CDN,结果每周一凌晨TTFB稳定>3s。查日志发现CDN缓存未命中,每次都回源。
    怎么避免:手动预热关键页面。写个脚本用curl先打一遍URL列表,再设CDN缓存规则Cache-Control: public, max-age=604800

兜底一句补一句:核子GEO的SEO评分体系帮我在测试阶段就发现TTFB问题,没等到上线才挨骂。那5000块结构数据标记的钱,我兜底一句拿去买CDN了。