fe-perf
1
总安装量
1
周安装量
#50849
全站排名
安装命令
npx skills add https://github.com/ingpdw/pdw-fe-dev-tool --skill fe-perf
Agent 安装分布
mcpjam
1
claude-code
1
replit
1
junie
1
zencoder
1
Skill 文档
FE Performance Optimization
$ARGUMENTSë¡ ì ë¬ë ëìì ì±ë¥ì ë¶ìíê³ ìµì ííë¤.
ë¶ì ì ì°¨
- ëì íì : íì¼ ëë “bundle” í¤ìëì ë°ë¼ ë¶ì ë²ì를 ê²°ì íë¤
- ì½ë ë¶ì: ì±ë¥ ìí°í¨í´ì ìë³íë¤
- ìµì í ì ì: ì°ì ììë³ë¡ ê°ì ìì ì ìíë¤
- ì ì©: ì¹ì¸ë ìµì í를 ì ì©íë¤
ì±ë¥ ìµì í ì¹´í ê³ ë¦¬
1. ë ëë§ ìµì í
ë¶íìí 리ë ëë§ ë°©ì§
// Bad â 매 ë ëë§ë¤ ì ê°ì²´ ìì±
function Parent() {
return <Child style={{ color: "red" }} data={[1, 2, 3]} />;
}
// Good â ìì ì 참조
const style = { color: "red" };
const data = [1, 2, 3];
function Parent() {
return <Child style={style} data={data} />;
}
React.memo ì ì í ì¬ì©
// ë¹ì©ì´ í° ì»´í¬ëí¸ìë§ ì ì©
const ExpensiveList = memo(function ExpensiveList({ items }: Props) {
return items.map((item) => <ExpensiveItem key={item.id} item={item} />);
});
useMemo / useCallback
// ë¹ì©ì´ í° ê³ì°ì useMemo
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// memoë ììì ì ë¬íë ì½ë°±ì useCallback
const handleSelect = useCallback((id: string) => {
setSelectedId(id);
}, []);
2. ì½ë ë¶í & Lazy Loading
ë¼ì°í¸ ê¸°ë° ì½ë ë¶í
Next.js App Routerë ìëì¼ë¡ ë¼ì°í¸ë³ ì½ë ë¶í ì ì ì©íë¤. ì¶ê° ìµì í:
// ë¬´ê±°ì´ ì»´í¬ëí¸ lazy loading
import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("@/components/HeavyChart"), {
loading: () => <Skeleton className="h-[400px]" />,
ssr: false, // í´ë¼ì´ì¸í¸ ì ì©
});
ì¡°ê±´ë¶ import
// Bad â íì ë¡ë
import { PDFViewer } from "react-pdf";
// Good â íìí ëë§ ë¡ë
const PDFViewer = dynamic(() =>
import("react-pdf").then((mod) => ({ default: mod.PDFViewer }))
);
3. ë²ë¤ ì¬ì´ì¦ ìµì í
í¸ë¦¬ìì´í¹ íì¸
// Bad â ì ì²´ ë¼ì´ë¸ë¬ë¦¬ import
import _ from "lodash";
_.debounce(fn, 300);
// Good â ê°ë³ í¨ì import
import debounce from "lodash/debounce";
debounce(fn, 300);
// Best â ìì ëì ì¬ì©
import { useDebouncedCallback } from "use-debounce";
ë²ë¤ ë¶ì
# Vite ë²ë¤ ë¶ì
npx vite-bundle-visualizer
# Next.js ë²ë¤ ë¶ì
ANALYZE=true next build # @next/bundle-analyzer íì
ë¬´ê±°ì´ ìì¡´ì± êµì²´ ê°ì´ë
| ë¬´ê±°ì´ ë¼ì´ë¸ë¬ë¦¬ | ê°ë²¼ì´ ëì |
|---|---|
moment (300KB) |
date-fns (í¸ë¦¬ìì´í¹) ëë dayjs (2KB) |
lodash (70KB) |
ê°ë³ import ëë ES ë¤ì´í°ë¸ ë©ìë |
uuid (10KB) |
crypto.randomUUID() (ë¤ì´í°ë¸) |
axios (30KB) |
fetch (ë¤ì´í°ë¸) |
classnames (2KB) |
clsx (1KB) ëë cn() |
4. ì´ë¯¸ì§ ìµì í
// Next.js Image ì»´í¬ëí¸ ì¬ì©
import Image from "next/image";
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // LCP ì´ë¯¸ì§ì priority
placeholder="blur" // ë¸ë¬ íë ì´ì¤íë
sizes="(max-width: 768px) 100vw, 50vw" // ë°ìí í¬ê¸°
/>
// ìì´ì½ â SVG inline ëë sprite
import { LucideIcon } from "lucide-react"; // í¸ë¦¬ìì´í¹ ì§ì
5. Server Component ìµì í
// Server Componentìì ë°ì´í° ê°ì ¸ì¤ê¸° (ì ë¡ ë²ë¤)
async function ProductList() {
const products = await db.product.findMany();
return products.map((p) => <ProductCard key={p.id} product={p} />);
}
// ë³ë ¬ ë°ì´í° íì¹
async function Dashboard() {
const [users, orders, stats] = await Promise.all([
getUsers(),
getOrders(),
getStats(),
]);
// ...
}
6. 리ì¤í¸ ê°ìí
// 100+ ìì´í
리ì¤í¸ì ê°ìí ì ì©
import { useVirtualizer } from "@tanstack/react-virtual";
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
<div ref={parentRef} className="h-[500px] overflow-auto">
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
height: virtualItem.size,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<ListItem item={items[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}
7. ìºì± ì ëµ
// React Query ìºì±
const { data } = useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
staleTime: 5 * 60 * 1000, // 5ë¶ê° fresh
gcTime: 30 * 60 * 1000, // 30ë¶ê° ìºì ì ì§
});
// Next.js fetch ìºì±
const data = await fetch("/api/data", {
next: { revalidate: 3600 }, // 1ìê° ISR
});
// Next.js unstable_cache
import { unstable_cache } from "next/cache";
const getCachedData = unstable_cache(
async () => db.query(),
["cache-key"],
{ revalidate: 3600 }
);
8. Web Vitals ì²´í¬í¬ì¸í¸
| ì§í | 목í | ìµì í í¬ì¸í¸ |
|---|---|---|
| LCP (Largest Contentful Paint) | < 2.5s | íì´ë¡ ì´ë¯¸ì§ priority, í°í¸ preload |
| FID/INP (Interaction to Next Paint) | < 200ms | ë¬´ê±°ì´ ì°ì° ë¶ë¦¬, useDeferredValue |
| CLS (Cumulative Layout Shift) | < 0.1 | ì´ë¯¸ì§ width/height ì§ì , ì¤ì¼ë í¤ |
| TTFB (Time to First Byte) | < 800ms | SSG/ISR, ìì§ ìºì± |
리í¬í¸ íì
# Performance Audit: [ëì]
## ìì½
- 주ì ì´ì: Nê°
- ìì ê°ì í¨ê³¼: [ë²ë¤ -NKB, ë ëë§ -Nms ë±]
## High Impact (ì°ì ì ì©)
### [P1] ì´ì ì 목
- **ìí¥**: [ë²ë¤ ì¬ì´ì¦ / ë ëë§ / ë¡ë© ìë]
- **íì¬**: ì¤ëª
- **ê°ì ì**: ì½ë
## Medium Impact
...
## Low Impact
...
ì¤í ê·ì¹
- ì¸ìê° ìì¼ë©´ ì¬ì©ììê² ë¶ì ëìì ì§ë¬¸íë¤
- “bundle” ì¸ì ì
package.json, build ì¤ì ì ë¶ìíë¤ - ê³¼ëí ìµì í를 ê²½ê³íë¤ (측ì ê°ë¥í ì´ìì ì§ì¤)
- ìµì í ì í ì°¨ì´ë¥¼ 구체ì ìì¹ë¡ ì¤ëª íë¤