-
Notifications
You must be signed in to change notification settings - Fork 5.4k
refactor(skills): split performance into references #18119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| --- | ||
| name: performance | ||
| description: Performance patterns and anti-patterns for ethereum.org covering TTFB, LCP, INP, CLS, bundle size, build memory, and RSC payload. Use when diagnosing a perf regression or reviewing proposed code changes for perf landmines. | ||
| user-invocable: false | ||
| --- | ||
|
|
||
| # Performance Knowledge | ||
|
|
||
| Knowledge base. No runtime actions; authoring conventions for maintainers below. Read the relevant `references/*.md` files directly via the Read tool. | ||
|
|
||
| ## Two entry points | ||
|
|
||
| - **Diagnosing** an existing regression → `references/diagnose-table.md` (symptom → bucket). | ||
| - **Reviewing** a proposed change before merge → `references/review-checklist.md` (diff pattern → rules to load). | ||
|
|
||
| Both flows always also load `references/anti-patterns.md`. | ||
|
|
||
| ## File map | ||
|
|
||
| | Topic | File | | ||
| | ------------------------------- | -------------------------------- | | ||
| | Symptom → bucket routing | `references/diagnose-table.md` | | ||
| | Diff pattern → rules routing | `references/review-checklist.md` | | ||
| | TTFB, edge caching, CDN | `references/edge-caching.md` | | ||
| | Bundle size, code splitting | `references/bundle.md` | | ||
| | Build OOM, ENOSPC, file tracing | `references/build.md` | | ||
| | RSC payload, HTML size | `references/rsc.md` | | ||
| | INP, main-thread work | `references/inp.md` | | ||
| | LCP, images | `references/images.md` | | ||
| | Cross-cutting anti-patterns | `references/anti-patterns.md` | | ||
|
|
||
| For data-fetching patterns (Trigger.dev tasks, Netlify Blobs, `src/lib/data` caching, internal `/api/*` routes, fetcher retries), use the `data-layer` skill instead. | ||
|
|
||
| ## Conventions for adding rules | ||
|
|
||
| - One topic per file; no content duplication across files. | ||
| - Cite a PR (`PR #N`) when a rule comes from a specific shipped change. Add a SHA only when no PR exists. | ||
| - Forward-looking ("do this, avoid that"). Keep entries prescriptive — no postmortems or incident narratives. | ||
| - New review check? Add a row to `review-checklist.md` AND the matching bucket file. Don't restate the rule inline. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # Cross-cutting anti-patterns | ||
|
|
||
| These look right but aren't. Load alongside the bucket-specific file for any review or diagnose pass. | ||
|
|
||
| | Looks like… | Actually… | | ||
| |--------------------------------------------------------------|----------------------------------------------------------------------------| | ||
| | Adding `CDN-Cache-Control` to fix TTFB | Netlify strips them; CF ignores due to `max-age=0`. | | ||
| | Using `unstable_cache` in MDX routes | Triggers ISR → runtime 404s. See `edge-caching.md`. | | ||
| | `` await import(`.../${locale}/${ns}.json`) `` | Webpack enumerates all combinations → OOM. Use `fs.readFile`. | | ||
| | Adding a `useSession` / `cookies()` in root layout | Every route becomes dynamic. Edge caching dies silently. | | ||
| | `priority: true` via `getImageProps()` on manual `<picture>` | `fetchPriority` attribute is not set; add it manually. | | ||
| | Bumping `revalidate` to hourly to "fix staleness" | Increases edge miss rate. Keep daily; add client `/api/*` for freshness. | | ||
| | Mocking DB/content in perf tests | Perf wins must be measured against real prod build (`pnpm build && start`).| | ||
| | Removing `common.json` keys without audit | Unused-lookups throw at runtime. Use `scripts/audit-common-translations.ts`.| |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # Build-time: memory, disk, tracing | ||
|
|
||
| ## `fs.readFile` > `await import()` for bundler-invisible content (PR #17589) | ||
|
|
||
| When loading content with a variable path (i18n JSON by locale, markdown by slug), dynamic `import()` forces webpack/Turbopack to enumerate every possible chunk — 3000+ chunks across locales × namespaces. | ||
|
|
||
| ```ts | ||
| // Bad — webpack enumerates all locale×namespace combinations | ||
| const messages = await import(`../intl/${locale}/${namespace}.json`) | ||
|
|
||
| // Good — invisible to module graph | ||
| const messages = JSON.parse(await fs.readFile( | ||
| path.join(process.cwd(), "src/intl", locale, `${namespace}.json`), | ||
| "utf8" | ||
| )) | ||
| ``` | ||
|
|
||
| Same fix applied to tutorial MD under Turbopack (SHA `30e0ecc6b`, 143k+ file trace). | ||
|
|
||
| ## Netlify build failures | ||
|
|
||
| **OOM during webpack compile** → apply `fs.readFile` pattern above; verify with `NODE_OPTIONS="--max-old-space-size=8192"` locally. | ||
|
|
||
| **ENOSPC (disk full)** → `.next/cache` is ~4GB webpack cache that Netlify never reuses. Fix in `netlify.toml`: | ||
|
|
||
| ```toml | ||
| command = "pnpm build && rm -rf .next/cache" | ||
| ``` | ||
|
|
||
| (PR #17971, SHA `12c01e79f`). Also check if `.next/server/.segments/` (Next.js 16+) is inflating — it duplicates into `.next/standalone/`. | ||
|
|
||
| **Missing files at runtime** → force into serverless bundle via `outputFileTracingIncludes`: | ||
|
|
||
| ```ts | ||
| // next.config.js | ||
| outputFileTracingIncludes: { | ||
| "/**/*": ["./src/data/**/*"], | ||
| } | ||
| ``` | ||
|
|
||
| (SHA `f1425bdb4`). | ||
|
|
||
| **Large static assets bloating function** → move to `public/` and reference as URL (SHA `11e38234b` — 47MB mp3 reduction). Also exclude via `outputFileTracingExcludes`. | ||
|
|
||
| ## Turbopack-specific | ||
|
|
||
| Both rules apply to Turbopack only — webpack handles these cases correctly. | ||
|
|
||
| - `turbopackIgnore` comments **only work on actual `import()` call sites**, not on helper wrappers that call them. Move the magic comment to where the dynamic `import()` actually appears, or remove the wrapper (SHA `3fdc233616`). | ||
| - Turbopack file-tracing warnings only resolve **inline string literals** in dynamic import paths. If the path is built from variables (e.g. `${baseDir}/${file}`), Turbopack can't see the target — inline the constant or split the dynamic segment so the literal portion is visible. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # Bundle size / code splitting | ||
|
|
||
| ## Lazy-load heavy below-fold deps | ||
|
|
||
| Always lazy-load anything above ~20KB gz: Swiper, Prism (`prism-react-renderer`), Solidity highlighting, Radix Dialog bodies, modals, accordion content below the fold, chart libraries. | ||
|
|
||
| Consolidate `next/dynamic` imports in `app/[locale]/_components/*LazyImports.tsx` with `ssr: false`: | ||
|
|
||
| ```tsx | ||
| const CodeExamples = dynamic(() => import(".../CodeExamples"), { ssr: false }) | ||
| const AppsHighlight = dynamic(() => import(".../AppsHighlight"), { ssr: false }) | ||
| ``` | ||
|
|
||
| Examples shipped: | ||
|
|
||
| - PR #17958 — `CodeExamples`, `AppsHighlight` | ||
| - PR #17661 — mobile menu content (`React.lazy()` + first-click trigger) — saved ~82KB | ||
| - SHA `a1f876f8f` — persona modal | ||
| - SHA `355070dc7` — homepage wrapper after upgrade chain | ||
|
|
||
| ## Asset weight | ||
|
|
||
| - **SVG: optimize + provide dark variants** rather than filter hacks (SHA `33132d2f8`). | ||
|
|
||
| ## Verify | ||
|
|
||
| Run `pnpm build` and check the route-level First Load JS deltas. For RSC payload, use Chrome DevTools Network → filter `?_rsc=` or measure raw HTML size with `curl -s URL | wc -c`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Diagnose: symptom → bucket | ||
|
|
||
| Pick the matching row. Load the named reference file. Always also load `anti-patterns.md`. | ||
|
|
||
| **For LCP, the bucket is layered.** Work `images.md` → `bundle.md` → `rsc.md` in that order — load all three when LCP is the metric, since hero preload, JS budget, and HTML/RSC weight all contribute. | ||
|
|
||
| **For TTFB:** this is almost certainly a CF-caching / devops issue, not an app-side fix. Surface that up front before proposing Netlify header changes — see `edge-caching.md`. | ||
|
|
||
| | Symptom | Bucket | Reference file | | ||
| |-------------------------------------------------|---------------------------------|-----------------------| | ||
| | P75 TTFB > 800ms in field data | Edge caching / routing | `edge-caching.md` | | ||
| | LCP > 2.5s | Images → bundle → RSC (in order)| `images.md`, `bundle.md`, `rsc.md` | | ||
| | INP > 200ms on interaction | Main-thread / analytics / lists | `inp.md` | | ||
| | CLS > 0.1 | Image dimensions / late mounts | `images.md` | | ||
| | Large HTML payload (>500KB) | RSC / translations | `rsc.md` | | ||
| | Large JS bundle on first load | Code splitting / lazy | `bundle.md` | | ||
| | Netlify build OOM or ENOSPC | Build-time / webpack tracing | `build.md` | | ||
| | Slow page but fast TTFB and small bundle | Data fetching / cache misses | _see `data-layer` skill_ | | ||
|
|
||
| ## Baseline first | ||
|
|
||
| Always collect numbers before coding: PSI field data, Sentry LCP/INP P75, `web-vitals` in dev, `pnpm build` output, `.next/` dir sizes. **No before = no after.** | ||
|
|
||
| If the regression only reproduces in `pnpm dev`, rebuild with `pnpm build && pnpm start` before believing it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # Edge caching & TTFB | ||
|
|
||
| ## Established root cause (memorized) | ||
|
|
||
| Netlify Edge uses per-node LRU caching; low-traffic entries get evicted regardless of TTL. Edge misses fall to Durable cache in **us-east-1**, which is a 400–600ms round-trip for global users (top traffic: China, US, India). Cloudflare is in the path but does **not** cache HTML because `max-age=0` is set and `CDN-Cache-Control` headers are stripped by Netlify before reaching CF. | ||
|
|
||
| ## Patterns that work | ||
|
|
||
| 1. **Daily revalidate + client-side refresh API for volatile widgets** (PR #17815 — gas table). Keep the full page statically cacheable at `revalidate = 86400`; put volatile data in `/api/*` routes that the client calls on mount. Do not bake hourly data into the SSR'd HTML. | ||
|
|
||
| 2. **Module-level `Map` caches for hot request-path lookups** (PR #17864 — `getTranslatedLocales()`). Cuts per-build repeated filesystem work and fixes inconsistency across concurrent requests. | ||
|
|
||
| 3. **Cache translation/metadata registries** instead of re-reading from disk per request. Same pattern as `cachedStaticPages`. | ||
|
|
||
| ## DON'T | ||
|
|
||
| - Do NOT add `unstable_cache` + `revalidate` on MDX/markdown routes with `generateStaticParams`. Triggers ISR and silently 404s on dynamic segments when `src/data` is not in the serverless bundle. Use `cache: "force-cache"` or `React.cache()` instead (SHA `e4d7a342c`, `bd9732ae0`, `e61afdbe2`). | ||
| - Do NOT add root-layout hooks that read request data (Sentry `getTraceData`, cookies, headers). One dynamic hook in `app/layout.tsx` makes every page dynamic and destroys edge caching (PR #16718 diagnosis). | ||
| - Do NOT propose adding `CDN-Cache-Control` headers as a TTFB fix — Netlify strips them (PR #17838 closed). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # Images / LCP | ||
|
|
||
| 1. **Manual `<picture>` + `getImageProps()` drops `fetchPriority`.** Set it manually on the `<img>`: | ||
|
|
||
| ```tsx | ||
| <img {...props} fetchPriority="high" /> | ||
| ``` | ||
|
|
||
| (PR #17958). `priority: true` on `getImageProps` is not enough. | ||
|
|
||
| 2. **Remove `priority` from below-fold images** — they compete with the hero for preload budget (SHA `d41f2a9b3` trustlogos). | ||
|
|
||
| 3. **Re-encode oversized assets** — PNG → JPG, resize to display size (SHA `e2430a276`: 3.3MB → 119KB banner). | ||
|
|
||
| 4. **Tighten `sizes` to the actual rendered width.** Loose `sizes` (e.g. `100vw` on a contained image, or `1200px` on a 512px column) make the browser pick an oversized source from the `srcSet`. On throttled mobile, those non-LCP images steal bandwidth from the LCP element. Lighthouse "Improve image delivery" almost always points here. SHA `6dc51d32a2` cut ~200 KB / −150 ms LCP by tightening homepage carousel + trust-logo `sizes`. | ||
|
|
||
| Match `sizes` to the breakpoints that actually constrain the image. Account for container padding (`calc(100vw - 32px)`), max-width caps, and grid columns. Examples from `SavingsCarousel.tsx` / `TrustLogos.tsx`: | ||
|
|
||
| ```tsx | ||
| // Bad: 100vw lies on desktop where the carousel caps at ~512px | ||
| <Image sizes="(max-width: 768px) 100vw, 1200px" ... /> | ||
|
|
||
| // Good: reflects column width at each breakpoint | ||
| <Image sizes="(max-width: 1024px) 384px, 512px" ... /> | ||
|
|
||
| // Good: account for page padding on mobile | ||
| <Image sizes="(max-width: 480px) calc(100vw - 32px), 100vw" ... /> | ||
| ``` | ||
|
|
||
| Verify in DevTools: inspect the `<img>`, check `currentSrc` — the `&w=` should match the rendered CSS width × DPR, not the largest entry in `srcSet`. | ||
|
|
||
| 5. **Lower `quality` on non-LCP images** — `quality={65}` is visually indistinguishable from default `75` for photographic content, ~15% smaller. Add new values to the `qualities` allow-list in `next.config.js` (Next.js 15 only honors whitelisted qualities). SHA `6dc51d32a2`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # INP / main-thread work | ||
|
|
||
| ## Patterns | ||
|
|
||
| 1. **Virtualize lists above ~30 rows** using `@tanstack/react-virtual` (already a dep). Shipped for: | ||
| - `/get-eth/` country picker (240 options): INP 1048ms → 248ms (PR #17912, SHA `db6902280`). | ||
| - Find Wallets list (PR #16233). | ||
|
|
||
| For `react-select`, virtualize `MenuList`; for tables, wrap rows. | ||
|
|
||
| 2. **Defer analytics to idle.** `trackCustomEvent` in `requestIdleCallback` (fallback `setTimeout(0)`); cache opt-out flag in memory so per-click does not re-parse `localStorage`. Capture URL **synchronously** before the idle callback (race fix, SHA `3dcaef917`). | ||
|
|
||
| 3. **Guard Matomo `init()` with `isOptedOut()`** (PR #17922). The tracker script fires a request even without `trackPageView` calls when `init()` runs. | ||
|
|
||
| 4. **Keep heavy panels mounted** — `<PersistentPanel>` pattern (PR #16694). Drawers/sheets that re-mount filter state every open are a common INP source. | ||
|
|
||
| 5. **`startTransition` + `useDeferredValue`** for filter updates (PR #16694 ProductTable). | ||
|
|
||
| 6. **Single-pass `.filter()`** — chained `.filter().filter().filter()` forces N array traversals; combine into one predicate (SHA `298e00f9b`). | ||
|
|
||
| 7. **Custom `memo` comparator** on filter components that receive heavy props (SHA `16973e2c3`). | ||
|
|
||
| 8. **Defer below-fold sections with `Suspense` + skeletons** so above-fold paint is not blocked by KPI/chart/simulator hydration (SHA `4d212c956` — homepage KPI/Carousel/Simulator). Pair with `generateMetadata` for the streaming SSR boundary. | ||
|
|
||
| ## Measure with web-vitals in prod build | ||
|
|
||
| ```tsx | ||
| onINP((m) => console.log(m.value, m.attribution)) | ||
| ``` | ||
|
|
||
| Compare before/after on the actual slow interaction, not the whole page. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # Review: diff contains → load this | ||
|
|
||
| Mechanical scan of changed code against ethereum.org's perf landmines — patterns that pass normal review but silently regress perf because of decisions in `next.config.js`, our Netlify edge setup, or scars from past incidents. For general Next.js / React hygiene (Server Components first, lazy modals, `next/image`, virtualized lists, `startTransition`), rely on CLAUDE.md and standard tooling. | ||
|
|
||
| ## How to use | ||
|
|
||
| 1. List the changed files (`git diff --name-only <base>...HEAD`). | ||
| 2. Walk the table below, top to bottom. For each row, grep the diff for the pattern. | ||
| 3. On any hit, load the named reference file and check the change against its rules. | ||
| 4. Always also load `anti-patterns.md` — it's the cross-cutting list that catches what the table misses. | ||
|
|
||
| | Diff contains… | Load | | ||
| |----------------------------------------------------------------------|--------------------------------------------------------------------------------| | ||
| | Changes under `app/**/layout.tsx` (esp. root layout) | `edge-caching.md` — root-layout dynamic hooks kill edge caching | | ||
| | `cookies()` / `headers()` in a server component or route | `edge-caching.md` — opts the route out of static caching | | ||
| | `unstable_cache(` on an MDX-rendered route | `anti-patterns.md` — triggers ISR + silent 404s on dynamic segments | | ||
| | `import(\`…${` (template-literal dynamic import path) | `build.md` — Webpack/Turbopack enumerates all combos → Netlify OOM | | ||
| | `getImageProps(` used inside a manual `<picture>` | `images.md` — `priority: true` does NOT set `fetchPriority`; add it manually | | ||
| | New/changed `<Image …sizes=` on a non-LCP image | `images.md` — tighten `sizes` to actual rendered width; loose values over-fetch | | ||
| | New file `app/**/page.tsx` | `edge-caching.md`, `rsc.md` — verify `revalidate` strategy + RSC payload | | ||
| | New file under `app/api/` | `edge-caching.md` — TTL strategy, internal-only fetch boundary | | ||
| | New keys in `src/intl/en/common*.json` | `rsc.md` — server-only vs client (`common.json` vs `common-server.json`) | | ||
| | Changes to `next.config.js` / `outputFileTracing*` | `build.md` — function bundle size, included paths | | ||
| | New large static asset (mp3, video, large image) | `build.md` — should be in `/public`, excluded from function trace | | ||
| | New import of a heavy lib (chart, syntax highlighter, > 20KB gz) | `bundle.md` — verify it's lazy-imported via `LazyImports.tsx` | | ||
| | New data fetcher / cron / blob storage | _see `data-layer` skill_ | | ||
|
|
||
| If the diff is small or none of the rows apply cleanly, load `anti-patterns.md` alone and scan against it. | ||
|
|
||
| For investigating an existing field regression instead of reviewing a diff, use `diagnose-table.md`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # RSC / translation payload | ||
|
|
||
| Targets: HTML < 500KB, RSC push calls < 70 (preferably < 40). | ||
|
|
||
| ## Patterns | ||
|
|
||
| 1. **Server Component conversion** (PR #17650 — Footer). If a component has `useTranslation` and one trivial `onClick`, split out the interactive piece (e.g., `GoToTopButton.tsx`) and make the parent a server component using `getTranslations` from `next-intl/server`. ~95% less hydration. | ||
|
|
||
| ## Measurement | ||
|
|
||
| ```bash | ||
| curl -s -H "RSC: 1" "https://localhost:3000/en/?_rsc=1" | wc -c # raw bytes | ||
| ``` | ||
|
|
||
| Before/after examples from closed PR #17633: `/en/` 843 → 537KB, `/en/staking/` 595 → 282KB. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noting this does show up when you type the full
/performancebut anything less and it does not show the auto-complete -- should be find