agentleFS
jaballerjaballer/react-claude-skills/react-performance/SKILL.md

react-performance

Skill0 starsChanged 5 months agoMITView on GitHub

React performance and Core Web Vitals — LCP, INP, CLS, bundle size, lazy loading, code splitting, memoization patterns (useMemo, useCallback, React.memo), image optimization, font optimization, when NOT to optimize. Use when optimizing slow components, debugging performance, building for performance-critical pages (marketing/landing), or auditing Core Web Vitals. Triggers on "performance", "slow", "Core Web Vitals", "LCP", "INP", "CLS", "bundle", "lazy", "memoize", "image optimization".

  • Reads credentials

18 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use orch-pipeline, parallel-execution-optimizer and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • renepfSkill

    orch-pipeline

    Shared orchestration engine for the orch-* skill family. Defines the gated Research-Plan-TDD-Review-Commit pipeline, the size classifier, the agent map, and the two human gates that the orch-* operation skills delegate to. Not usually invoked directly.

    Also in 3 projects that use this

  • marcusgollSkill

    parallel-execution-optimizer

    Identify and execute independent operations in parallel for 3-5x speedup. Auto-analyzes task dependencies, groups into batches, launches parallel Task() calls. Applies to /optimize (5 checks), /ship pre-flight (5 checks), /implement (task batching), /prototype (N screens). Auto-triggers when detecting multiple independent operations in a phase.

    Also in 4 projects that use this

  • ronmkrSkill

    vite-patterns

    Vite build tool patterns including config, plugins, HMR, env variables, proxy setup, SSR, library mode, dependency pre-bundling, and build optimization. Activate when working with vite.config.ts, Vite plugins, or Vite-based projects.

    Also in 4 projects that use this

  • RojiosSkill

    defi-amm-security

    Security checklist for Solidity AMM contracts, liquidity pools, and swap flows. Covers reentrancy, CEI ordering, donation or inflation attacks, oracle manipulation, slippage, admin controls, and integer math.

    Also in 3 projects that use this

  • RojiosSkill

    security-bounty-hunter

    Hunt for exploitable, bounty-worthy security issues in repositories. Focuses on remotely reachable vulnerabilities that qualify for real reports instead of noisy local-only findings.

    Also in 4 projects that use this

  • renepfSkill

    plan-orchestrate

    Read a plan document, decompose it into steps, design a per-step agent chain from the ECC catalogue, and emit ready-to-paste /orchestrate custom prompts. Generative only — never invokes /orchestrate itself. Use when the user has a multi-step plan and wants to drive it through orchestrate without composing chains by hand.

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

6 rules. 0 are common, 3 are distinctive, and 3 are very specific to this project.

  • 0

    Common

    Most of the 18 edited versions we read keep these.

  • 3

    Distinctive

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When asked to "make this faster", check in this order.

    line 12
  2. Distinctive:

    Then: look at React-specific concerns (re-renders, expensive components)

    line 19
  3. Very specific:

    Use @tanstack/react-virtual or react-window for any list > 100 items.

    line 156
  4. Distinctive:

    Don't bother with the historical 47-link-tag favicon dance unless you're shipping to a true legacy audience.

    line 322
  5. Very specific:

    await document.fonts.ready: before screenshotting if you must use a custom font.

    line 356
  6. Very specific:

    Don't try to inline a @font-face declaration in the OG template — the screenshotter's headless browser still needs to download the font …

    line 358

“Common” is measured: kept by at least 6 in 10 of the 18 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

react-performance

Senior performance work is mostly not about React. It's about images, fonts, layout shift, and not shipping JS users don't need. Optimize React last.

The performance triage order

When asked to "make this faster", check in this order. The first three usually fix 80%.

  1. Images — are they sized, lazy-loaded below the fold, modern formats?
  2. Fonts — are they self-hosted, preloaded, font-display: swap?
  3. Layout shift — do images/ads have reserved space?
  4. Bundle size — what's in the JS? Code split anything route-bound?
  5. Third-party scripts — what's blocking?
  6. Then look at React-specific concerns (re-renders, expensive components)

Mid-level engineers reach for React.memo first. Senior engineers reach for <Image priority>.

Core Web Vitals (memorize these thresholds)

MetricWhat it measuresGoodNeeds work
LCP (Largest Contentful Paint)Time to render the biggest above-fold element≤ 2.5s> 4s
INP (Interaction to Next Paint)Latency on user interactions (replaces FID in 2024)≤ 200ms> 500ms
CLS (Cumulative Layout Shift)How much things visually jump≤ 0.1> 0.25

Plus useful supplements:

  • FCP (First Contentful Paint): first text/image, ≤ 1.8s
  • TTFB (Time to First Byte): server response, ≤ 0.8s
  • TBT (Total Blocking Time): main-thread blocked during load, ≤ 200ms

LCP — make the hero render fast

The LCP element is usually the hero image, hero heading, or hero video. Speed it up:

Image LCP fixes

// Next.js
import Image from 'next/image';

<Image
  src="/hero.jpg"
  alt="Build smarter, ship faster"
  width={1200}
  height={630}
  priority           // preload, skip lazy
  fetchPriority="high"
  sizes="100vw"      // or specific sizes per breakpoint
/>

Plain HTML equivalent:

<link rel="preload" as="image" href="/hero.avif" type="image/avif" fetchpriority="high" />
<picture>
  <source type="image/avif" srcset="/hero.avif" />
  <source type="image/webp" srcset="/hero.webp" />
  <img
    src="/hero.jpg"
    alt="…"
    width="1200"
    height="630"
    fetchpriority="high"
    decoding="async"
  />
</picture>
  • Always width/height (prevents CLS)
  • Modern formats: AVIF, WebP fallback to JPEG
  • Responsive sizes: srcset + sizes so phones don't download desktop images
  • Below-the-fold: loading="lazy" (default off for above-the-fold)
  • Don't lazy-load the LCP image — it should be eager + priority

Font LCP fixes

// Next.js — self-hosts and inlines optimally
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], display: 'swap' });

Plain HTML:

<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preload" as="font" type="font/woff2" href="/fonts/inter-var.woff2" crossorigin />
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-display: swap;   /* render with fallback first, swap when loaded */
}
  • Self-host WOFF2 — eliminates a third-party DNS hop
  • Variable fonts — one file for all weights
  • font-display: swap — render text immediately with fallback
  • Subset to needed characters (Latin only if your audience is Latin)

Render-blocking resources

  • Move non-critical CSS out of the critical path
  • Defer third-party scripts: <script defer> or <script async> (defer is usually right)
  • Inline critical CSS if you're squeezing TTFB

INP — keep interactions snappy

INP is the new FID. It measures the worst event handler delay over a session.

Top INP fixes

  1. Don't do expensive work in event handlers
    // Bad — re-filters 10k items on every keystroke, then renders
    onChange={e => setQuery(e.target.value)}  // OK
    // but if filtering happens during render and it's heavy → INP suffers
    
    // Fix: useDeferredValue
    const deferredQuery = useDeferredValue(query);
    const results = useMemo(() => filterHeavy(items, deferredQuery), [items, deferredQuery]);
  2. Break up long tasks
    // Yield to the browser between batches
    async function processChunked(items: Item[]) {
      for (let i = 0; i < items.length; i += 100) {
        processBatch(items.slice(i, i + 100));
        await new Promise(r => setTimeout(r, 0));  // yields
      }
    }
  3. Use CSS for visual feedback first
    <button className="active:scale-95 transition-transform">…</button>
    CSS feedback is instant; JS work can come after.
  4. startTransition for non-urgent updates
    const [isPending, startTransition] = useTransition();
    onClick={() => {
      startTransition(() => {
        setBigStateUpdate();   // marked as low-priority
      });
    }}
  5. Virtualize long lists Use @tanstack/react-virtual or react-window for any list > 100 items.

CLS — don't make things jump

CLS is the cheapest to fix. Common causes:

CauseFix
Images without width/heightAlways set them (or use aspect-ratio CSS)
Web fonts swapping in (FOUT/FOIT)font-display: swap + size-adjust fallback fonts
Dynamically injected ads/embedsReserve fixed space with min-height
Late-loading hero textRender server-side, not client-side
Animating layout propertiesAnimate transform instead

Reserve aspect ratio:

.video-embed { aspect-ratio: 16 / 9; }
.image-wrapper { aspect-ratio: var(--ratio); }

Or use size-adjust to match fallback font metrics to web font:

@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 107%;
  ascent-override: 90%;
  /* tools: github.com/seek-oss/capsize */
}

Bundle size — what to ship, what to defer

See what's in your bundle

  • Next.js: ANALYZE=true next build (with @next/bundle-analyzer)
  • Vite: vite-plugin-visualizer
  • General: source-map-explorer

Common bundle bloat causes

CauseFix
Importing the whole lodashimport debounce from 'lodash-es/debounce' (or use native)
Moment.jsUse date-fns (tree-shakable) or Intl.DateTimeFormat
Whole icon libraryPer-icon imports or use SVG inline
Bundling polyfills for modern browsersSet browserslist to "modern"
Client component when server would doUse Server Components (Next App Router)
Importing dev-only codeCheck that process.env.NODE_ENV branches are dead-code-eliminated

Code splitting

// Route-level (Next App Router does this automatically)
// Per-page is already split

// Component-level lazy load
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <ChartSkeleton />,
  ssr: false,  // if it can't SSR
});

// Plain React
const HeavyChart = lazy(() => import('./HeavyChart'));
<Suspense fallback={<ChartSkeleton />}>
  <HeavyChart />
</Suspense>

Good candidates for code splitting:

  • Modals / drawers (only loaded when opened)
  • Below-the-fold sections (use IntersectionObserver to trigger)
  • Heavy editors / charts / maps
  • Localized content not on the current path

React-specific perf (only after the above)

When to memoize

useMemo — when:

  • The computed value is genuinely expensive (filtering 1000+ items, parsing, formatting)
  • The value is a referenced dependency of another hook that re-runs unnecessarily
  • The value is passed as a prop to a React.memo'd child

useCallback — when:

  • The function is a referenced dependency in useEffect / useMemo
  • The function is passed to a React.memo'd child
  • The function is in a deeply re-rendering tree

React.memo — when:

  • The component is rendered many times in a list
  • The component is expensive AND props are stable
  • You've profiled and confirmed re-renders are the bottleneck

When NOT to memoize:

  • Cheap components (a button, a small div)
  • Components rendered once
  • Props are inline objects/arrays anyway ({ x: 1 } is new every render → memo useless)
// Often-wrong premature memo
const Greeting = React.memo(({ name }: { name: string }) => <p>Hello {name}</p>);
// Cost: equality check on every render. Benefit: zero, since <p> is cheap.

React 19+ note

React Compiler (when enabled) handles most memoization automatically. If you're on React 19 + Compiler, you can stop manually adding useMemo/useCallback and just let the compiler. Mention this in a walkthrough if relevant.

Common React perf footguns

// Bad — new object every render, defeats child memo
<Child config={{ foo: 1 }} />

// Bad — inline component, remounts every render
function Parent() {
  function Inner() { return <div />; }
  return <Inner />;
}

// Bad — useState init runs every render
const [data] = useState(expensiveCompute());
// Good — lazy init
const [data] = useState(() => expensiveCompute());

// Bad — useEffect with object dep, runs every render
useEffect(() => {…}, [{ id: userId }]);
// Good — primitive dep
useEffect(() => {…}, [userId]);

Image optimization checklist

  • width and height attributes set (or aspect-ratio CSS)
  • loading="lazy" for below-the-fold
  • loading="eager" + fetchpriority="high" for the LCP image
  • Modern format (AVIF / WebP) with JPEG/PNG fallback
  • Responsive srcset + sizes
  • decoding="async" (Next/Image does this by default)
  • alt text (a11y, not perf, but never miss)

Font optimization checklist

  • Self-hosted WOFF2 (avoid third-party CSS files)
  • font-display: swap
  • Subset to needed character ranges
  • Variable font if you use 3+ weights
  • <link rel="preload"> for the critical weight
  • Fallback font with size-adjust to match metrics

Social + brand assets

A separate problem from photos and fonts — these ship in the document head, get crawled by social platforms, and need their own sizing/format rules.

The five files every production site needs

FileSizeFormatWhere it goes
favicon.svg< 2 KBSVG<link rel="icon" type="image/svg+xml" href="/favicon.svg">
favicon.ico (legacy fallback)< 15 KBICO with 16×16 + 32×32<link rel="icon" sizes="32x32" href="/favicon.ico"> — only if you must support pre-Chromium Edge or older Safari
apple-touch-icon.png< 10 KBPNG, 180×180<link rel="apple-touch-icon" href="/apple-touch-icon.png"> (iOS rounds the corners itself)
og.png< 500 KB (target ~200 KB)PNG or JPG, 1200×630<meta property="og:image" content="…"> + Twitter Card
manifest.webmanifest (optional PWA)< 1 KBJSON<link rel="manifest" href="/manifest.webmanifest">

Favicon — SVG primary, PNG fallback

SVG favicons render crisp at any DPI, scale to OS dark mode, and are tiny. Use SVG as primary and only add a PNG/ICO fallback if your audience includes browsers that don't support SVG icons (basically: pre-2019 Edge).

Don't bother with the historical 47-link-tag favicon dance unless you're shipping to a true legacy audience. Modern minimum:

<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />

For a dark-aware SVG favicon:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
  <rect width="64" height="64" rx="14" fill="light-dark(#aa3bff, #c084fc)" />
  <path d="…" fill="light-dark(#fff, #0c0a14)" />
</svg>

light-dark() is supported on all modern browsers and respects color-scheme: light dark.

OG / Twitter card — the 1200×630 image

Standard dimensions across platforms (Facebook, LinkedIn, Twitter/X, Slack unfurl, iMessage rich link):

  • 1200 × 630 (1.91:1 aspect)
  • PNG or JPG, max ~5 MB (platform caps), target ~200–500 KB
  • Critical text in the center 1200×600, since some platforms crop the edges
  • No fine text — render at 1× and you're done; mobile previews are ~300px wide, so anything smaller than 24px on a 1200×630 canvas is illegible

Generating OG cards via screenshot — system fonts only

A common pattern is rendering an HTML template at 1200×630 and screenshotting it. The trap: web fonts haven't loaded yet when the screenshot fires, so you get FOIT/FOUT artifacts (missing characters, fallback fonts) baked into your card.

Two ways out:

  1. Use system fonts in the OG template only. -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif. Looks fine in a card, dodges the font-loading race entirely.
  2. await document.fonts.ready before screenshotting if you must use a custom font.

Don't try to inline a @font-face declaration in the OG template — the screenshotter's headless browser still needs to download the font file before rendering.

The meta tag set (paste-ready)

<!-- Open Graph — https://ogp.me -->
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Your Site" />
<meta property="og:title" content="Your Title" />
<meta property="og:description" content="One sentence." />
<meta property="og:url" content="https://example.com/" />
<meta property="og:image" content="https://example.com/og.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Describe the card visually." />

<!-- Twitter Cards — https://developer.x.com/en/docs/x-for-websites/cards/ -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Your Title" />
<meta name="twitter:description" content="One sentence." />
<meta name="twitter:image" content="https://example.com/og.png" />
<meta name="twitter:image:alt" content="Describe the card visually." />

<!-- Theme + color scheme -->
<meta name="theme-color" content="#aa3bff" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#16171d" media="(prefers-color-scheme: dark)" />
<meta name="color-scheme" content="light dark" />

<!-- Canonical -->
<link rel="canonical" href="https://example.com/" />

Both og: and twitter: blocks are needed — Twitter prefers its own tags but falls back to OG; LinkedIn reads only OG; Slack reads both. Ship both.

Brand asset checklist

  • favicon.svg exists, references brand color, dark-mode aware
  • apple-touch-icon.png 180×180, brand background, no rounded corners (iOS rounds them)
  • og.png 1200×630, < 500 KB, key content centered
  • Open Graph tags present (og:title, og:description, og:url, og:image + width/height/alt)
  • Twitter Card tags present (twitter:card="summary_large_image", title/description/image/alt)
  • theme-color meta with light/dark variants
  • canonical link
  • Tested in https://www.opengraph.xyz/ or https://cards-dev.twitter.com/validator (or equivalent)

Performance audit walkthrough

  1. Lighthouse in Chrome DevTools → mobile, simulated 4G, "Performance" category
  2. WebPageTest for real-world conditions across geographies
  3. Chrome DevTools Performance panel for INP / main-thread profiling
  4. Coverage panel for unused JS/CSS
  5. Network panel with throttling for image/font loading order
  6. Real User Monitoring in production: Vercel Speed Insights, Cloudflare Web Analytics, or Netlify's own RUM

When the interviewer asks "how would you make this faster?"

Senior answer follows the order from this skill:

  1. "First I'd check the LCP element — usually an image. Make sure it's sized, in a modern format, and not lazy-loaded."
  2. "Then fonts — font-display: swap, self-hosted, preload the critical weight."
  3. "Then layout shift — reserve aspect ratios for media."
  4. "Then bundle — what's importing the whole lodash? What can be a Server Component?"
  5. "Then React-level — but I'd profile before assuming React.memo matters."
  6. "I'd measure on a throttled mobile profile, not my MacBook on wifi."

Narration phrases

  • "The LCP element is the hero image, so I gave it priority and a modern format."
  • "I'm not adding useMemo here — the children are cheap, and the wrap cost would exceed the recompute cost."
  • "I deferred the chart with dynamic() because it's below the fold and weighs 60 KB."
  • "I virtualized the list because we render 500 rows and only ~10 are visible."
  • "I wrapped the search filter in useDeferredValue so keystrokes stay responsive even on slow phones."
  • "The animation is on transform, not top/width, so it doesn't trigger layout — keeps CLS at zero."

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.