移动端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">包裹,别漏了@id和mainEntityOfPage——这两个字段是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抓页面时直接跳过首页内容,因为页面布局一直在晃。
怎么避免:所有图片必须设置明确的width和height属性,动态内容用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%的手动排查时间。