agentleFS
PMDevSolutionsPMDevSolutions/Claudius/.claude/skills/react-performance-optimization/SKILL.md

react-performance-optimization

Skill1 starsChanged 8 months agoMITView on GitHub

Performance profiling and optimization for React applications. Covers React Profiler, bundle analysis, code splitting, memoization patterns, image optimization, Core Web Vitals, and server-side rendering performance. Keywords: performance, bundle size, code splitting, web vitals, profiling, React.memo, useMemo, useCallback, lazy loading, LCP, INP, CLS

  • Reads credentials
  • Installs packages

2 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

12 rules. About 0 are common, 7 are distinctive, and 5 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈7

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 5

    Very specific

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

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use this skill when:

    line 16
  2. Distinctive:

    Look for Layout Thrashing (forced synchronous layouts)

    line 80
  3. Very specific:

    Use priority prop on Next.js Image for hero images

    line 404
  4. Distinctive:

    Remove render-blocking JS from the critical path (code split)

    line 406
  5. Distinctive:

    Use a CDN for static assets

    line 407
  6. Very specific:

    Use useTransition for state updates that can be deferred

    line 416
  7. Distinctive:

    Avoid synchronous layout reads in event handlers

    line 419
  8. Very specific:

    Always set width and height on images and videos (or use aspect-ratio)

    line 452
  9. Distinctive:

    Avoid inserting content above existing content (banners, ads)

    line 454
  10. Very specific:

    Use font-display: swap with size-adjust to minimize font swap shift

    line 455
  11. Very specific:

    Use CSS contain: layout on sections that load independently

    line 456
  12. Distinctive:

    Run through this checklist when optimizing:

    line 529

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “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 Optimization

Overview

This skill provides a systematic approach to identifying and resolving performance issues in React applications. It covers profiling tools, bundle optimization, rendering optimization, asset loading, and Core Web Vitals improvement.

Core Principle: Measure first, optimize second. Never apply performance optimizations speculatively. Every optimization adds complexity; only add complexity when measurements show a real problem.

When to Use

Use this skill when:

  • Application feels slow or janky
  • Bundle size is too large (over 200KB gzipped for initial load)
  • Core Web Vitals scores are below thresholds
  • Components re-render too frequently
  • Page load time exceeds 3 seconds
  • Lighthouse Performance score is below 80

Trigger phrases:

  • "Optimize performance"
  • "Reduce bundle size"
  • "Fix slow rendering"
  • "Improve web vitals"
  • "Profile this component"
  • "Code split this route"
  • "Why is this component slow"

Profiling Tools

React DevTools Profiler

The primary tool for identifying unnecessary re-renders and slow components.

How to profile:

  1. Open React DevTools > Profiler tab
  2. Click "Record"
  3. Interact with the application (the specific flow that feels slow)
  4. Click "Stop"
  5. Analyze the flamegraph

What to look for:

  • Components that render when they should not (gray = did not render, colored = rendered)
  • Components with high "self time" (the component's own rendering cost, excluding children)
  • Cascading re-renders (a parent change causing deep tree re-renders)

Ranked chart view: Shows components sorted by render time. Focus on the top entries.

React Profiler Component (Programmatic)

import { Profiler, type ProfilerOnRenderCallback } from "react";

const onRender: ProfilerOnRenderCallback = (id, phase, actualDuration) => {
  if (actualDuration > 16) {
    // Longer than one frame at 60fps
    console.warn(`Slow render: ${id} (${phase}) took ${actualDuration.toFixed(2)}ms`);
  }
};

function App() {
  return (
    <Profiler id="App" onRender={onRender}>
      <MainContent />
    </Profiler>
  );
}

Chrome DevTools Performance Tab

For measuring overall page performance beyond React:

  1. Open DevTools > Performance
  2. Click Record, perform the action, click Stop
  3. Analyze the Main thread timeline for long tasks (> 50ms)
  4. Look for Layout Thrashing (forced synchronous layouts)

Bundle Analysis

Analyzing Bundle Size

# Vite: use rollup-plugin-visualizer
pnpm add -D rollup-plugin-visualizer

# Next.js: use @next/bundle-analyzer
pnpm add -D @next/bundle-analyzer

Vite configuration:

// vite.config.ts
import { visualizer } from "rollup-plugin-visualizer";

export default defineConfig({
  plugins: [
    react(),
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
      filename: "bundle-report.html",
    }),
  ],
});

Next.js configuration:

// next.config.ts
import withBundleAnalyzer from "@next/bundle-analyzer";

const config = withBundleAnalyzer({
  enabled: process.env.ANALYZE === "true",
})({
  // next config
});

export default config;
# Generate report
ANALYZE=true pnpm build

Bundle Size Budgets

CategoryBudget (gzipped)
Initial JS (first load)< 100KB
Per-route JS< 50KB
Total CSS< 50KB
Individual dependency< 30KB
Total first load (JS + CSS)< 200KB

Common Large Dependencies and Alternatives

Heavy LibrarySizeLighter AlternativeSize
moment.js72KBdate-fns (tree-shakeable)2-10KB
lodash (full)72KBlodash-es (tree-shakeable)2-5KB
chart.js60KBlightweight-charts or visxVaries
react-icons (full)40KB+Import individual icons< 1KB
axios13KBfetch (built-in)0KB

Check dependency size before installing:

# Check size of a package
npx bundlephobia-cli package-name

Code Splitting

Route-Based Splitting with React.lazy

import { lazy, Suspense } from "react";

// Each lazy import creates a separate chunk
const Dashboard = lazy(() => import("@/pages/dashboard"));
const Settings = lazy(() => import("@/pages/settings"));
const Analytics = lazy(() => import("@/pages/analytics"));

function App() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/analytics" element={<Analytics />} />
      </Routes>
    </Suspense>
  );
}

Component-Level Splitting

Split heavy components that are not always visible:

const HeavyEditor = lazy(() => import("@/components/rich-text-editor"));
const ChartDashboard = lazy(() => import("@/components/chart-dashboard"));

function ArticlePage({ article }: { article: Article }) {
  const [isEditing, setIsEditing] = useState(false);

  return (
    <div>
      <article>{article.content}</article>
      {isEditing && (
        <Suspense fallback={<EditorSkeleton />}>
          <HeavyEditor content={article.content} />
        </Suspense>
      )}
    </div>
  );
}

Named Exports with Lazy

React.lazy requires default exports. For named exports, use an intermediate module:

// Option 1: Re-export wrapper
const Chart = lazy(() =>
  import("@/components/charts").then((mod) => ({ default: mod.BarChart }))
);

// Option 2: Barrel file with default export
// components/charts/bar-chart-lazy.ts
export { BarChart as default } from "./bar-chart";

Next.js Dynamic Imports

import dynamic from "next/dynamic";

const DynamicEditor = dynamic(() => import("@/components/editor"), {
  loading: () => <EditorSkeleton />,
  ssr: false, // Disable SSR for browser-only components
});

Memoization Patterns

When to Use React.memo

Use when:

  • A component renders frequently but its props rarely change
  • The component is expensive to render (large DOM tree, complex calculations)
  • The parent re-renders often for reasons unrelated to this child

Do NOT use when:

  • Props change on every render (memoization overhead with no benefit)
  • The component is cheap to render (a few DOM elements)
  • You have not measured a performance problem
import { memo } from "react";

interface ExpensiveListProps {
  items: Item[];
  onSelect: (id: string) => void;
}

const ExpensiveList = memo(function ExpensiveList({ items, onSelect }: ExpensiveListProps) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id} onClick={() => onSelect(item.id)}>
          <ExpensiveItemRenderer item={item} />
        </li>
      ))}
    </ul>
  );
});

When to Use useMemo

Use when:

  • Computing a derived value that is expensive (sorting/filtering large arrays, complex calculations)
  • Creating an object/array that is passed as a prop to a memoized child

Do NOT use when:

  • The computation is trivial (simple arithmetic, string concatenation)
  • The result is not passed to memoized children
function ProductList({ products, filter }: Props) {
  // Good: filtering 10,000 products is expensive
  const filteredProducts = useMemo(
    () => products.filter((p) => p.category === filter).sort((a, b) => a.price - b.price),
    [products, filter]
  );

  // Bad: useMemo for trivial computation
  // const fullName = useMemo(() => `${first} ${last}`, [first, last]);
  // Just write: const fullName = `${first} ${last}`;

  return <MemoizedGrid items={filteredProducts} />;
}

When to Use useCallback

Use when:

  • Passing a callback to a memoized child component (React.memo)
  • The callback is a dependency of a child's useEffect

Do NOT use when:

  • The child is not memoized (the callback reference doesn't matter)
  • The function is used only in event handlers of the current component
function Parent() {
  const [items, setItems] = useState<Item[]>([]);

  // Good: MemoizedChild will not re-render when Parent re-renders for other reasons
  const handleDelete = useCallback((id: string) => {
    setItems((prev) => prev.filter((item) => item.id !== id));
  }, []);

  return <MemoizedChild onDelete={handleDelete} items={items} />;
}

Memoization Decision Flowchart

Is the component slow? (measured, not guessed)
├── No → Do nothing
└── Yes → Is it re-rendering unnecessarily?
    ├── No → Profile further (maybe the render itself is slow)
    │   └── Consider: useMemo for expensive computations
    └── Yes → Why is it re-rendering?
        ├── Parent re-renders → Wrap child in React.memo
        │   └── Props still change? → Stabilize props with useMemo/useCallback
        ├── Context changes → Split context or use context selectors
        └── Own state changes → Reduce state scope or split component

Image Optimization

Next.js Image Component

import Image from "next/image";

// Static import (best: enables automatic optimization)
import heroImage from "@/assets/images/hero.webp";

<Image
  src={heroImage}
  alt="Hero illustration"
  priority                    // Above the fold: load immediately
  placeholder="blur"          // Show blurred placeholder during load
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

// Remote images
<Image
  src="https://cdn.example.com/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  loading="lazy"              // Below the fold: lazy load
/>

General React Image Optimization

// Responsive images with srcset
<img
  src="/images/hero-800.webp"
  srcSet="/images/hero-400.webp 400w, /images/hero-800.webp 800w, /images/hero-1600.webp 1600w"
  sizes="(max-width: 768px) 100vw, 50vw"
  alt="Hero"
  loading="lazy"
  decoding="async"
  width={800}
  height={600}
/>

// Lazy-loaded image component
function LazyImage({ src, alt, ...props }: React.ImgHTMLAttributes<HTMLImageElement>) {
  return (
    <img
      src={src}
      alt={alt}
      loading="lazy"
      decoding="async"
      {...props}
    />
  );
}

Image Format Priority

  1. AVIF -- Best compression, growing browser support
  2. WebP -- Good compression, wide browser support
  3. JPEG -- Fallback for photos
  4. PNG -- Only for images requiring transparency
  5. SVG -- For icons and illustrations (inline or as component)

Core Web Vitals

LCP (Largest Contentful Paint) -- Target: < 2.5s

The largest visible element (usually hero image or heading).

Optimization strategies:

  • Preload the LCP resource: <link rel="preload" as="image" href="hero.webp">
  • Use priority prop on Next.js Image for hero images
  • Inline critical CSS (or use next/font for font optimization)
  • Remove render-blocking JS from the critical path (code split)
  • Use a CDN for static assets
  • Server-side render the above-fold content

INP (Interaction to Next Paint) -- Target: < 200ms

Time from user interaction to the next visual update.

Optimization strategies:

  • Break up long tasks with startTransition for non-urgent updates
  • Use useTransition for state updates that can be deferred
  • Move heavy computation to Web Workers
  • Debounce rapid-fire inputs (search, scroll handlers)
  • Avoid synchronous layout reads in event handlers
import { useTransition } from "react";

function SearchPage() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState<Result[]>([]);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (value: string) => {
    setQuery(value); // Urgent: update input immediately

    startTransition(() => {
      // Non-urgent: filter/search can be deferred
      setResults(filterResults(allData, value));
    });
  };

  return (
    <>
      <input value={query} onChange={(e) => handleSearch(e.target.value)} />
      {isPending ? <Spinner /> : <ResultsList results={results} />}
    </>
  );
}

CLS (Cumulative Layout Shift) -- Target: < 0.1

Visual stability; elements should not shift during load.

Optimization strategies:

  • Always set width and height on images and videos (or use aspect-ratio)
  • Reserve space for dynamic content with skeleton placeholders
  • Avoid inserting content above existing content (banners, ads)
  • Use font-display: swap with size-adjust to minimize font swap shift
  • Use CSS contain: layout on sections that load independently
// Skeleton placeholder reserves space
function CardSkeleton() {
  return (
    <div className="h-64 w-full animate-pulse rounded-lg bg-muted" />
  );
}

function CardGrid() {
  return (
    <Suspense fallback={
      <div className="grid grid-cols-3 gap-4">
        <CardSkeleton />
        <CardSkeleton />
        <CardSkeleton />
      </div>
    }>
      <AsyncCardGrid />
    </Suspense>
  );
}

Server-Side Rendering Performance

Streaming SSR (React 19 / Next.js App Router)

// app/page.tsx - Streaming with Suspense boundaries
import { Suspense } from "react";

export default function Page() {
  return (
    <div>
      {/* Sent immediately */}
      <Header />
      <HeroSection />

      {/* Streamed when ready */}
      <Suspense fallback={<ProductGridSkeleton />}>
        <ProductGrid />  {/* async server component */}
      </Suspense>

      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews />  {/* async server component, independent stream */}
      </Suspense>

      <Footer />
    </div>
  );
}

Static Generation vs. Dynamic Rendering

// Static: generated at build time (fastest)
// Good for: marketing pages, blog posts, documentation
export const dynamic = "force-static";

// Dynamic: rendered per-request
// Good for: personalized content, real-time data
export const dynamic = "force-dynamic";

// ISR: static with revalidation
// Good for: product pages, listings (fresh enough, fast)
export const revalidate = 3600; // Re-generate every hour

Performance Checklist

Run through this checklist when optimizing:

[ ] Measured the problem with React Profiler / Chrome DevTools
[ ] Bundle analyzed -- no oversized dependencies
[ ] Routes are code-split with React.lazy or dynamic imports
[ ] Images use next/image or responsive srcset with lazy loading
[ ] Fonts loaded with next/font or @fontsource with display swap
[ ] Above-fold content renders without JS (SSR or static)
[ ] LCP element preloaded
[ ] No layout shift from loading content (skeletons, dimensions set)
[ ] Heavy computations use useMemo (measured, not guessed)
[ ] Memoized children receive stable props (useCallback where needed)
[ ] Non-urgent updates wrapped in startTransition
[ ] Third-party scripts loaded with async/defer or next/script
[ ] Lighthouse Performance score >= 90

Common Mistakes

  1. Memoizing everything -- React.memo, useMemo, and useCallback have overhead. Only use them when profiling shows a real benefit.
  2. Not setting image dimensions -- Causes CLS. Always provide width/height or aspect-ratio.
  3. Loading all routes eagerly -- Code-split routes you don't need on initial load.
  4. Importing entire icon libraries -- Import individual icons: import { Search } from "lucide-react", not import * as Icons.
  5. Using index as key in dynamic lists -- Causes incorrect reconciliation. Use stable unique IDs.
  6. Fetching in useEffect without caching -- Use TanStack Query or SWR for data fetching with caching, deduplication, and stale-while-revalidate.
  7. Blocking the main thread -- Move heavy computation to Web Workers or break into smaller tasks with requestIdleCallback.

Skill Version: 1.0.0 Last Updated: 2026-03-11

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.

Reviews can't be read right now.

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.