SPFx React: Performance and Debugging
Practical techniques to keep SPFx React solutions snappy and debuggable.
Tooling overview
- React DevTools Profiler: measure commit times and rendering hotspots.
- Chrome Performance panel: CPU profiles, JS flame charts, layout/paint.
- Network panel: cache headers, request waterfalls, long TTFB.
- Lighthouse & Web Vitals: quick UX signals (CLS, LCP, FID).
Measure with React Profiler
Wrap key trees with Profiler to capture render durations.
import React, { Profiler } from 'react';
function onRender(
id: string,
phase: 'mount' | 'update',
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number
) {
// Send to App Insights or console
console.info(`[${id}] ${phase} took ${actualDuration.toFixed(1)}ms`);
}
export function ListWithProfiler() {
return (
<Profiler id="UserList" onRender={onRender}>
<UserList />
</Profiler>
);
}
Avoid unnecessary re-renders
- Stabilize handlers with
useCallbackwhen passing to large lists. - Derive data with
useMemoinstead of recomputing every render. - Use
React.memofor pure presentational rows. - Keep prop shapes stable; avoid inline objects/arrays in JSX.
type RowProps = { user: { id: string; name: string }; onSelect: (id: string) => void };
export const Row = React.memo(function Row({ user, onSelect }: RowProps) {
const handleClick = React.useCallback(() => onSelect(user.id), [onSelect, user.id]);
return <button onClick={handleClick}>{user.name}</button>;
}, (prev, next) => prev.user.id === next.user.id && prev.onSelect === next.onSelect);
Virtualize large lists
Render only visible items with Fluent UI’s List or DetailsList to avoid DOM bloat.
import { List } from '@fluentui/react';
export function VirtualizedList({ items }: { items: any[] }) {
const onRenderCell = (item?: any, index?: number): JSX.Element | null => {
if (!item) return null;
return <Row user={item} onSelect={() => { /* ... */ }} />;
};
return <List items={items} onRenderCell={onRenderCell} />;
}
Debounce and throttle expensive work
Debounce search queries and throttle scroll/resize handlers.
function useDebounced(value: string, ms: number) {
const [debounced, setDebounced] = React.useState(value);
React.useEffect(() => {
const t = setTimeout(() => setDebounced(value), ms);
return () => clearTimeout(t);
}, [value, ms]);
return debounced;
}
export function SearchBox({ onQuery }: { onQuery: (q: string) => void }) {
const [q, setQ] = React.useState('');
const dq = useDebounced(q, 300);
React.useEffect(() => onQuery(dq), [dq, onQuery]);
return <input value={q} onChange={e => setQ(e.target.value)} placeholder="Search" />;
}
Cache and batch network calls
- Cache list responses (ETag/If-None-Match) and reuse between property changes.
- Prefer
$selectand$topwith SharePoint, and batch Graph requests when possible. - Abort in-flight requests on unmount or rapid prop changes.
Debugging patterns
- Log render counts by incrementing a
useRefand printing inuseEffect. - Trace state changes with small middleware in reducers or contexts.
- Use
console.tablefor array diffs before/after memoization.
export function RenderCounter() {
const renders = React.useRef(0);
renders.current++;
React.useEffect(() => {
console.log(`Renders: ${renders.current}`);
});
return null;
}
SPFx‑specific notes
- Property pane changes can re-render frequently; debounce heavy effects.
- Lazy‑load heavy UI (charts/editors) with
React.lazyandSuspense. - Keep SPFx
Contextusage in containers; presentational components stay pure. - Send profiler metrics to Application Insights via a custom
onRender.