skills/react-performance/SKILL.md
React 效能最佳化模式。用於 memo/useMemo/useCallback 正確使用、虛擬化列表、Code Splitting、Bundle 優化、Re-render 診斷。
npx skillsauth add vincent119/ai-rules-kit react-performanceInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
僅在子元件 props 穩定但父元件頻繁 re-render 時使用:
import { memo } from 'react';
interface ExpensiveListProps {
items: Item[];
onSelect: (id: string) => void;
}
// 適合 memo:接收大量資料,父元件因其他狀態頻繁更新
export const ExpensiveList = memo(function ExpensiveList({
items,
onSelect,
}: ExpensiveListProps) {
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
// 搭配自訂比較函式(僅在必要時)
export const UserAvatar = memo(
function UserAvatar({ user }: { user: User }) {
return <img src={user.avatarUrl} alt={user.name} />;
},
(prev, next) => prev.user.id === next.user.id,
);
import { useMemo, useCallback, useState } from 'react';
interface DashboardProps {
transactions: Transaction[];
filter: string;
}
export function Dashboard({ transactions, filter }: DashboardProps) {
// useMemo:昂貴的過濾與計算
const filtered = useMemo(
() => transactions.filter((t) => t.category === filter),
[transactions, filter],
);
const total = useMemo(
() => filtered.reduce((sum, t) => sum + t.amount, 0),
[filtered],
);
// useCallback:傳給 memo 子元件的 callback
const handleSelect = useCallback((id: string) => {
console.log('selected:', id);
}, []);
return (
<div>
<p>Total: {total}</p>
<ExpensiveList items={filtered} onSelect={handleSelect} />
</div>
);
}
何時不需要:
// 不需要 useMemo:簡單計算
const fullName = `${firstName} ${lastName}`; // 直接計算即可
// 不需要 useCallback:沒有傳給 memo 子元件
const handleClick = () => setCount((c) => c + 1); // 直接定義即可
// 問題:每次 render 建立新物件/陣列
function Parent() {
return <Child style={{ color: 'red' }} items={[1, 2, 3]} />;
}
// 解法 1:提取為模組層級常數
const STYLE = { color: 'red' } as const;
const ITEMS = [1, 2, 3] as const;
function Parent() {
return <Child style={STYLE} items={ITEMS} />;
}
// 解法 2:狀態拆分,避免無關更新
// 錯誤:整個物件作為一個 state
const [state, setState] = useState({ name: '', count: 0 });
// 正確:拆分為獨立 state
const [name, setName] = useState('');
const [count, setCount] = useState(0);
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
interface VirtualListProps {
items: string[];
}
export function VirtualList({ items }: VirtualListProps) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
overscan: 5,
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
transform: `translateY(${virtualItem.start}px)`,
height: `${virtualItem.size}px`,
width: '100%',
}}
>
{items[virtualItem.index]}
</div>
))}
</div>
</div>
);
}
import { lazy, Suspense } from 'react';
// 路由層級分割
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
export function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// 元件層級分割(大型元件按需載入)
const HeavyChart = lazy(() => import('./components/HeavyChart'));
export function Report({ showChart }: { showChart: boolean }) {
return (
<div>
<h1>Report</h1>
{showChart && (
<Suspense fallback={<div>Loading chart...</div>}>
<HeavyChart />
</Suspense>
)}
</div>
);
}
import { useRef, useEffect } from 'react';
// 開發環境用:追蹤哪個 prop 變更導致 re-render
export function useWhyDidYouRender(name: string, props: Record<string, unknown>) {
const prevProps = useRef(props);
useEffect(() => {
const changes: Record<string, { from: unknown; to: unknown }> = {};
for (const key of Object.keys(props)) {
if (prevProps.current[key] !== props[key]) {
changes[key] = { from: prevProps.current[key], to: props[key] };
}
}
if (Object.keys(changes).length > 0) {
console.log(`[${name}] re-render caused by:`, changes);
}
prevProps.current = props;
});
}
React.memo:子元件 props 穩定但父元件頻繁更新時使用useMemo:計算成本高(O(n) 以上的迴圈、排序、過濾)時使用useCallback:callback 傳給 memo 子元件時使用tools
基於 SLA/SLO 量化評估事故影響的計算模型與業務影響矩陣。適用於「SLA 影響」、「SLO 違反」、「影響評估」、「營收損失估算」、「Error Budget」、「可用性計算」、「事故成本評估」等量化事故業務影響的任務。強化 impact-assessor 的評估能力。注意:事故原因分析與改善規劃不在此技能範圍內。
research
根因分析(RCA)方法論詳細指南。提供 5 Whys、Fishbone 圖、Fault Tree Analysis、變更分析等結構化 RCA 技術,以及認知偏誤防範清單。適用於「根因分析」、「RCA」、「5 Whys」、「魚骨圖」、「Fault Tree」、「原因分析方法論」、「變更分析」等事故原因分析任務。強化 root-cause-investigator 的分析能力。注意:時間軸重建與改善規劃不在此技能範圍內。
testing
事故事後分析(Postmortem)完整流程。協調 7 個執行階段:資訊收集 → 時間軸重建 → 根因分析 → 影響評估 → 改善規劃 → 報告審查 → 整合報告,最終產出完整的 Postmortem 報告。適用於「寫事故報告」、「post-incident 分析」、「RCA 報告」、「事故時間軸整理」、「建立改善措施」等請求。注意:即時 Incident Response(on-call)、監控系統設定、告警配置不在此技能範圍內。
content-media
投影片版面模式庫。提供 20 種投影片類型的最佳版面配置、格線系統、色彩與字型設計 Token。適用於「投影片版面」、「Slide Layout」、「設計系統」、「格線」、「字型」、「色彩規範」等投影片視覺設計任務。強化 visual-designer 的設計能力。注意:PPT/Keynote 檔案直接輸出不在此技能範圍內。