perf(home): 首屏 eager+关键 CSS 内联,Lighthouse 伪影根因修毕
- 车道 D 三件收编(op-gitea):①index.astro 首两图 eager+首图
fetchpriority=high(Image 默认 lazy 曾压 LCP 至 6.4s,好模式现
1.3s)②inline-home-css.mjs:BaseLayout 11KB 内联首页 head(省
发现+下载跳)③build.sh 守卫挂钩
- 诊断结论入档:home 中位漂移根因=测试床伪影(python server 不压
缩,裸传 67KB vs 生产 gzip 9.2KB;lantern 对未压缩 HTML 流式归因
双峰 99↔165ms 翻转出两个世界)——gzip 等价口径中位 100
(FCP 0.9-1.1s/LCP 1.3-1.4s/TBT 0/CLS 0)
- 测试床转正:gz-server.py 泛化入仓(ThreadingHTTPServer+gzip
[端口 目录] 参数化)——兼治 SW 预缓存风暴竞速(单线程 server
曾把 E2E /gallery/ networkidle 饿死,实锤于收编验收)
- E2E serviceWorkers:'block':SW 有专属测试,站点 E2E 求确定性
- 三连否定留档:SW✗/css链✗/子集内联 v3✗(6×63 反例钉死,已
回滚)——残尾 20% 离群属 lantern trace 抖动,采中位+多轮
车道:op-gitea(过程三连否定+测试床移交);收编验收:内联 10KB
实出+首图 eager 实证+E2E 31/31(SW 屏蔽后确定性复现)
验证: gz 测试床 18743 gzip 10KB(生产等价口径);node tests/e2e.mjs
31/31 PASS;构建+守卫链复跑
Spec: none (性能三件+测试床转正,车道级交付)
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
71b8985c17
commit
0b6524f53c
5 files changed
+62
-2
No files matched your search
@@ -27,6 +27,8 @@ pnpm astro build
|
|||||||
# 按页字体 CSS 子集化(车道 A 任务 1):裁 @font-face 声明数,构建仓与本地同链;
|
# 按页字体 CSS 子集化(车道 A 任务 1):裁 @font-face 声明数,构建仓与本地同链;
|
||||||
# 存在性守卫:新文件未推送到 main 前构建仓无此脚本,绝不让发布链因车道在途文件断掉
|
# 存在性守卫:新文件未推送到 main 前构建仓无此脚本,绝不让发布链因车道在途文件断掉
|
||||||
[ -f scripts/build-perf/subset-font-css.mjs ] && node scripts/build-perf/subset-font-css.mjs dist || echo "[build] subset 脚本未就绪,跳过"
|
[ -f scripts/build-perf/subset-font-css.mjs ] && node scripts/build-perf/subset-font-css.mjs dist || echo "[build] subset 脚本未就绪,跳过"
|
||||||
|
# 首页阻塞 CSS 内联(车道 D):守卫先行同上——脚本未到 main 前绝不炸链
|
||||||
|
[ -f scripts/build-perf/inline-home-css.mjs ] && node scripts/build-perf/inline-home-css.mjs || echo "[build] inline-home 脚本未就绪,跳过"
|
||||||
|
|
||||||
echo "[build] rsync -> $STAGING"
|
echo "[build] rsync -> $STAGING"
|
||||||
mkdir -p "$STAGING"
|
mkdir -p "$STAGING"
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import gzip, os, functools
|
||||||
|
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
|
||||||
|
|
||||||
|
class GZ(SimpleHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
path = self.translate_path(self.path)
|
||||||
|
if os.path.isdir(path):
|
||||||
|
path = os.path.join(path, 'index.html')
|
||||||
|
compressible = os.path.isfile(path) and not path.endswith(('.woff2', '.png', '.jpg', '.webp', '.ico', '.svg'))
|
||||||
|
if compressible:
|
||||||
|
with open(path, 'rb') as f: body = f.read()
|
||||||
|
gz = gzip.compress(body, 6)
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-Type', self.guess_type(path))
|
||||||
|
self.send_header('Content-Encoding', 'gzip')
|
||||||
|
self.send_header('Content-Length', str(len(gz)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(gz)
|
||||||
|
else:
|
||||||
|
super().do_GET()
|
||||||
|
|
||||||
|
# 用法:gz-server.py [端口=18743] [目录=www-staging]
|
||||||
|
# 团队标准测试床(车道 D 移交):ThreadingHTTPServer+gzip=生产 nginx/ESA 等价口径;
|
||||||
|
# 单线程 SimpleHTTPServer 双宗罪:不压缩致 LH 双峰伪影 + 与 SW 预缓存风暴竞速
|
||||||
|
import sys
|
||||||
|
port = int(sys.argv[1]) if len(sys.argv) > 1 else 18743
|
||||||
|
root = sys.argv[2] if len(sys.argv) > 2 else os.path.expanduser('~/docker_apps/blog/www-staging')
|
||||||
|
print(f'gz server on {port} -> {root}')
|
||||||
|
ThreadingHTTPServer(('127.0.0.1', port), functools.partial(GZ, directory=root)).serve_forever()
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* 首页 render-blocking CSS 内联(车道 D):把 dist/index.html 唯一的阻塞样式表
|
||||||
|
* (BaseLayout 共享块,~11KB)内联进 <head>。
|
||||||
|
* 慢模式双峰根因不在 css(实测:字体子集也内联后 6.0s 模式反成主流),
|
||||||
|
* 本脚本只保留 BaseLayout 内联这一确定性收益(省一次发现+下载跳)。
|
||||||
|
*/
|
||||||
|
import { readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
|
||||||
|
const htmlPath = 'dist/index.html';
|
||||||
|
let html;
|
||||||
|
try {
|
||||||
|
html = readFileSync(htmlPath, 'utf8');
|
||||||
|
} catch {
|
||||||
|
console.error('[inline-home-css] dist/index.html 不存在,跳过');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const m = html.replace(/<noscript>[\s\S]*?<\/noscript>/g, '').match(/<link rel="stylesheet" href="(\/_astro\/[^"']+\.css)">/);
|
||||||
|
if (!m || !html.includes(m[0])) {
|
||||||
|
console.log('[inline-home-css] 无阻塞样式表链接,跳过');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
const css = readFileSync(`dist${m[1]}`, 'utf8');
|
||||||
|
writeFileSync(htmlPath, html.replace(m[0], `<style>${css}</style>`));
|
||||||
|
console.log(`[inline-home-css] ${m[1]} (${css.length}B) 已内联进首页`);
|
||||||
@@ -73,13 +73,16 @@ const photos = (await getCollection('photos')).slice(0, 4);
|
|||||||
<a class="tap more" href="/gallery/">进入图库 →</a>
|
<a class="tap more" href="/gallery/">进入图库 →</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="thumbs">
|
<div class="thumbs">
|
||||||
{photos.map((photo) => (
|
{photos.map((photo, i) => (
|
||||||
<a class="thumb" href="/gallery/" aria-label={`查看图库:${photo.data.title}`}>
|
<a class="thumb" href="/gallery/" aria-label={`查看图库:${photo.data.title}`}>
|
||||||
|
{/* 首两图 eager(Astro Image 默认 lazy 会把 LCP 压到 6s+),首图再拔 fetchpriority */}
|
||||||
<Image
|
<Image
|
||||||
src={photo.data.image}
|
src={photo.data.image}
|
||||||
alt={photo.data.alt ?? photo.data.title}
|
alt={photo.data.alt ?? photo.data.title}
|
||||||
widths={[180, 360]}
|
widths={[180, 360]}
|
||||||
sizes="(min-width: 820px) 20vw, 42vw"
|
sizes="(min-width: 820px) 20vw, 42vw"
|
||||||
|
loading={i < 2 ? 'eager' : 'lazy'}
|
||||||
|
fetchpriority={i === 0 ? 'high' : undefined}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ const ok = (name, pass, note = '') => {
|
|||||||
const browser = await chromium.launch();
|
const browser = await chromium.launch();
|
||||||
|
|
||||||
async function newPage(viewport, ua) {
|
async function newPage(viewport, ua) {
|
||||||
const ctx = await browser.newContext({ viewport, userAgent: ua });
|
const ctx = await browser.newContext({ serviceWorkers: 'block', viewport, userAgent: ua });
|
||||||
const page = await ctx.newPage();
|
const page = await ctx.newPage();
|
||||||
page.on('pageerror', (e) => console.log(' [pageerror]', String(e).slice(0, 120)));
|
page.on('pageerror', (e) => console.log(' [pageerror]', String(e).slice(0, 120)));
|
||||||
return page;
|
return page;
|
||||||
|
|||||||
Reference in new issue
Block a user