diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index c853e2ef8..3a5372e1b 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -360,7 +360,7 @@ jobs: run: pnpm --filter @useupup/e2e-test test:e2e:drive-sandbox Lighthouse: - name: Lighthouse (landing SEO + Best Practices) + name: Lighthouse (landing SEO + Best Practices + mobile perf ratchet) runs-on: ubuntu-latest steps: - name: Checkout the repository @@ -384,17 +384,27 @@ jobs: # lhci boots `next start` itself and asserts per lighthouserc.cjs: # SEO=100 everywhere; Best Practices 100 on docs pages, ratcheted # on home/framework pages (third-party embed cookies). A11y is NOT - # asserted here — the axe ratchet above owns it; perf is excluded - # (noisy on shared runners; size-limit owns bundle weight). + # asserted here — the axe ratchet above owns it. Performance is not + # asserted in THIS config either; it has its own mobile run below. - name: Lighthouse audit + assertions (SEO / Best Practices) run: pnpm --filter @useupup/landing run lighthouse + # Mobile performance, on its own port and its own config, asserted + # at WARN level only: the score is genuinely noisy on a shared + # runner, so this is a visible ratchet in the log and the uploaded + # report rather than a gate. (`continue-on-error` is banned by the + # test-quality guard and unnecessary — warn assertions never fail.) + - name: Lighthouse mobile performance ratchet (warn-only) + run: pnpm --filter @useupup/landing run lighthouse:mobile + - name: Upload Lighthouse reports if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: nightly-lighthouse-reports - path: apps/landing/.lighthouseci-reports/ + path: | + apps/landing/.lighthouseci-reports/ + apps/landing/.lighthouseci-mobile-reports/ if-no-files-found: warn retention-days: 14 diff --git a/.gitignore b/.gitignore index a13d9b2ad..9056a7098 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,4 @@ apps/e2e-test/screenshots/ # filesystem report output uploaded as a CI artifact .lighthouseci/ apps/landing/.lighthouseci-reports/ +apps/landing/.lighthouseci-mobile-reports/ diff --git a/CLAUDE.md b/CLAUDE.md index 091a8f520..7ec773529 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -617,8 +617,15 @@ DrivePlugin`. All three popup providers now persist a token-expiry key and refre SEO=100 on every audited page and Best Practices 100 on docs pages / ratcheted ~0.74 on home+framework pages whose StackBlitz embed + ads tag set third-party cookies; a11y is deliberately NOT asserted — the axe ratchet - owns it, Lighthouse's a11y audits are axe-core anyway — and perf is excluded - as CI-runner noise; size-limit owns bundle weight), static `build:storybook` + owns it, Lighthouse's a11y audits are axe-core anyway; perf is not asserted + in THIS config. Performance is no longer skipped outright: a second step in + the same job runs `pnpm --filter @useupup/landing run lighthouse:mobile` + (config `apps/landing/lighthouserc.mobile.cjs`: mobile emulation, its own + port 4464 so both configs run back to back, median of 5) and asserts + `categories:performance` at **warn** level — a visible ratchet in the log and + the uploaded report, never a red night, since the score really is noisy on a + shared runner and `continue-on-error` is banned by the test-quality guard; + size-limit still owns bundle weight), static `build:storybook` for all six frameworks, `smoke:packages`, the mastra LLM evals (only when the `OPENROUTER_API_KEY` Actions secret exists — absent, the job goes green with a loud skip notice, never silently), and the **Drive-Sandbox** job — the live diff --git a/apps/e2e-test/landing/thumbs-flow.spec.ts b/apps/e2e-test/landing/thumbs-flow.spec.ts index ada1b3840..fbea39467 100644 --- a/apps/e2e-test/landing/thumbs-flow.spec.ts +++ b/apps/e2e-test/landing/thumbs-flow.spec.ts @@ -168,6 +168,13 @@ test.describe('Ask AI thumbs feedback', () => { ) .toBe(true) + // The demo section is client-only and mounts the first time it comes + // within ~400px of the viewport (it is the heaviest thing on the page, + // and on a phone it costs seconds of main-thread time nobody who never + // scrolls to it should pay). Scroll it into view so the Ask-AI panel + // below actually exists. + await page.locator('#demo').scrollIntoViewIfNeeded() + // Ask one short question and wait for the assistant's completed turn. const panel = page.locator('.upup-ie-ai-panel') const input = panel.locator('#upup-ai-message') diff --git a/apps/landing/lighthouserc.cjs b/apps/landing/lighthouserc.cjs index 1b28bf615..f7048aaa3 100644 --- a/apps/landing/lighthouserc.cjs +++ b/apps/landing/lighthouserc.cjs @@ -1,8 +1,10 @@ // Nightly Lighthouse gate for the public site — SEO + Best Practices ONLY. // Accessibility is deliberately not asserted here (the nightly axe ratchet in -// `pnpm run e2e:a11y` owns it — Lighthouse's a11y audits are axe-core anyway), -// and Performance is excluded (score is unusably noisy on shared CI runners; -// bundle weight is already gated by size-limit). +// `pnpm run e2e:a11y` owns it — Lighthouse's a11y audits are axe-core anyway). +// Performance is not asserted in THIS config: it runs alongside, mobile-only +// and warn-only, from `lighthouserc.mobile.cjs` (the score is noisy on shared +// CI runners, so it is a visible ratchet rather than a gate; bundle weight +// stays gated by size-limit). // // Run locally after building: // pnpm exec turbo run build --filter=@useupup/landing diff --git a/apps/landing/lighthouserc.mobile.cjs b/apps/landing/lighthouserc.mobile.cjs new file mode 100644 index 000000000..f1b247f34 --- /dev/null +++ b/apps/landing/lighthouserc.mobile.cjs @@ -0,0 +1,76 @@ +// Nightly Lighthouse MOBILE PERFORMANCE ratchet for the public site. +// +// Separate from `lighthouserc.cjs` (SEO + Best Practices, desktop defaults) +// because it needs mobile emulation, more runs, and a different port so both +// configs can run back to back in the same job. Every assertion here is `warn`, +// deliberately: the score IS noisy on a shared CI runner, so this exists to make +// a regression visible in the job log and the uploaded report, not to red the +// night. `continue-on-error` is banned by scripts/ci/test-quality-guard.mjs and +// is not needed — a warn-level assertion never fails the run. +// +// Thresholds are the measured post-fix local medians minus 0.05. Raise them when +// the site gets faster; never lower one to make a red go away without saying why. +// +// Run locally after building: +// pnpm exec turbo run build --filter=@useupup/landing +// pnpm --filter @useupup/landing run lighthouse:mobile + +const PORT = 4464 +const page = path => `http://localhost:${PORT}${path}` + +module.exports = { + ci: { + collect: { + // Both marketing shapes (home and a framework page share one + // parameterized layout) plus one docs page, the third template. + url: [page('/'), page('/react/'), page('/docs/getting-started/')], + numberOfRuns: 5, + startServerCommand: `pnpm exec next start -p ${PORT}`, + startServerReadyPattern: 'Ready in', + startServerReadyTimeout: 60000, + settings: { + onlyCategories: ['performance'], + // Default lighthouse emulation is already mobile (Moto G Power + // + simulated 4G); stated here so a future default change + // cannot silently turn this into a desktop run. + formFactor: 'mobile', + screenEmulation: { + mobile: true, + width: 412, + height: 823, + deviceScaleFactor: 1.75, + disabled: false, + }, + }, + }, + assert: { + // Judge the median of the five runs, not the worst one — a single + // runner hiccup must not read as a regression. + aggregationMethod: 'median', + assertMatrix: [ + { + // Home + framework pages: the live uploader demo, the + // StackBlitz editor and the scene animations all load + // behind viewport gates, so the initial page is light. + matchingUrlPattern: '^https?://[^/]+/(react/)?$', + // Measured medians on this build: / = 0.74, /react/ = 0.71. + assertions: { + 'categories:performance': ['warn', { minScore: 0.66 }], + }, + }, + { + // Docs pages carry no embeds at all. + matchingUrlPattern: '.*/docs/.*', + // Measured median on this build: 0.73. + assertions: { + 'categories:performance': ['warn', { minScore: 0.68 }], + }, + }, + ], + }, + upload: { + target: 'filesystem', + outputDir: '.lighthouseci-mobile-reports', + }, + }, +} diff --git a/apps/landing/package.json b/apps/landing/package.json index 22778bdb5..4c3123896 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -8,6 +8,7 @@ "start": "next start", "lint": "eslint . --max-warnings 0", "lighthouse": "lhci autorun --config=lighthouserc.cjs", + "lighthouse:mobile": "lhci autorun --config=lighthouserc.mobile.cjs", "typecheck": "fumadocs-mdx && tsc --noEmit", "test": "vitest run", "clean": "rm -rf .next node_modules/.cache" diff --git a/apps/landing/src/app/[framework]/page.tsx b/apps/landing/src/app/[framework]/page.tsx index dcec5fb53..5db8d5e71 100644 --- a/apps/landing/src/app/[framework]/page.tsx +++ b/apps/landing/src/app/[framework]/page.tsx @@ -1,7 +1,7 @@ import type { Metadata } from 'next' import { notFound } from 'next/navigation' import '@useupup/interactive-example/styles' -import { InteractiveExampleClient } from '@/components/InteractiveExampleClient' +import DeferredInteractiveExample from '@/components/DeferredInteractiveExample' import { interactiveExampleEnvProps } from '@/lib/interactive-example-props' import { FRAMEWORK_IDS, getFramework } from '@/lib/frameworks' import StructuredData from '@/components/StructuredData' @@ -78,7 +78,7 @@ export default async function FrameworkPage({ />
- {process.env.NODE_ENV === 'production' && ( <> + {/* lazyOnload: Hotjar is session-recording, never + needed for the page to work, and on a throttled + phone its loader competed with hydration for the + main thread. */}