Frontend Performance Optimization: From 35% Faster Loads to Better UX
Code splitting, TanStack Query caching, and render discipline that cut WealthHat dashboard load times by 35% — practical patterns for React apps at scale.
Performance is a product feature
Users do not care about your bundle analyzer screenshot. They care whether the portfolio dashboard feels instant when markets move.
While optimizing WealthHat's advisor portal, we targeted initial page load and interaction latency. The result: ~35% reduction in initial load time on core dashboards through code splitting, smarter data fetching, and render discipline.
At TheFoundersLab, similar work improved Lighthouse performance and accessibility scores by ~25% through strategic caching and component memoization. Performance wins compound across retention and support load.
Measure before you optimize
We started with real data:
- Web Vitals in production (LCP, INP, CLS)
- Route-level bundle analysis via
@next/bundle-analyzer - React Profiler on heaviest dashboard views
- Network waterfalls for aggregation endpoints
Guessing is expensive. One dashboard route was 420KB gzipped because it imported an entire charting library for a single sparkline.
Code splitting that matches user journeys
Not every route needs every dependency. We split by route and by interaction:
import dynamic from "next/dynamic";
const AllocationChart = dynamic(
() => import("@/features/portfolio/AllocationChart"),
{ loading: () => <ChartSkeleton />, ssr: false }
);
Heavy visualizations loaded only when the user navigated to analytics. Skeletons preserved layout stability (good CLS).
Rule: if a component is below the fold or behind a tab, defer it.
Data fetching: TanStack Query as a performance layer
Fetching is usually the bottleneck, not React reconciliation. TanStack Query gave us:
- Stale-while-revalidate — instant cached data, background refresh
- Request deduplication — multiple widgets, one network call
- Prefetching on hover for likely next routes
export function usePortfolioSummary(clientId: string) {
return useQuery({
queryKey: ["portfolio", clientId, "summary"],
queryFn: () => fetchPortfolioSummary(clientId),
staleTime: 60_000,
gcTime: 5 * 60_000,
});
}
This pattern also reduced data aggregation latency by ~40% perceived at the UI layer because advisors saw cached snapshots immediately while fresh data synced.
Render discipline in React 18+
Common fixes that mattered:
- Colocate state — do not lift filter state to a parent that re-renders the entire table
- Memoize expensive derived data with
useMemo, not every callback withuseCallback - Virtualize long lists — financial transaction histories can be thousands of rows
- Avoid anonymous inline objects passed to memoized children when profiling shows churn
We profiled before memoizing. Premature React.memo everywhere adds complexity without gains.
Asset and font strategy
- Serve images in WebP/AVIF with explicit dimensions
- Subset fonts and use
font-display: swap - Tree-shake icon libraries — import named icons, not entire packs
At Timbu, optimizing CSS and Tailwind usage reduced stylesheet bloat by ~20% while keeping design consistency.
Performance budgets in CI
We set soft budgets per route (JS KB, LCP threshold). Pull requests that regressed beyond tolerance required justification. This cultural shift mattered as much as the technical work.
Key takeaways
- Profile production first — optimize what users actually hit
- Split code by journey, not just by folder structure
- Treat TanStack Query (or similar) as a caching performance layer
- Pair bundle work with data-fetch strategy — they win together