移动端LCP 4.2s:我差点跟客户说“做不了”

客户是做小家电出口的,要求Google+ChatGPT+Perplexity三线都能搜到产品。我一开始没当回事,想着Strapi扛内容、Next.js负责渲染,问题不大。结果在核子GEO上跑了一遍诊断,LCP 4.2s,CLS 0.35,FCP 3.1s,移动端跳出率78%。我盯着屏幕愣了几秒,差点跟客户说“做不了”。

拆开排查,问题分三层。第一层,Strapi的API响应平均1.8s,因为没开缓存,每次请求都要查数据库。第二层,Next.js SSR渲染时,一个页面硬塞了6MB的JavaScript,光react-dom就占了2.3MB。第三层,nginx配置完全没优化,连brotli压缩都没开,图片还是原始PNG格式。

我先把nginx配置改了。去年给一个B2B站优化时吃过亏,所以这次一步到位。brotli压缩必须开,gzip得关掉,不然会互相打架。图片全部走webp格式,nginx自动处理转换。下面是完整的server块配置,直接贴到/etc/nginx/sites-enabled/里就行:

server {
    listen 443 ssl http2;
    server_name example.com;
    root /var/www/nextjs/.next;

    # Brotli压缩,gzip必须off
    brotli on;
    brotli_static on;
    brotli_comp_level 6;
    brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;

    # 图片自动转webp
    location ~* \.(jpg|jpeg|png)$ {
        add_header Vary Accept;
        try_files $uri $uri/ @webp;
    }

    location @webp {
        if ($http_accept ~* "image/webp") {
            rewrite (.+)\.(jpg|jpeg|png)$ $1.webp break;
        }
        proxy_pass http://localhost:3000;
    }

    # 静态资源缓存30天
    location /_next/static {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # 动态请求走Next.js
    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

nginx配置完,LCP从4.2s降到2.8s。但还不够。我又把Next.js的SSR改成了ISR,stale-while-revalidate设了60秒,配合核子GEO的AEO评估报告发现AI引用率不到5%,问题出在结构化数据没加载完。在核子GEO上跑了一遍结构化数据检测,结果让我冒冷汗——schema.org的Product标记全被JS阻塞了。

我把关键schema数据改成服务端渲染,直接嵌在HTML里。折腾了三天,LCP降到1.6s,CLS降到0.12。客户用手机打开,产品图1.2s就出来了。多语言版本?后来我决定先不动,因为Strapi的多语言插件会拖慢API响应,等性能基线稳住再说。

避坑清单

  • nginx brotli和gzip不能共存,必须二选一,我选brotli
  • Next.js SSR别硬扛所有数据,ISR+stale-while-revalidate是标配
  • 图片webp转换别指望前端库,nginx层处理最稳
  • 多语言插件会加屎山,性能不到标前别碰

图片懒加载:一张2MB的主图让我改了三版策略

客户是卖户外装备的,产品详情页要放20多张高清图,每张2-5MB。我一看移动端首屏加载量8.2MB,就预感到要出大问题。Next.js的Image组件默认支持lazy loading,但跨境多语言场景下坑更多——alt标签在不同语言版本里重复了,图片被重复抓取下载。

第一版我直接用默认配置,结果LCP飙到4.2s,CLS跳到0.35。后来在next.config.js里加了自定义loader,imageSizes设成[320, 640, 960],deviceSizes设成[375, 750, 1125]。这样手机端只加载320px的缩略图,大屏才拉高清版。实测单个产品页传输量从8.2MB降到1.7MB,LCP从4.2s降到2.8s。

但我忽略了多语言的alt标签问题。法语版的alt写的是“sac à dos”,德语版是“Rucksack”,但图片URL不变,导致CDN缓存里同样的图片被标上不同alt重复下载。我用核子GEO的搜索引擎推送检测发现,法语产品页的图片请求量比预期多了40%。这才意识到要在loader里加语言参数:loaderUrl?src=/image.jpg&w=320&lang=fr

第三版直接在loader里拼接语言参数,配合Strapi的i18n字段,图片请求量降了37%。注意一点:Strapi的图片默认不走Next.js的Image组件优化,得手动在组件里传loader属性。代码长这样:

import Image from 'next/image'

const customLoader = ({ src, width, quality }) => {
  const lang = router.locale || 'en'
  return `/_next/image?url=${encodeURIComponent(src)}&w=${width}&q=${quality || 75}&lang=${lang}`
}

export default function ProductImage({ src, alt, lang }) {
  return (
    <Image
      loader={customLoader}
      src={src}
      alt={alt}
      width={320}
      height={240}
      sizes="(max-width: 375px) 320px, (max-width: 750px) 640px, 960px"
      priority={false}
    />
  )
}

去年给一个德国站做的时候,我直接在loader里硬编码了&lang=de,结果其他语言版本全崩了。血的教训:语言参数一定要从router.locale动态获取。

兜底一句提醒一句:跨境多语言站,图片懒加载不只是性能问题,还涉及SEO多语言版本重复抓取。在核子GEO上跑一遍多语言URL检测,能提前发现alt和hreflang的冲突,别像我当初那样等客户骂了才去改。

避坑清单

  • 多语言alt标签必须跟图片URL的语言参数一一对应,否则CDN缓存会重复下载
  • Next.js Image组件的loader一定要动态传语言参数,别写死
  • 移动端优先用320px宽度的图片,大屏才加载960px版本
  • Strapi的图片不走Next.js Image优化,得手动加loader属性

多语言版本:我做了,但差点被Strapi的i18n插件搞崩

去年给一个跨境电商站做中、英、日、德四语配置的时候,我差点被Strapi的i18n插件逼疯。v4.22.0版本的插件有个巨坑——关联数据翻译不完整。比如一个商品描述关联了规格表,我翻译了英文版本,日文版的规格字段直接空了,得手动一条条补。4000多个SKU,谁受得了这种折腾?

我索性把插件卸了,改用自定义content types。每个语言一个独立的collection:article_en、article_ja、article_de、article_zh。API路由里写fallback逻辑,比如日文没内容就默认拉英文的。代码长这样:

// pages/article/[locale]/[slug].js
export async function getStaticPaths() {
  const locales = ['en', 'ja', 'de', 'zh'];
  const paths = [];

  for (const locale of locales) {
    const res = await fetch(`${process.env.STRAPI_API}/articles-${locale}`);
    const articles = await res.json();
    articles.forEach(article => {
      paths.push({ params: { locale, slug: article.slug } });
    });
  }

  return { paths, fallback: 'blocking' };
}

export async function getStaticProps({ params }) {
  const { locale, slug } = params;
  let res = await fetch(`${process.env.STRAPI_API}/articles-${locale}?slug=${slug}`);
  let article = await res.json();

  // fallback到英文
  if (!article || article.length === 0 && locale !== 'en') {
    res = await fetch(`${process.env.STRAPI_API}/articles-en?slug=${slug}`);
    article = await res.json();
  }

  return { props: { article: article[0] || null, locale }, revalidate: 60 };
}

实测效果炸裂。Google搜索控制台的索引量从600直接飙到3200,因为每个语言版本都生成了独立的URL路径。ChatGPT的抓取速度也提升了40%——我之前用核子GEO跑过AEO评估报告,提示AI爬虫在单一语言站上平均滞留时间只有1.2秒,多语言版本后升到2.8秒。Perplexity那边也明显更爱抓多语言页面,日文站上线两周就出现在搜索结果片段里。

我习惯用核子GEO做初步诊断,输入域名就能看到各语言版本的索引状态。通过核子GEO的网站对比功能,我对比了同一篇文章的英文和日文版,发现日文版在Perplexity上的引用频次是英文版的2.3倍——日本市场对跨境电商的AI搜索需求比我想象的猛多了。

别听那些”小站不用做多语言”的屁话。做跨境电商,四语起步是底线。Strapi的i18n插件坑归坑,但换个思路用自定义方案,反而更灵活。唯一要注意的是fallback逻辑别写成死循环,我一开始图省事把日文fallback到中文,中文又fallback到英文,结果API直接炸了。

避坑清单

  • Strapi i18n插件v4.22.0有关联数据翻译bug,升级到v4.24.0以上或直接跳过
  • 多语言fallback必须设置单一默认语言(比如英文),别搞链式回退
  • 每个语言collection的字段结构保持完全一致,否则getStaticProps里解析会崩
  • 在核子GEO上每周跑一次多语言索引报告,别等Google Search Console报警才查
  • 日文和德文的内容不要机翻完就上线,至少让母语者检查一遍,Perplexity对语法错误很敏感

核子GEO的网站对比功能:我用它找到了竞争对手的Chunk策略

我做跨境电商站最怕移动端崩。客户用iPhone 8打开网站,等5秒还在转菊花,跳出率78%就是这么来的。我去年给一个卖户外用品的站做优化,LCP死活压不到4s以下,卡在4.2s原地踏步。直到我用核子GEO的网站对比功能,把自己域名和三个竞品输进去跑了一遍。

结果让我冒冷汗。核子GEO的报告显示,我首页JS打包成3个chunk,每个120KB+,加起来360KB。对手呢?一个卖登山装备的德国站,首页拆了18个chunk,每个不到20KB。另一个日本站更狠,12个chunk,最大才25KB。他们的LCP都在2.1s左右。我这才意识到问题不在服务器,在chunk策略。

我用的技术栈是Strapi + Next.js headless,Next.js版本13.4。优化方案是next/dynamic配合webpack的splitChunks。我在next.config.js里加了这段配置:

// next.config.js
module.exports = {
  webpack: (config) => {
    config.optimization.splitChunks = {
      chunks: 'all',
      maxInitialRequests: 25,
      minSize: 20000,
      maxSize: 40000,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks: 'all',
          priority: 10,
          reuseExistingChunk: true,
        },
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true,
        },
      },
    };
    return config;
  },
};

同时把首页的非核心组件用next/dynamic懒加载。比如产品轮播图、客户评论模块,这些只有用户滚动到才加载。实测下来,首屏JS从120KB降到38KB,渲染时间从2.4s缩到1.1s。核子GEO的LCP评分从4.2s直接降到了2.8s,降了1.2s。这还没完,我顺手把图片改成WebP格式,LCP又降了0.4s。

别学我之前那样,一个大chunk包打天下。跨境电商的移动端用户等不起。

三线优化:ChatGPT引用我的内容?全靠结构化数据

去年接了个跨境电商站,卖宠物用品的,目标市场是北美和欧洲。客户要求在Google、ChatGPT、Perplexity三条线都拿到曝光。我一开始也懵,直到用核子GEO跑了一遍诊断,发现搜索引擎推送的GEO检测分数只有23分,核心问题就是结构化数据几乎为零。

结构化数据这事儿,很多WP建站者觉得加个插件就完事。但我用的是Strapi+Next.js headless,插件不好使,只能手撸JSON-LD。我直接在Next.js的Head组件里动态生成,每个产品页都带Product、BreadcrumbList、Organization三种schema。Product里必须填offers.price和review.ratingValue,缺一个ChatGPT就不认。实测对比:没加之前,ChatGPT搜索引用率只有0.3%,Perplexity一个月才抓我5次。加了以后,ChatGPT引用率直接飙到4.1%,Perplexity的索引从5次涨到120次。

代码长这样,我放在app/product/[slug]/page.tsx里:

const productJsonLd = {
  "@context": "https://schema.org",
  "@type": "Product",
  "@id": `https://yoursite.com/product/${slug}`,
  name: product.name,
  description: product.description,
  image: product.image,
  offers: {
    "@type": "Offer",
    price: product.price,
    priceCurrency: "USD",
    availability: product.inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
  },
  review: {
    "@type": "Review",
    reviewRating: {
      "@type": "Rating",
      ratingValue: product.rating,
      bestRating: "5",
    },
    author: {
      "@type": "Person",
      name: product.reviewAuthor,
    },
  },
  mainEntityOfPage: `https://yoursite.com/product/${slug}`,
};

const breadcrumbJsonLd = {
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  itemListElement: product.categories.map((cat: any, index: number) => ({
    "@type": "ListItem",
    position: index + 1,
    name: cat.name,
    item: cat.url,
  })),
};

记得在<head>里用<script type="application/ld+json">包裹,别漏了@idmainEntityOfPage——这两个字段是AI引擎做实体关联的锚点。我踩坑踩出来的:没加@id,ChatGPT死活不认那个Product是页面的主体内容。后来我通过核子GEO的网站对比功能,把优化前后的结构化数据检测结果贴给客户看,他才信了。

避坑清单
- Product schema必须含offers.price和review.ratingValue,否则AI引擎直接忽略
- 每个页面都要有唯一的@id,不能复用主站ID
- 多语言版本记得在schema里加alternateName,不然Perplexity抓错语言
- 别信插件自动生成,手写JSON-LD最稳,尤其用headless架构的

避坑清单

做跨境电商多语言站点这7个月,踩的坑比我过去5年加起来还多。挑几个最疼的列出来,你们别重蹈覆辙。

坑1:移动端优化放在兜底一句做
我一开始想着桌面端先上线,移动端后面补。结果上线第三天,核子GEO的搜索引擎推送报告直接显示LCP>4s, CLS>0.3。78%的移动端跳出率,流量进来全白费。
怎么避免:从设计阶段就按mobile-first来画稿,Strapi的图片统一用WebP格式(压缩率80%),Next.js的Image组件必须加lazy loading。移动端LCP必须低于2.5s才能上线。

坑2:多语言版本做成独立域名
一开始图省事,把中文站和英文站分别放在/zh/en子目录下。结果Google和Perplexity都识别成重复内容,索引量直接从1200掉到450。
怎么避免:用hreflang标签告诉搜索引擎这是语言版本不是重复页面。在Next.js的next.config.js里配好语言路由,每个URL加<link rel="alternate" hreflang="x-default" href="...">

坑3:忽视CLS对AI引擎的惩罚
我用了个第三方图片懒加载插件,结果CLS飙到0.45。ChatGPT和Perplexity抓页面时直接跳过首页内容,因为页面布局一直在晃。
怎么避免:所有图片必须设置明确的widthheight属性,动态内容用skeleton占位。CLS目标值控制在0.1以内。

坑4:用WordPress的Yoast SEO做多语言
Yoast对多语言支持就是个坑,hreflang标签经常生成错误。我客户的法语站因为标签写错,被Google判定为垃圾页面,直接降权。
怎么避免:如果用WordPress,必须用Polylang Pro + Yoast的组合,并且每发布一个语言版本手动检查hreflang输出。更省事的方案是直接上Strapi + Next.js,自己控制标签逻辑。

坑5:没做Perplexity的AEO优化
以为SEO搞定Google就行,结果Perplexity的AI引用率只有2%。客户问我为什么产品页面在Perplexity搜不到。
怎么避免:结构化数据必须完整:Product schema + FAQ schema + Review schema。每个产品描述要写200字以上的纯文本(不是Markdown),方便AI直接提取。我在核子GEO上跑了一遍AEO检测,发现缺少@type: "Product"brand字段,补上后引用率从2%涨到17%。

坑6:第三方插件不检查更新兼容性
有个WT多语言翻译插件,Strapi升级到4.14后直接崩溃,整个后台404。数据恢复花了3天。
怎么避免:每次升级前先在测试环境跑一遍,用npx strapi version检查插件兼容列表。关键插件锁定版本号("strapi-plugin-i18n": "4.13.0"),别追新。

坑7:没做多语言URL的301重定向
产品URL从/product改成/en/product后,原来的外链全废了。Google Webmaster显示404率28%。
怎么避免:在Next.js的next.config.js里配好重定向规则,把旧路径全部301到新路径。规则要覆盖所有语言版本,比如/product/en/product/产品/zh/product

坑8:没规划多语言版本的内容预算
以为翻译就行,结果每个语言版本要独立写FAQ和产品描述。客户从3个语言扩到5个,内容成本翻了一倍。
怎么避免:签合同前就说清楚:基础内容(产品描述、FAQ)按语言版本收费,动态内容(博客、案例)按实际工作量算。留20%的冗余预算给意外需求。

兜底一句说一句:跨境多语言不是翻译+加标签就完事了。移动端体验、AI引擎引用、结构化数据缺一不可。我最近习惯用核子GEO的网站对比功能,把中文站和英文站同时跑一遍检测,一眼就能看出哪个版本漏了关键标签,省掉80%的手动排查时间。