Modern frontend patterns for React Server Components, performance optimization, and Core Web Vitals
52
57%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./skills/frontend-excellence/SKILL.mdServer Components run on the server and send rendered HTML to the client. They can directly access databases, filesystems, and internal APIs without exposing them to the browser.
// app/products/page.tsx (Server Component by default)
async function ProductsPage() {
const products = await db.query("SELECT * FROM products WHERE active = true");
return (
<main>
<h1>Products</h1>
<ProductList products={products} />
<AddToCartButton /> {/* Client Component */}
</main>
);
}Rules:
useState, useEffect, or browser APIs'use client' at the top of the file'use client' boundary as deep in the tree as possibleimport { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
<Header /> {/* renders immediately */}
<Suspense fallback={<ChartSkeleton />}>
<AnalyticsChart /> {/* streams when ready */}
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders /> {/* streams independently */}
</Suspense>
</div>
);
}Each Suspense boundary streams independently. Place boundaries around data-fetching components to avoid blocking the entire page.
import dynamic from 'next/dynamic';
const HeavyEditor = dynamic(() => import('@/components/Editor'), {
loading: () => <EditorSkeleton />,
ssr: false,
});
const AdminPanel = dynamic(() => import('@/components/AdminPanel'));Split on:
// next.config.js
module.exports = {
experimental: {
optimizePackageImports: ['lucide-react', '@heroicons/react', 'lodash-es'],
},
};Checklist:
npx next build and review the output size per route@next/bundle-analyzer to identify large dependenciesmoment with date-fns or dayjs (save ~200KB)import { debounce } from 'lodash-es/debounce'import { Search } from 'lucide-react'| Metric | Good | Needs Work | Poor |
|---|---|---|---|
| LCP (Largest Contentful Paint) | <2.5s | 2.5-4.0s | >4.0s |
| INP (Interaction to Next Paint) | <200ms | 200-500ms | >500ms |
| CLS (Cumulative Layout Shift) | <0.1 | 0.1-0.25 | >0.25 |
<link rel="preload" as="image" href="..." />priority prop on above-the-fold <Image> componentswidth/height on images to prevent layout shiftsimport Image from 'next/image';
<Image
src="/hero.jpg"
alt="Descriptive alt text"
width={1200}
height={630}
priority // preload for LCP images
sizes="(max-width: 768px) 100vw, 50vw"
placeholder="blur"
blurDataURL={base64} // inline tiny placeholder
/>next/image or equivalent (automatic WebP/AVIF, responsive srcset)sizes attribute to avoid downloading oversized imagesplaceholder="blur" with a base64 data URL for perceived performance// app/layout.tsx
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap', // show fallback font immediately
preload: true,
variable: '--font-inter',
});
export default function RootLayout({ children }) {
return (
<html className={inter.variable}>
<body>{children}</body>
</html>
);
}next/font for zero-CLS font loading with automatic subsettingdisplay: 'swap' to avoid invisible text during loadwidth and height on images and videosaspect-ratio CSS for responsive media containersmin-heightcontain: layout for components that change sizeimport { onCLS, onINP, onLCP } from 'web-vitals';
onCLS(console.log);
onINP(console.log);
onLCP(console.log);Measure real user metrics (RUM), not just lab scores. Vercel Analytics and Google Search Console provide field data.
ebdf1d5
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.