agentleFS
Shiven0504Shiven0504/claude-skills-collection/skills/custom/perf-optimizer/SKILL.md

perf-optimizer

Skill6 starsChanged 8 months agoMITView on GitHub

Optimize frontend performance in Next.js/React applications — Core Web Vitals, bundle size reduction, lazy loading, image optimization, code splitting, caching strategies, and rendering performance. Use this skill whenever the user mentions performance, page speed, slow loading, Core Web Vitals, LCP, CLS, INP, bundle size, code splitting, lazy loading, memoization, or wants to make their app faster. Also trigger when the user asks about optimizing images, reducing JavaScript, improving Time to Interactive, or diagnosing rendering bottlenecks.

  • Reads credentials
  • Installs packages

2 other projects use a version of this.

Adopted by

Most recently taken up by

The file

Show its rules and text

We split this skill into the rules it gives an agent

13 rules. About 0 are common, 4 are distinctive, and 9 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈4

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 9

    Very specific

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

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Don't load components the user might never see (modals, charts, editors) upfront:

    line 122
  2. Very specific:

    Use import { specific } not import *

    line 196
  3. Very specific:

    Check that libraries export ESM (look for "module" field in their package.json)

    line 197
  4. Distinctive:

    Avoid side effects in module scope

    line 198
  5. Distinctive:

    Keep pages static whenever possible — they're served from CDN and are the fastest option.

    line 405
  6. Distinctive:

    When a user asks "make my app faster," start with these high-impact items:

    line 409
  7. Very specific:

    Add priority to the LCP image

    line 411
  8. Very specific:

    Use next/image for all images with proper sizes

    line 412
  9. Very specific:

    Use next/font instead of <link> to Google Fonts

    line 413
  10. Very specific:

    Add loading.tsx skeletons for async pages

    line 415
  11. Very specific:

    Check bundle with ANALYZE=true npm run build — look for obvious bloat

    line 417
  12. Very specific:

    Use revalidate on fetch calls instead of no-store where possible

    line 418
  13. Very specific:

    Ensure no useEffect fetching in client components — move to server components

    line 420

≈ “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

Frontend Performance Optimization

Help users diagnose and fix performance issues in Next.js/React applications with Tailwind CSS. Focus on high-impact changes rather than micro-optimizations.

Core Web Vitals

Google's Core Web Vitals are the three metrics that matter most for real-world user experience:

MetricWhat it measuresGoodNeeds workPoor
LCP (Largest Contentful Paint)Loading — how fast the main content appears< 2.5s2.5–4s> 4s
INP (Interaction to Next Paint)Responsiveness — how fast the page responds to input< 200ms200–500ms> 500ms
CLS (Cumulative Layout Shift)Visual stability — how much the page jumps around< 0.10.1–0.25> 0.25

Measuring performance

# Next.js built-in analytics
# Add to next.config.js:
# experimental: { webVitals: true }

# Or use the web-vitals library
npm install web-vitals
// app/layout.tsx — report Web Vitals
export function reportWebVitals(metric: {
  id: string
  name: string
  value: number
}) {
  console.log(metric) // or send to analytics
}

Browser tools:

  • Lighthouse (Chrome DevTools > Lighthouse tab) — synthetic lab test
  • Chrome DevTools Performance tab — detailed trace of what's happening
  • PageSpeed Insights — real-world field data from Chrome users

Image Optimization

Images are typically the largest elements on a page and the biggest opportunity for improvement. Next.js has a built-in Image component that handles optimization automatically.

Use next/image

import Image from 'next/image'

// Local images — automatically optimized at build time
import heroImage from '@/public/hero.jpg'

export function Hero() {
  return (
    <Image
      src={heroImage}
      alt="Product dashboard showing analytics"
      priority          // LCP image — preload it
      placeholder="blur" // show blur while loading (local images only)
      className="rounded-lg"
    />
  )
}

// Remote images — optimized on-demand
export function Avatar({ user }: { user: User }) {
  return (
    <Image
      src={user.avatarUrl}
      alt={`${user.name}'s avatar`}
      width={40}
      height={40}
      className="rounded-full"
    />
  )
}

Key rules

  1. Always set priority on the LCP image — the hero image, main product photo, or whatever is the largest visible element above the fold. This tells Next.js to preload it.
  2. Always provide width and height (or use fill) — this prevents layout shift (CLS) because the browser knows the space to reserve before the image loads.
  3. Use fill for responsive containers:
<div className="relative aspect-video">
  <Image
    src="/banner.jpg"
    alt="Banner"
    fill
    className="object-cover rounded-lg"
    sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  />
</div>
  1. Provide sizes when using fill or responsive layouts — this tells the browser which size to download instead of always fetching the largest version:
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  1. Configure remote domains in next.config.js:
images: {
  remotePatterns: [
    { protocol: 'https', hostname: 'cdn.example.com' },
  ],
},

Code Splitting and Lazy Loading

Dynamic imports for heavy components

Don't load components the user might never see (modals, charts, editors) upfront:

import dynamic from 'next/dynamic'

// Heavy chart library — only load when rendered
const Chart = dynamic(() => import('@/components/chart'), {
  loading: () => <div className="h-64 bg-gray-100 animate-pulse rounded-lg" />,
})

// Modal — only load when opened
const SettingsModal = dynamic(() => import('@/components/settings-modal'))

export function Dashboard() {
  const [showSettings, setShowSettings] = useState(false)

  return (
    <>
      <Chart data={chartData} />
      <button onClick={() => setShowSettings(true)}>Settings</button>
      {showSettings && <SettingsModal onClose={() => setShowSettings(false)} />}
    </>
  )
}

Disable SSR for client-only components

Some libraries (maps, rich text editors, canvas-based tools) crash during server rendering:

const Map = dynamic(() => import('@/components/map'), {
  ssr: false,
  loading: () => <div className="h-96 bg-gray-100 animate-pulse rounded-lg" />,
})

Route-based splitting

Next.js automatically code-splits by route — each page only loads its own JavaScript. This works out of the box with the App Router.

For large shared components used on only some pages, move them out of the shared layout and import them only in the pages that need them.

Bundle Size

Analyze your bundle

# Install the analyzer
npm install -D @next/bundle-analyzer

# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer(nextConfig)

# Run analysis
ANALYZE=true npm run build

Common bundle bloaters and fixes

ProblemFix
Full lodash importedUse lodash/debounce instead of import { debounce } from 'lodash'
moment.js (330KB)Switch to date-fns (tree-shakeable) or native Intl APIs
Icon libraries (full set)Import individual icons: import { Search } from 'lucide-react'
Unused dependenciesRun npx depcheck to find them
Large date/number formattingUse native Intl.DateTimeFormat and Intl.NumberFormat

Tree shaking

ES modules are tree-shakeable — unused exports are removed at build time. Ensure:

  • Use import { specific } not import *
  • Check that libraries export ESM (look for "module" field in their package.json)
  • Avoid side effects in module scope

Rendering Performance

Avoid unnecessary re-renders

React re-renders a component when its state changes or its parent re-renders. Most re-renders are fine — React is fast. Only optimize when you measure an actual problem.

When to reach for memoization:

// useMemo — expensive computation
const sortedItems = useMemo(
  () => items.sort((a, b) => a.name.localeCompare(b.name)),
  [items]
)

// useCallback — stable function reference for child components
const handleSelect = useCallback((id: string) => {
  setSelectedId(id)
}, [])

// React.memo — skip re-render when props haven't changed
const ExpensiveList = memo(function ExpensiveList({ items }: { items: Item[] }) {
  return items.map(item => <ExpensiveRow key={item.id} item={item} />)
})

Don't memoize everything. Memoization has its own cost (memory, comparison checks). Use it when:

  • A component renders frequently with the same props
  • A computation is genuinely expensive (sorting/filtering large lists, complex calculations)
  • A callback is passed to many child components or used in a dependency array

Virtualize long lists

For lists with hundreds or thousands of items, render only what's visible:

npm install @tanstack/react-virtual
"use client"
import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef } from 'react'

function VirtualList({ items }: { items: Item[] }) {
  const parentRef = useRef<HTMLDivElement>(null)

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 64,
  })

  return (
    <div ref={parentRef} className="h-96 overflow-auto">
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div
            key={virtualRow.key}
            className="absolute w-full px-4 py-3 border-b"
            style={{
              height: virtualRow.size,
              transform: `translateY(${virtualRow.start}px)`,
            }}
          >
            {items[virtualRow.index].name}
          </div>
        ))}
      </div>
    </div>
  )
}

Loading Strategies

Streaming with Suspense

Break pages into independent sections that load in parallel. Fast sections appear immediately while slower ones stream in:

import { Suspense } from 'react'

export default function DashboardPage() {
  return (
    <div className="grid grid-cols-12 gap-6 p-6">
      {/* Fast — renders immediately */}
      <div className="col-span-12">
        <h1 className="text-2xl font-bold">Dashboard</h1>
      </div>

      {/* Each section loads independently */}
      <div className="col-span-8">
        <Suspense fallback={<ChartSkeleton />}>
          <RevenueChart />
        </Suspense>
      </div>

      <div className="col-span-4">
        <Suspense fallback={<ListSkeleton />}>
          <RecentOrders />
        </Suspense>
      </div>

      <div className="col-span-12">
        <Suspense fallback={<TableSkeleton />}>
          <ActivityTable />
        </Suspense>
      </div>
    </div>
  )
}

Prefetching

Next.js prefetches linked routes automatically when <Link> elements are visible in the viewport. For programmatic prefetching:

"use client"
import { useRouter } from 'next/navigation'

function ProductCard({ product }: { product: Product }) {
  const router = useRouter()

  return (
    <div
      onMouseEnter={() => router.prefetch(`/products/${product.id}`)}
      className="p-4 border rounded-lg hover:shadow-md transition-shadow cursor-pointer"
      onClick={() => router.push(`/products/${product.id}`)}
    >
      {product.name}
    </div>
  )
}

Fonts

Use next/font to avoid layout shift

// app/layout.tsx
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // show fallback font immediately, swap when loaded
  variable: '--font-inter',
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={inter.variable}>
      <body className="font-sans">{children}</body>
    </html>
  )
}
// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      fontFamily: {
        sans: ['var(--font-inter)', ...defaultTheme.fontFamily.sans],
      },
    },
  },
}

next/font self-hosts fonts with zero layout shift — no external requests to Google Fonts, no FOUT/FOIT.

Caching

Next.js caching layers

  1. Request Memoization — duplicate fetch() calls in the same render pass are automatically deduplicated
  2. Data Cache — fetch() responses are cached on the server across requests
  3. Full Route Cache — static routes are pre-rendered at build time
  4. Router Cache — visited routes are cached in the browser for instant back/forward navigation

Revalidation strategies

// Time-based — revalidate every hour
fetch(url, { next: { revalidate: 3600 } })

// On-demand — revalidate when data changes
// In a Server Action or API route:
import { revalidatePath, revalidateTag } from 'next/cache'

revalidatePath('/products')           // revalidate a specific path
revalidateTag('products')             // revalidate all fetches tagged 'products'

Static vs dynamic rendering

Pages are statically rendered by default. A page becomes dynamic when it uses:

  • cookies(), headers(), searchParams
  • fetch() with { cache: 'no-store' }
  • export const dynamic = 'force-dynamic'

Keep pages static whenever possible — they're served from CDN and are the fastest option.

Quick Wins Checklist

When a user asks "make my app faster," start with these high-impact items:

  1. Add priority to the LCP image
  2. Use next/image for all images with proper sizes
  3. Use next/font instead of <link> to Google Fonts
  4. Dynamic import heavy components (charts, editors, modals)
  5. Add loading.tsx skeletons for async pages
  6. Wrap independent data-fetching sections in <Suspense>
  7. Check bundle with ANALYZE=true npm run build — look for obvious bloat
  8. Use revalidate on fetch calls instead of no-store where possible
  9. Virtualize lists with 100+ items
  10. Ensure no useEffect fetching in client components — move to server components

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.