SPFx React: Performance and Debugging

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 useCallback when passing to large lists.
  • Derive data with useMemo instead of recomputing every render.
  • Use React.memo for 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 $select and $top with 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 useRef and printing in useEffect.
  • Trace state changes with small middleware in reducers or contexts.
  • Use console.table for 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.lazy and Suspense.
  • Keep SPFx Context usage in containers; presentational components stay pure.
  • Send profiler metrics to Application Insights via a custom onRender.