KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
07 · 性能与核心网页指标 — keel 龙骨
本章目标:LCP / INP / CLS 达标,且静态资源体积与缓存策略合理。 验收标准:GSC 的「核心网页指标」报告显示"良好",或本地 Lighthouse 三项达标。
本章目标:LCP / INP / CLS 达标,且静态资源体积与缓存策略合理。
验收标准:GSC 的「核心网页指标」报告显示"良好",或本地 Lighthouse 三项达标。
定位:性能是放大器不是发动机。内容不收录时优化性能毫无意义;但收录之后,加载体验会直接影响排名与跳出率。这一章只讲与 SEO 强相关的部分。
一、三个指标分别是什么
| 指标 | 全称 | 衡量 | 良好阈值 | 差 |
|---|---|---|---|---|
| LCP | Largest Contentful Paint | 最大内容元素的渲染时间(加载速度) | ≤ 2.5s | > 4s |
| INP | Interaction to Next Paint | 交互到下一帧(响应速度,2024 起取代 FID) | ≤ 200ms | > 500ms |
| CLS | Cumulative Layout Shift | 累计布局偏移(视觉稳定性) | ≤ 0.1 | > 0.25 |
看哪个数据? GSC 里的是真实用户数据(field data,CrUX),比本地 Lighthouse(lab data)更有说服力。本地测的是"我这台机器这次访问",GSC 反映的是真实访客的 P75。
若 GSC 数据不足(新站常见),先用本地 Lighthouse 定位问题:
npx lighthouse https://www.xxxxxx.cn/learning/ --preset=desktop --output=json --quiet
二、LCP:让主内容赶紧出来
LCP 元素通常是首屏最大的图片或文本块。三条最有效的手段:
① 预渲染 / SSR(已在第 01 章做过)
这是本站最大的一笔收益:正文内联在 HTML 里,首屏不需要等 JS 下载、解析、执行、发请求、再渲染。对内容型站点,这一步通常能把 LCP 从 3~4s 降到 1s 出头。
② 消除关键路径上的阻塞
<!-- 首屏关键 CSS 内联,其余延后 -->
<style>/* critical css */</style>
<link rel="stylesheet" href="/assets/main.css" media="print" onload="this.media='all'">
<!-- 非关键 JS 延后 -->
<script src="/assets/app.js" defer></script>
③ 图片与字体
<!-- 显式尺寸防 CLS,loading=lazy 减少首屏负担 -->
<img src="/og/cover.png" width="1200" height="630" loading="lazy" decoding="async">
<!-- 字体不阻塞渲染 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
⚠️ 不要给首屏图片加 loading="lazy" ——它会推迟加载,反而拖慢 LCP。只对首屏之外的图片用。
三、INP:让交互有响应
INP 主要受主线程长任务影响。内容型站点最常见的两个来源:
① Markdown 渲染 / 代码高亮
几百行 Markdown 同步解析会阻塞主线程。本站的做法是:
// 把渲染结果缓存,避免每次路由切换重解析
const { data } = useQuery({
queryKey: ['lesson', id],
queryFn: () => fetchLesson(id),
staleTime: Infinity, // 内容不变就不重新取
});
配合 TanStack Query 的缓存,切回已读章节是即时的。
② 大量 DOM 操作
一个真实案例:给每个代码块挂"复制"按钮时用了 MutationObserver 守护,每次内容重渲染都重新扫描全部 pre 元素——在长文页面上造成明显卡顿。
修法是缩小观察范围(只观察新增节点)并在完成后断开:
const observer = new MutationObserver((records) => {
for (const r of records) r.addedNodes.forEach(attachIfCodeBlock);
});
observer.observe(container, { childList: true, subtree: true });
// 组件卸载时 observer.disconnect()
原则:任何 O(n) 的 DOM 遍历,都要问一句"这个 n 会不会变成几百"。
四、CLS:别让页面跳
布局偏移的三个常见来源与解法:
| 来源 | 解法 |
|---|---|
| 图片/iframe 无尺寸 | 显式 width / height,或用 aspect-ratio |
| 字体加载导致文字重排 | font-display: optional 或预加载字体 |
| 动态插入内容(公告条、骨架屏尺寸不符) | 为异步内容预留等尺寸占位 |
最后一条在真实项目里踩过:进入路由时先显示一个矮骨架屏,内容加载后换成高得多的真实区块,页面"跳"了一下。
解法是骨架屏尺寸必须与真实内容一致,至少高度一致:
.skeleton-lesson { min-height: 420px; } /* 与真实卡片等高 */
CLS 是最容易一次修好的指标——它几乎总是"尺寸没预留"这一个原因。
五、缓存与传输
nginx 侧的静态资源策略
# 带 hash 的资源:长缓存 + immutable
location /assets/ {
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
}
# index.html:每次校验(否则发版后用户拿不到新版本)
location = /index.html {
add_header Cache-Control "no-cache";
}
# 内容 JSON:短缓存,保证更新可见
location /content/ {
add_header Cache-Control "public, max-age=300";
}
# 文本压缩(gzip 或 brotli)
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
immutable 只能给带 hash 的文件用。给 index.html 用会导致发版后用户长时间看到旧页面。
检查是否生效
curl -sI https://www.xxxxxx.cn/learning/assets/index-abc123.js | grep -i cache-control
curl -sI -H 'Accept-Encoding: gzip' https://www.xxxxxx.cn/learning/ | grep -i content-encoding
传输层
HTTPS 已开启的情况下,确认 HTTP/2 生效(多路复用对多资源页面帮助明显):
curl -sI --http2 https://www.xxxxxx.cn/learning/ | head -1 # 应显示 HTTP/2
六、体积:先看是不是真的有问题
不要盲目做代码分割。先量:
# 构建产物体积分布
du -sh dist/assets/* | sort -rh | head -10
# 首屏实际加载了多少
curl -s https://www.xxxxxx.cn/learning/ | grep -oE 'assets/[A-Za-z0-9._-]+\.(js|css)' | sort -u
内容型站点的合理目标:首屏 JS 总量 < 300KB(gzip 后 < 100KB)。
本站按路由做代码分割后,阅读页与首页各自只加载自己需要的 chunk。判断分割是否生效:
curl -s https://www.xxxxxx.cn/learning/ | grep -oE 'assets/ReaderPage-[A-Za-z0-9_-]+\.js'
# 阅读页专属 chunk 不在首页 HTML 里 → 分割生效
⚠️ 不要为了分割而分割:如果所有 chunk 都在首屏被同时加载,分割只是增加了请求数。先看路由粒度是否真的被用上。
七、本章验收
# 1. HTTP/2 与压缩
curl -sI --http2 https://www.xxxxxx.cn/learning/ | head -1
curl -sI -H 'Accept-Encoding: gzip' https://www.xxxxxx.cn/learning/ | grep -i content-encoding
# 2. 缓存策略
curl -sI https://www.xxxxxx.cn/learning/assets/index-abc123.js | grep -i cache-control
# 3. 首屏资源数量与体积
du -sh dist/assets/* | sort -rh | head -5
- GSC「核心网页指标」显示良好(或本地 Lighthouse 三项达标)
- 首屏图片未使用
loading="lazy",且带尺寸属性 - 异步内容有等尺寸占位(CLS ≈ 0)
- 带 hash 资源
immutable长缓存,index.html为no-cache - gzip/brotli 已开启,HTTP/2 生效
- 首屏 JS 体积在合理范围,路由级代码分割确实生效
最后一章把这些动作变成可长期执行的例行检查。