Skip to content

Commit afe1df5

Browse files
authored
Merge pull request #15 from deaneeth/perf/lighthouse-speed-index
perf(web): fix Lighthouse Speed Index — replace spinner with SVG PE-grid skeleton
2 parents bbbf99a + e0b6ed3 commit afe1df5

5 files changed

Lines changed: 90 additions & 15 deletions

File tree

web/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.vercel

web/src/components/LandingEmbed.tsx

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,91 @@ const FSM_LABELS: Record<string, string> = {
1919
};
2020

2121
// ── Loading skeleton ──────────────────────────────────────────────────────────
22+
// Mirrors PEGrid's exact SVG layout (same CELL/GAP/offsets) so the page is
23+
// visually "complete" at FCP (~0.8 s) instead of showing an empty spinner for
24+
// the 7 s it takes WASM to download. This is the primary fix for Speed Index.
25+
26+
const SK_CELL = 88;
27+
const SK_GAP = 10;
28+
const SK_N = 4;
29+
const SK_WEST_W = 70;
30+
const SK_TOP_H = 40;
31+
const SK_SOUTH_H = 64;
32+
const SK_GRID = SK_N * SK_CELL + (SK_N - 1) * SK_GAP; // 382
33+
const SK_W = SK_WEST_W + SK_GRID + 12; // 464
34+
const SK_H = SK_TOP_H + SK_GRID + SK_SOUTH_H; // 486
2235

2336
function Skeleton() {
37+
const cells = Array.from({ length: SK_N }, (_, row) =>
38+
Array.from({ length: SK_N }, (_, col) => ({
39+
x: SK_WEST_W + col * (SK_CELL + SK_GAP),
40+
y: SK_TOP_H + row * (SK_CELL + SK_GAP),
41+
delay: ((row + col) % 4) * 0.25,
42+
})),
43+
).flat();
44+
2445
return (
25-
<div
26-
className="flex flex-col items-center justify-center gap-3 animate-pulse"
27-
style={{ minHeight: 420 }}
28-
aria-busy="true"
29-
>
30-
<div
31-
className="h-6 w-6 rounded-full border-2 border-t-transparent animate-spin"
32-
style={{ borderColor: "var(--signal-cyan)", borderTopColor: "transparent" }}
33-
role="status"
34-
/>
35-
<p
36-
className="font-mono text-[11px] uppercase tracking-[0.12em]"
37-
style={{ color: "var(--signal-cyan)" }}
46+
<div style={{ minHeight: 420 }} aria-busy="true" aria-label="Loading simulator">
47+
<svg
48+
viewBox={`0 0 ${SK_W} ${SK_H}`}
49+
style={{ width: "100%", maxWidth: SK_W, display: "block", margin: "0 auto" }}
50+
aria-hidden="true"
3851
>
39-
Compiling RTL → WASM
40-
</p>
52+
<defs>
53+
<style>{`
54+
@keyframes tpu-sk-pulse {
55+
0%,100%{opacity:.55} 50%{opacity:.9}
56+
}
57+
.tpu-sk { animation: tpu-sk-pulse 1.8s ease-in-out infinite; }
58+
`}</style>
59+
</defs>
60+
61+
{/* 4×4 PE cells — same positions as the live PEGrid */}
62+
{cells.map(({ x, y, delay }, i) => (
63+
<rect
64+
key={i}
65+
className="tpu-sk"
66+
style={{ animationDelay: `${delay}s` }}
67+
x={x}
68+
y={y}
69+
width={SK_CELL}
70+
height={SK_CELL}
71+
rx={4}
72+
fill="var(--pe-idle)"
73+
/>
74+
))}
75+
76+
{/* West-edge activation-input placeholders */}
77+
{Array.from({ length: SK_N }, (_, row) => {
78+
const cy = SK_TOP_H + row * (SK_CELL + SK_GAP) + SK_CELL / 2;
79+
return (
80+
<g key={row} opacity={0.25}>
81+
<circle cx={SK_WEST_W - 20} cy={cy} r={6} fill="var(--muted-foreground)" />
82+
<line
83+
x1={SK_WEST_W - 14}
84+
y1={cy}
85+
x2={SK_WEST_W - 3}
86+
y2={cy}
87+
stroke="var(--muted-foreground)"
88+
strokeWidth={1.5}
89+
/>
90+
</g>
91+
);
92+
})}
93+
94+
{/* Loading label — same vertical position as PEGrid south outputs */}
95+
<text
96+
x={SK_WEST_W + SK_GRID / 2}
97+
y={SK_TOP_H + SK_GRID + SK_SOUTH_H - 16}
98+
textAnchor="middle"
99+
fontFamily="var(--font-geist-mono, monospace)"
100+
fontSize={10}
101+
fill="var(--muted-foreground)"
102+
opacity={0.45}
103+
>
104+
Compiling RTL → WASM
105+
</text>
106+
</svg>
41107
</div>
42108
);
43109
}

web/src/layouts/Layout.astro

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ interface Props {
66
ogImage?: string;
77
/** Override canonical URL. Defaults to current page URL. */
88
canonical?: string;
9+
/** Emit a <link rel="preload"> for tiny_tpu.wasm on pages that use WASM. */
10+
preloadWasm?: boolean;
911
}
1012
1113
const {
1214
title,
1315
description = "TPU simulator: SystemVerilog compiled to WebAssembly. Learn how a TPU works with a live systolic array visualizer running real RTL in your browser.",
1416
ogImage,
1517
canonical,
18+
preloadWasm = false,
1619
} = Astro.props;
1720
1821
const site = Astro.site?.toString().replace(/\/$/, "") ?? "";
@@ -72,6 +75,9 @@ const structuredData = {
7275
<meta name="description" content={description} />
7376
<link rel="canonical" href={pageUrl} />
7477
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
78+
{preloadWasm && (
79+
<link rel="preload" as="fetch" href="/tiny_tpu.wasm" crossorigin="anonymous" />
80+
)}
7581

7682
<!-- Open Graph -->
7783
<meta property="og:type" content="website" />

web/src/pages/app.astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const GITHUB_URL = "https://github.com/deaneeth/tiny-tpu";
1010
<Layout
1111
title="TinyTPU Visualizer — Interactive Systolic Array Simulator"
1212
description="Interactive TPU simulator: enter matrices and watch a 4×4 systolic array visualizer execute matrix multiply from real SystemVerilog RTL via WebAssembly."
13+
preloadWasm={true}
1314
>
1415
<div class="flex min-h-screen flex-col page">
1516
<!-- Nav - NOT sticky; the visualizer is a full-page tool, sticky overlaps content -->

web/src/pages/index.astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ const peCells = Array.from({ length: 16 }, (_, i) => {
7575
<Layout
7676
title="TinyTPU — Systolic Array Visualizer | Learn How a TPU Works"
7777
description="TPU simulator: 4×4 weight-stationary systolic array in synthesizable SystemVerilog, compiled to WebAssembly. Watch real RTL execute live in your browser."
78+
preloadWasm={true}
7879
>
7980
<div class="page flex flex-col min-h-screen">
8081

0 commit comments

Comments
 (0)