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 声明数,构建仓与本地同链;
|
||||
# 存在性守卫:新文件未推送到 main 前构建仓无此脚本,绝不让发布链因车道在途文件断掉
|
||||
[ -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"
|
||||
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>
|
||||
</div>
|
||||
<div class="thumbs">
|
||||
{photos.map((photo) => (
|
||||
{photos.map((photo, i) => (
|
||||
<a class="thumb" href="/gallery/" aria-label={`查看图库:${photo.data.title}`}>
|
||||
{/* 首两图 eager(Astro Image 默认 lazy 会把 LCP 压到 6s+),首图再拔 fetchpriority */}
|
||||
<Image
|
||||
src={photo.data.image}
|
||||
alt={photo.data.alt ?? photo.data.title}
|
||||
widths={[180, 360]}
|
||||
sizes="(min-width: 820px) 20vw, 42vw"
|
||||
loading={i < 2 ? 'eager' : 'lazy'}
|
||||
fetchpriority={i === 0 ? 'high' : undefined}
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ const ok = (name, pass, note = '') => {
|
||||
const browser = await chromium.launch();
|
||||
|
||||
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();
|
||||
page.on('pageerror', (e) => console.log(' [pageerror]', String(e).slice(0, 120)));
|
||||
return page;
|
||||
|
||||
Reference in new issue
Block a user