Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions apps/web/perf-baseline.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"updatedAt": "2026-08-03T20:35:00.443Z",
"metrics": {
"mainJsGzip": 258,
"mainCssGzip": 34923,
"mainJsGzip": 423231,
"mainCssGzip": 36274,
"stackBuilderJsGzip": 77059,
"largestJsGzip": 665363,
"totalJsGzip": 1978679
Expand All @@ -13,5 +13,19 @@
"stackBuilderJsGzip": 12288,
"largestJsGzip": 20480,
"totalJsGzip": 81920
},
"entryMeasurementVersion": 2,
"entryMeasurementMigration": {
"date": "2026-09-05T14:34:40.633Z",
"source": "origin/main at 8ea3b9b70 before performance changes, built with Vite manifest enabled",
"previousMainJsGzip": 258,
"assets": {
"js": [
"assets/index-gRR60bg9.js"
],
"css": [
"assets/index-DOVWU8Xw.css"
]
}
}
}
52 changes: 38 additions & 14 deletions apps/web/scripts/check-performance-budget.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { gzipSync } from "node:zlib";
import { collectEntryAssets } from "./performance-entry-assets.mjs";

const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname);
const WEB_DIR = path.resolve(SCRIPT_DIR, "..");
const ASSETS_DIR = path.resolve(WEB_DIR, ".vercel/output/static/assets");
const MANIFEST_PATH = path.resolve(ASSETS_DIR, "../.vite/manifest.json");
const BASELINE_PATH = path.resolve(WEB_DIR, "perf-baseline.json");
const REPORT_DIR = path.resolve(WEB_DIR, "reports/performance");
const CURRENT_REPORT_PATH = path.resolve(REPORT_DIR, "current.json");
Expand All @@ -18,8 +20,6 @@ const TRACKED_KEYS = [
"totalJsGzip",
];

const MAIN_JS_PATTERNS = [/^main-.*\.js$/, /^index-.*\.js$/];
const MAIN_CSS_PATTERNS = [/^main-.*\.css$/, /^index-.*\.css$/];
const LOCALIZED_CONTENT_JS_PATTERN =
/^(?:localized-content-|(?:es|zh-Hant|zh|ja|ko|de|fr|uk)[.-]).*\.js$/;
const LAZY_SYNTAX_JS_PATTERN = /^lazy-syntax-(?:language|theme)-.*\.js$/;
Expand Down Expand Up @@ -54,10 +54,16 @@ async function getFileSize(filePath) {
};
}

function findAsset(entries, patterns) {
return [...entries]
.sort((a, b) => a.gzip - b.gzip || a.file.localeCompare(b.file))
.find((entry) => patterns.some((pattern) => pattern.test(entry.file)));
async function measureEntryAssets(files) {
if (files.length === 0) throw new Error("Required entry assets are missing from client manifest");
let raw = 0;
let gzip = 0;
for (const file of files) {
const size = await getFileSize(path.resolve(ASSETS_DIR, "..", file));
raw += size.raw;
gzip += size.gzip;
}
return { file: files.join(", "), raw, gzip };
}

function isLocalizedContentAsset(file) {
Expand Down Expand Up @@ -85,8 +91,10 @@ async function collectMetrics() {
cssSizes.push({ file, ...size });
}

const mainJs = findAsset(jsSizes, MAIN_JS_PATTERNS);
const mainCss = findAsset(cssSizes, MAIN_CSS_PATTERNS);
const manifest = JSON.parse(await fs.readFile(MANIFEST_PATH, "utf8"));
const entryAssets = collectEntryAssets(manifest);
const mainJs = await measureEntryAssets(entryAssets.js);
const mainCss = await measureEntryAssets(entryAssets.css);
const stackBuilderJs = [...jsSizes]
.filter((entry) => /^stack-builder-.*\.js$/.test(entry.file))
.sort((a, b) => b.gzip - a.gzip || a.file.localeCompare(b.file))[0];
Expand Down Expand Up @@ -117,6 +125,8 @@ async function collectMetrics() {

return {
generatedAt: new Date().toISOString(),
entryMeasurementVersion: 2,
entryAssets,
assetCount: {
js: jsSizes.length,
budgetedJs: budgetedJsSizes.length,
Expand Down Expand Up @@ -205,17 +215,27 @@ async function writeSummary(summaryLines) {
await fs.writeFile(SUMMARY_PATH, `${summaryLines.join("\n")}\n`, "utf8");
}

async function updateBaseline(current) {
let budgets = { ...DEFAULT_BUDGETS };
async function readExistingBaseline() {
try {
const existing = JSON.parse(await fs.readFile(BASELINE_PATH, "utf8"));
if (existing?.budgets) budgets = { ...budgets, ...existing.budgets };
} catch {
// Baseline does not exist yet.
return JSON.parse(await fs.readFile(BASELINE_PATH, "utf8"));
} catch (error) {
if (error?.code === "ENOENT") return null;
throw error;
}
}

async function updateBaseline(current) {
const existing = await readExistingBaseline();
if (existing && existing.entryMeasurementVersion !== 2) {
throw new Error(
"Entry measurement changed to the manifest's static dependency graph. Migrate the entry baseline using the pre-change build before updating it; do not reset it to the optimized build.",
);
}
const budgets = { ...DEFAULT_BUDGETS, ...existing?.budgets };

const baseline = {
updatedAt: new Date().toISOString(),
entryMeasurementVersion: 2,
Comment thread
Marve10s marked this conversation as resolved.
metrics: Object.fromEntries(TRACKED_KEYS.map((key) => [key, current.metrics[key]])),
budgets,
};
Expand All @@ -239,6 +259,10 @@ async function checkAgainstBaseline(current) {
const baselineRaw = await fs.readFile(BASELINE_PATH, "utf8");
const baseline = JSON.parse(baselineRaw);

if (baseline.entryMeasurementVersion !== 2) {
throw new Error("Entry measurement changed to the manifest's static dependency graph. Migrate the entry baseline using the pre-change build before comparing; do not reset it to the optimized build.");
}

for (const key of TRACKED_KEYS) {
if (typeof baseline?.metrics?.[key] !== "number") {
throw new Error(`Baseline is missing metric "${key}"`);
Expand Down
22 changes: 22 additions & 0 deletions apps/web/scripts/performance-entry-assets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export function collectEntryAssets(manifest) {
const entryKeys = Object.keys(manifest).filter(
(key) => manifest[key].isEntry && manifest[key].file.endsWith(".js"),
);
if (entryKeys.length === 0) throw new Error("Client manifest has no JavaScript entry");

const visited = new Set();
const js = new Set();
const css = new Set();
const visit = (key) => {
if (visited.has(key)) return;
const chunk = manifest[key];
if (!chunk) throw new Error(`Missing static dependency in client manifest: ${key}`);
visited.add(key);
js.add(chunk.file);
for (const file of chunk.css ?? []) css.add(file);
for (const dependency of chunk.imports ?? []) visit(dependency);
};
for (const key of entryKeys) visit(key);

return { js: [...js].sort(), css: [...css].sort() };
}
Binary file added apps/web/src/assets/fonts/Geist-Variable.woff2
Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion apps/web/src/components/campaign/run-before-clone-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from "react-icons/tb";

import { TechIcon } from "@/components/ui/tech-icon";
import { trackCampaignEvent } from "@/lib/analytics/campaign-analytics";
import { trackCampaignEvent } from "@/lib/analytics/campaign-events";
import {
CAMPAIGN_BUILDER_SEARCH,
CAMPAIGN_PRESETS,
Expand Down
16 changes: 13 additions & 3 deletions apps/web/src/components/changelog-widget.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useCallback, useEffect, useState } from "react";
import { lazy, Suspense, useCallback, useEffect, useState } from "react";

import { ChangelogModal } from "@/components/changelog-modal";
import { registerVisit } from "@/lib/analytics/visitor";
import { latestChangelogRelease } from "@/lib/content/changelog";
import {
Expand All @@ -13,6 +12,11 @@ import { getLocaleDateTag } from "@/lib/i18n/locales";
import { m } from "@/paraglide/messages.js";
import { getLocale } from "@/paraglide/runtime.js";

const ChangelogModal = lazy(async () => {
const { ChangelogModal } = await import("@/components/changelog-modal");
return { default: ChangelogModal };
});

function formatReleaseDate(publishedAt: string, fallback: string): string {
const parsed = new Date(publishedAt);
if (Number.isNaN(parsed.getTime())) return fallback;
Expand All @@ -27,6 +31,7 @@ function formatReleaseDate(publishedAt: string, fallback: string): string {
export function ChangelogWidget() {
const [isVisible, setIsVisible] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [hasOpenedModal, setHasOpenedModal] = useState(false);

useEffect(() => {
if (!latestChangelogRelease) return;
Expand Down Expand Up @@ -65,6 +70,7 @@ export function ChangelogWidget() {

markInteracted("opened");
setIsVisible(false);
setHasOpenedModal(true);
setIsModalOpen(true);
}, [markInteracted]);

Expand Down Expand Up @@ -148,7 +154,11 @@ export function ChangelogWidget() {
</div>
) : null}

<ChangelogModal open={isModalOpen} onOpenChange={setIsModalOpen} />
{hasOpenedModal && (
<Suspense fallback={null}>
<ChangelogModal open={isModalOpen} onOpenChange={setIsModalOpen} />
</Suspense>
)}
</>
);
}
37 changes: 27 additions & 10 deletions apps/web/src/components/effects/shader-canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export function ShaderCanvas({ fragmentShader, className, uniforms, paused }: Sh
uniformsRef.current = uniforms;
const pausedRef = useRef(paused);
pausedRef.current = paused;
const updatePlaybackRef = useRef<(() => void) | null>(null);

useEffect(() => {
updatePlaybackRef.current?.();
}, [paused]);

useEffect(() => {
const canvas = canvasRef.current;
Expand Down Expand Up @@ -73,6 +78,8 @@ export function ShaderCanvas({ fragmentShader, className, uniforms, paused }: Sh
const uResolution = gl.getUniformLocation(program, "u_resolution");

let raf = 0;
let inView = false;
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
const start = performance.now();
let lastT = 0;

Expand All @@ -91,6 +98,8 @@ export function ShaderCanvas({ fragmentShader, className, uniforms, paused }: Sh
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

const render = () => {
raf = 0;
if (!inView || document.hidden) return;
if (!pausedRef.current) {
resize();
const t = (performance.now() - start) / 1000;
Expand Down Expand Up @@ -122,22 +131,30 @@ export function ShaderCanvas({ fragmentShader, className, uniforms, paused }: Sh
} else {
gl.uniform1f(uTime, lastT);
}
raf = requestAnimationFrame(render);
};
render();

const onVisibility = () => {
if (document.hidden) {
cancelAnimationFrame(raf);
} else {
if (!pausedRef.current && !reducedMotion.matches) {
raf = requestAnimationFrame(render);
}
};
document.addEventListener("visibilitychange", onVisibility);

const updatePlayback = () => {
cancelAnimationFrame(raf);
render();
};
updatePlaybackRef.current = updatePlayback;
const observer = new IntersectionObserver(([entry]) => {
inView = entry.isIntersecting;
updatePlayback();
});
observer.observe(canvas);
document.addEventListener("visibilitychange", updatePlayback);
reducedMotion.addEventListener("change", updatePlayback);

return () => {
updatePlaybackRef.current = null;
cancelAnimationFrame(raf);
document.removeEventListener("visibilitychange", onVisibility);
observer.disconnect();
document.removeEventListener("visibilitychange", updatePlayback);
reducedMotion.removeEventListener("change", updatePlayback);
gl.deleteProgram(program);
gl.deleteShader(vs);
gl.deleteShader(fs);
Expand Down
39 changes: 27 additions & 12 deletions apps/web/src/components/home/combinations-section.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import { motion } from "motion/react";
import { useEffect, useMemo, useState } from "react";
import { motion, useInView, useReducedMotion } from "motion/react";
import { useEffect, useMemo, useRef, useState } from "react";

import { combinationsMetrics } from "@/lib/builder/combinations-count";
import { HOME_COMBINATIONS_METRICS } from "@/lib/project/home-display-data";
import { PROJECT_ECOSYSTEM_COPY } from "@/lib/project/project-stats";
import { m } from "@/paraglide/messages.js";

const { totalScientific, yearsAtOneMillisecondScientific, universeLifetimesScientific } =
combinationsMetrics;
HOME_COMBINATIONS_METRICS;

export default function CombinationsSection() {
const sectionRef = useRef<HTMLElement>(null);
const inView = useInView(sectionRef);
const reducedMotion = useReducedMotion();
const funFacts = useMemo(
() => [
m.homeFactUniverseLifetimes({
mantissa: universeLifetimesScientific.mantissa,
exponent: universeLifetimesScientific.exponent,
}),
m.homeFactSand({
mantissa: combinationsMetrics.universeSandRatioScientific.mantissa,
exponent: combinationsMetrics.universeSandRatioScientific.exponent,
mantissa: HOME_COMBINATIONS_METRICS.universeSandRatioScientific.mantissa,
exponent: HOME_COMBINATIONS_METRICS.universeSandRatioScientific.exponent,
}),
m.homeFactEcosystems(PROJECT_ECOSYSTEM_COPY),
m.homeFactUnique(),
Expand All @@ -28,14 +31,26 @@ export default function CombinationsSection() {
const [factIndex, setFactIndex] = useState(0);

useEffect(() => {
const id = window.setInterval(() => {
setFactIndex((i) => (i + 1) % funFacts.length);
}, 4000);
return () => window.clearInterval(id);
}, [funFacts.length]);
if (!inView || reducedMotion) return;
let interval: number | undefined;
const update = () => {
window.clearInterval(interval);
if (!document.hidden) {
interval = window.setInterval(() => {
setFactIndex((i) => (i + 1) % funFacts.length);
}, 4000);
}
};
update();
document.addEventListener("visibilitychange", update);
return () => {
window.clearInterval(interval);
document.removeEventListener("visibilitychange", update);
};
}, [funFacts.length, inView, reducedMotion]);

return (
<section className="relative border-t border-border bg-muted/30">
<section ref={sectionRef} className="relative border-t border-border bg-muted/30">
<div className="px-4 py-20 sm:px-8 sm:py-28">
<div className="grid grid-cols-12 items-end gap-x-4 gap-y-6">
<div className="col-span-12 sm:col-span-6">
Expand Down
Loading
Loading