Rendering & React Performance Deep Dive for a High-Scale DeFi Portfolio UI
How I solved severe rendering stalls, re-render storms, CSS-in-JS bottlenecks, and list virtualization challenges in a large, real-time portfolio interface.
Summary (TL;DR)
The DeFi platform’s portfolio UI struggled under real workloads:
- 100s–1000s of positions
- nested components for tokens, protocols, and chains
- frequent SSE updates
- expensive dynamic Material-UI CSS-in-JS
- heavy computations per row
Users experienced visible frame drops, scroll lag, and UI freezes. React profiler showed 20–40 ms render spikes, cascaded tree re-renders, and layout trashing caused by per-render dynamic styling.
I redesigned the rendering architecture:
- converted expensive CSS-in-JS to stable classNames
- isolated heavy components with memoization
- introduced list virtualization for the portfolio view
- deferred non-critical calculations
- eliminated cascading re-renders
- reduced style recalculation by ~70%
- sustained a smooth 60 FPS experience even with large portfolios
This became the template for every large, high-frequency interactive view in the app.
Background & Context
The platform’s portfolio page displays:
- many wallets
- each wallet → many protocols
- each protocol → positions across multiple chains
- nested LP, lending, borrowing, staking, and pool positions
- dynamic metrics (APR, PnL, liquidity values) updated from SSE
- icons, badges, charts, expandable rows
This produced lists with:
- hundreds to thousands of rows
- deep nested structures
- heavy presentational components
- high update rate (every few seconds from SSE)
Combined with Material-UI’s emotion-based CSS-in-JS, the UI suffered:
- stalling on every update
- style recalculations across the entire tree
- render storms when list items updated
- layout shifts triggered by dynamic inline styles
- scroll jank due to React repeatedly re-rendering rows offscreen
The core issue wasn’t “React being slow”— it was the interaction between rendering workload and CSS-in-JS at scale.
Performance Symptoms
Real UX Issues
- noticeable freezes during active updates
- 100% CPU spikes when expanding/collapsing items
- scroll jank when the dataset was large
- chart updates causing entire rows to re-render
- theme & palette values causing deep style regeneration
Measured Problems
Using Chrome Performance + React Profiler:
- 20–40 ms render spikes on simple interactions
- style recalculation dominating the main thread
- large “purple bars” in DevTools from emotion generating new classnames
- massive wasted renders across nested trees (token → protocol → wallet)
- render cascades triggered by props churn in list rows
This was catastrophic for a UI expected to run 60 FPS.
Investigation & Profiling
I profiled using:
- React Profiler (Commit / Render Flamecharts)
- Browser Performance Panel
- CSS Style Recalculation flamegraphs
- Interaction-to-Next-Paint timeline analysis
Findings
A. CSS-in-JS style generation was the top offender
Material-UI’s emotion engine generated new classnames on every render, because dynamic values (colors, spacing, sizes) were passed via props.
This triggered:
- style parsing
- rule insertion
- layout recalculation
- paint invalidation
B. Huge re-render cascades on every SSE update
Even if one row updated, React re-rendered:
- parent
- siblings
- nested children
- memoized components that received unstable props
Real flamechart example pattern:
Row -> TokenIcon -> Tooltip -> ChainBadge -> Chart -> APRBadge -> ...
C. Large lists rendered all items at once
Even items off-screen rendered and re-rendered every update.
D. Inline dynamic styles caused huge layout shifts
Example:
style={{ width: dynamicValue }} inside rows.
Layout thrashing appeared as:
- repeated forced reflows
- expensive layout chains in DevTools
High-Level Solution
I reorganized rendering around four pillars:
1. Virtualize large interactive list
Use react-virtualized (or similar) to render only visible window.
2. Make styles stable
Reduce CSS-in-JS generation to zero during updates.
3. Make component boundaries predictable
React.memo, useMemo, stable selector layers.
4. Defer expensive work off the render path
Batch updates, debounce, and schedule slow transforms.
Architecture Overview
Mermaid diagram showing the rendering pipeline:
flowchart TD
DATA[SSE Stream: Frequent Updates] --> SELECTORS[Memoized Selectors]
SELECTORS --> VIRT[Virtualized List Window]
VIRT --> ROW[Memoized Row Components]
ROW --> STYLE[Static Classes + Minimal CSS-in-JS]
ROW --> DEFER[Deferred/Idle Expensive Work]
DEFER --> ROW
STYLE --> FRAME[Stable Layout\nNo Reflows]
ROW --> FRAME
Detailed Design & Key Fixes
List Virtualization: Rendering Only What’s Visible
Before:
- All ~500–1000 rows rendered
- Every update re-rendered the entire list
- Many components were off-screen but still expensive
After:
- Only ~12–20 rows mounted at any time
- Off-screen rows destroyed or recycled
- Updates scoped to visible window
- Virtualized scroll container
Example (high-level):
<VirtualizedList
rowCount={positions.length}
rowHeight={ROW_HEIGHT}
rowRenderer={RowRenderer}
/>
Virtualization alone cut commit times from:
60–120 ms → 5–12 ms
Stopping Style Recalculation Hell (CSS-in-JS Fix)
The platform used Material-UI’s emotion-based CSS-in-JS.
Problem:
Every dynamic prop → new classname → full style re-run.
Fix: Shift dynamic values into static class composition
Example before:
<Box sx={{ color: isPositive ? green[500] : red[500], padding: dynamicPadding }}>
{value}
</Box>
This caused:
- new class per render
- style parsing
- layout/paint cycle
After:
const classes = useStyles(); // static JSS
<div className={clsx(classes.value, isPositive && classes.positive)}>
{value}
</div>
And dynamic layout parameters handled with:
- CSS variables updated minimally
- or
- tiny inline styles that do NOT trigger reflows
Impact:
~70% drop in style recalculation time.
Eliminating Re-render Cascades
The goal:
Only re-render what MUST re-render.
Tools used:
React.memofor row componentsuseMemofor data transformationsuseCallbackfor stable handlers- stable props from selector layer
Example:
const Row = React.memo(function Row({ position }) {
return <RowView position={position} />;
});
Preventing unstable prop churn:
I introduced a selector layer:
const selectPosition = createSelector(
state => state.positions.byId,
(_, id) => id,
(positions, id) => positions[id]
);
Now rows receive deeply stable objects, not new ones every render.
Re-render count dropped from ~500 to ~12 during updates.
Deferring Expensive Calculations
Some transformations:
- APR formatting
- PnL calculation
- chart smoothing
- historical value derivation
- token metadata merges
were too heavy to run synchronously.
Fixes:
useDeferredValuefor smoothing visual updatesrequestIdleCallbackfor non-critical transforms- debounce SSE bursts before updating view
Example:
const deferredPositions = useDeferredValue(positions);
Reducing Layout Thrashing
Inline dynamic styles removed. Dimensions stabilized.
Example improvement:
Before:
height: dynamic based on content
After:
height: fixed row height
This turned:
- unpredictable layout → stable layout
- repeated reflows → minimal layout work
Representative Pseudocode
Row Component (Optimized)
const PositionRow = React.memo(function PositionRow({ id }) {
const position = usePositionSelector(id); // memoized selector
const apr = useMemo(() => formatAPR(position.apr), [position.apr]);
const pnl = useMemo(() => computePnL(position), [position.value, position.cost]);
return (
<div className={clsx(classes.row, position.isPositive && classes.positive)}>
<TokenIcon token={position.token} />
<span>{position.valueUSD}</span>
<span>{apr}</span>
<span>{pnl}</span>
</div>
);
});
Results
Quantitative
- ~70% reduction in style recalculation time
- ~60–80% reduction in re-render count per update
- Scroll FPS: 20–40 → solid 60 FPS
- Render spikes: 20–40 ms → 4–8 ms
- CPU usage greatly reduced
Qualitative
- List scrolling became smooth
- No UI freezes during updates
- Portfolio feels “instant” under high volume
- Component tree much easier to reason about
- Architecture reusable across other dashboards
Why This Matters
Rendering performance is often underestimated in data-heavy apps.
In DeFi UIs, the combination of:
- large lists
- nested components
- frequent updates
- dynamic visualizations
makes performance engineering critical.
Fixing rendering is almost always a 10× ROI operation:
- zero backend changes
- no feature changes
- purely architectural improvements
- immediate user-visible impact
This case study shows how much performance depends on understanding:
- React reconciliation
- browser rendering pipeline
- CSS-in-JS internals
- layout cost
- virtualization patterns
and designing a system meant to handle real data scale.
Final Thoughts
The biggest lesson:
Rendering performance is architecture, not micro-tweaks.
Shifting from dynamic styling and unbounded rendering → to stable classes, memoized boundaries, and virtualization completely transformed the experience.
This project strengthened the platform’s UI foundation and created a reusable pattern for every complex view across the product.