fix(home): early boot reads load as an async data: script so stylesheets no longer hold them back (BUG-1127)
An inline classic script after the stylesheets waited for them to download; on staging the reads left at 2.4-3.1 s. Measurement script gains --css-delay and --ua iphone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6e2f0af5a2
commit
c2265dabb2
@@ -14,6 +14,11 @@
|
||||
*
|
||||
* /exec-daemon/node scripts/measure-home-first-load.mjs --base http://127.0.0.1:3920 --delay 600 [--runs 3] [--port 9340] [--throttle slow]
|
||||
*
|
||||
* `--css-delay 1500` holds every stylesheet response that long (TASK-home-first-load-fix
|
||||
* -20261001): an inline script after the stylesheets waits for them, which a
|
||||
* local server hides because CSS arrives instantly.
|
||||
* `--ua iphone` uses an iPhone Safari user agent (headless Chrome engine, not WebKit).
|
||||
*
|
||||
* `--throttle slow` emulates 1.6 Mbps down / 150 ms latency so JS and font
|
||||
* downloads cost something against a local server. "First-screen JS" is the
|
||||
* scripts the HTML itself references (parser-initiated), not chunks loaded later.
|
||||
@@ -30,6 +35,9 @@ const BASE = (args.base ?? "http://127.0.0.1:3000").replace(/\/$/, "");
|
||||
const DELAY = Number(args.delay ?? 600);
|
||||
const RUNS = Number(args.runs ?? 1);
|
||||
const PORT = Number(args.port ?? 9340);
|
||||
const CSS_DELAY = Number(args["css-delay"] ?? 0);
|
||||
const IPHONE_UA = "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1";
|
||||
const UA = args.ua === "iphone" ? IPHONE_UA : null;
|
||||
const THROTTLE = args.throttle === "slow" ? { offline: false, latency: 150, downloadThroughput: 1_600_000 / 8, uploadThroughput: 750_000 / 8 } : null;
|
||||
|
||||
const ACCOUNT = JSON.stringify({
|
||||
@@ -72,6 +80,7 @@ async function measureOnce() {
|
||||
const urls = new Map();
|
||||
const jsDone = [];
|
||||
const parserJs = new Set();
|
||||
const cssReleased = [];
|
||||
const apiStarts = [];
|
||||
ws.onmessage = (message) => {
|
||||
const m = JSON.parse(message.data);
|
||||
@@ -86,6 +95,12 @@ async function measureOnce() {
|
||||
events.push([at, `api ${m.params.request.method} ${url.replace(/^https?:\/\/[^/]+/, "").slice(0, 60)} → ${status} (answered ${now()}ms)`]);
|
||||
void send("Fetch.fulfillRequest", { requestId: m.params.requestId, responseCode: status, responseHeaders: [{ name: "content-type", value: "application/json" }], body: Buffer.from(body).toString("base64") });
|
||||
}, DELAY);
|
||||
} else if (CSS_DELAY > 0 && /\.css(\?|$)/.test(url)) {
|
||||
setTimeout(() => {
|
||||
events.push([now(), `stylesheet released ${url.split("/").pop().split("?")[0]}`]);
|
||||
cssReleased.push(now());
|
||||
void send("Fetch.continueRequest", { requestId: m.params.requestId });
|
||||
}, CSS_DELAY);
|
||||
} else {
|
||||
void send("Fetch.continueRequest", { requestId: m.params.requestId });
|
||||
}
|
||||
@@ -103,6 +118,10 @@ async function measureOnce() {
|
||||
await send("Fetch.enable", { patterns: [{ urlPattern: "*" }] });
|
||||
await send("Network.enable");
|
||||
if (THROTTLE) await send("Network.emulateNetworkConditions", THROTTLE);
|
||||
if (UA) {
|
||||
await send("Network.setUserAgentOverride", { userAgent: UA, platform: "iPhone" });
|
||||
await send("Emulation.setDeviceMetricsOverride", { width: 390, height: 844, deviceScaleFactor: 3, mobile: true });
|
||||
}
|
||||
await send("Page.enable");
|
||||
t0 = performance.now();
|
||||
await send("Page.navigate", { url: `${BASE}/` });
|
||||
@@ -129,8 +148,10 @@ async function measureOnce() {
|
||||
if (revealed !== null && at > revealed) break;
|
||||
if (!waves.length || at - waves[waves.length - 1] > DELAY / 2) waves.push(at);
|
||||
}
|
||||
const bootReads = await send("Runtime.evaluate", { expression: `(() => { const r = window.__jyotishaBootReads; return r === undefined ? "absent" : JSON.stringify(Object.keys(r)); })()`, returnByValue: true });
|
||||
const firstCss = cssReleased.length ? Math.min(...cssReleased) : null;
|
||||
chrome.kill();
|
||||
return { events, summary: { revealed, lastJs, firstApi, firstApiVsLastJs: firstApi !== null && lastJs !== null ? firstApi - lastJs : null, apiRounds: waves.length, serifFontRequestsBeforeLoad: fontsBeforeLoad } };
|
||||
return { events, summary: { firstStylesheetReleased: firstCss, firstApiBeforeCss: firstCss !== null && firstApi !== null ? firstApi < firstCss : null, bootReadsAfterReveal: bootReads?.result?.value ?? null, revealed, lastJs, firstApi, firstApiVsLastJs: firstApi !== null && lastJs !== null ? firstApi - lastJs : null, apiRounds: waves.length, serifFontRequestsBeforeLoad: fontsBeforeLoad } };
|
||||
}
|
||||
|
||||
const summaries = [];
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Toaster } from "@/components/ui/sonner";
|
||||
import { StaleBuildGuard } from "@/components/stale-build-guard";
|
||||
import { StaleClientRecovery } from "@/components/stale-client-recovery";
|
||||
import { ViewportScrollLock } from "@/components/viewport-scroll-lock";
|
||||
import { bootEarlyReadsScript } from "@/lib/boot-early-reads";
|
||||
import { bootEarlyReadsScriptSrc } from "@/lib/boot-early-reads";
|
||||
import { firstPaintFallbackBootScript } from "@/lib/first-paint-fallback";
|
||||
import { themePreferenceBootScript } from "@/lib/theme-preference";
|
||||
|
||||
@@ -59,8 +59,10 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac
|
||||
{/* Classic script, not a module: it has to run when the client bundle never does. */}
|
||||
<script dangerouslySetInnerHTML={{ __html: firstPaintFallbackBootScript() }} />
|
||||
{/* Home cold-boot reads leave with the HTML instead of after the bundle
|
||||
runs (BUG-1127). The script is a no-op on every path except "/". */}
|
||||
<script dangerouslySetInnerHTML={{ __html: bootEarlyReadsScript() }} />
|
||||
runs (BUG-1127). The script is a no-op on every path except "/".
|
||||
async + data: URL, not inline: an inline script after the
|
||||
stylesheets would wait for them to download (fix 2026-10-01). */}
|
||||
<script async src={bootEarlyReadsScriptSrc()} />
|
||||
{enableReactDevTools && (
|
||||
<>
|
||||
<Script
|
||||
|
||||
@@ -78,6 +78,18 @@ export function bootEarlyReadsScript(): string {
|
||||
})();`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The script as an async data: URL (BUG-1127 fix, 2026-10-01). An inline
|
||||
* classic script placed after the page's stylesheets waits until they have
|
||||
* downloaded (script-blocking stylesheets), which on staging held the early
|
||||
* reads back 2.4-3.1 s. An `async` external classic script is not blocked by
|
||||
* stylesheets, and a data: URL costs no round trip. Plain ASCII base64, so
|
||||
* the attribute needs no escaping.
|
||||
*/
|
||||
export function bootEarlyReadsScriptSrc(): string {
|
||||
return `data:text/javascript;base64,${Buffer.from(bootEarlyReadsScript(), "utf8").toString("base64")}`;
|
||||
}
|
||||
|
||||
function bootReads(): BootReads | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
const reads = (window as unknown as Record<string, BootReads | undefined>)[BOOT_READS_GLOBAL];
|
||||
|
||||
@@ -8,7 +8,7 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
import * as acorn from "acorn";
|
||||
|
||||
import { bindCurrentSubjectAccount, currentSubjectStorageKey, LAST_SUBJECT_ACCOUNT_KEY, readCurrentSubjectId, resetSubjectCatalogForTests } from "../src/lib/current-subject.ts";
|
||||
import { BOOT_ACCOUNT_PATH, BOOT_CHART_PROFILES_PATH, BOOT_MODELS_PATH, BOOT_READ_INIT, BOOT_READ_MAX_AGE_MS, BOOT_READS_GLOBAL, bootEarlyReadsScript, bootSessionsPath, clearBootReads, takeBootRead, takeBootSessionsRead, withBootFallback } from "../src/lib/boot-early-reads.ts";
|
||||
import { BOOT_ACCOUNT_PATH, BOOT_CHART_PROFILES_PATH, BOOT_MODELS_PATH, BOOT_READ_INIT, BOOT_READ_MAX_AGE_MS, BOOT_READS_GLOBAL, bootEarlyReadsScript, bootEarlyReadsScriptSrc, bootSessionsPath, clearBootReads, takeBootRead, takeBootSessionsRead, withBootFallback } from "../src/lib/boot-early-reads.ts";
|
||||
import { chinaLocations } from "../src/data/china-locations.ts";
|
||||
import { ensureChinaLocations, loadedChinaCountry, profileNeedsChinaLocations, resetChinaLocationsForTests } from "../src/lib/china-locations-client.ts";
|
||||
import { readProfile, selectedBirthPlace } from "../src/lib/home-profile.ts";
|
||||
@@ -233,7 +233,13 @@ test("the early-read script is classic ES5 and only acts on / (T8)", () => {
|
||||
assert.deepEqual(runBootScript(path).sent, [], `${path} sends nothing`);
|
||||
}
|
||||
const layout = readFileSync(new URL("../src/app/layout.tsx", import.meta.url), "utf8");
|
||||
assert.match(layout, /__html: bootEarlyReadsScript\(\)/);
|
||||
// 原值:assert.match(layout, /__html: bootEarlyReadsScript\(\)/)(内联经典脚本)。
|
||||
// 新值:<script async src={bootEarlyReadsScriptSrc()} />,src 是同一段脚本的 base64 data: URL。
|
||||
// 原因:TASK-home-first-load-fix-20261001——内联脚本排在样式表之后,要等样式表下完才执行,线上早发推迟 2.4~3.1 s。
|
||||
assert.match(layout, /<script async src=\{bootEarlyReadsScriptSrc\(\)\} \/>/);
|
||||
const src = bootEarlyReadsScriptSrc();
|
||||
assert.match(src, /^data:text\/javascript;base64,[A-Za-z0-9+/=]+$/);
|
||||
assert.equal(Buffer.from(src.slice(src.indexOf(",") + 1), "base64").toString("utf8"), bootEarlyReadsScript(), "the data URL carries exactly the ES5 script");
|
||||
});
|
||||
|
||||
test("the early reads are the normal requests, byte for byte (T8)", () => {
|
||||
|
||||
Reference in New Issue
Block a user