Stepan Samko | Consulting [email protected]

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:

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:

This became the template for every large, high-frequency interactive view in the app.


Background & Context

The platform’s portfolio page displays:

This produced lists with:

Combined with Material-UI’s emotion-based CSS-in-JS, the UI suffered:

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

Measured Problems

Using Chrome Performance + React Profiler:

This was catastrophic for a UI expected to run 60 FPS.


Investigation & Profiling

I profiled using:

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:

B. Huge re-render cascades on every SSE update

Even if one row updated, React re-rendered:

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:


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:

After:

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:

After:

const classes = useStyles(); // static JSS

<div className={clsx(classes.value, isPositive && classes.positive)}>
  {value}
</div>

And dynamic layout parameters handled with:

Impact:

~70% drop in style recalculation time.


Eliminating Re-render Cascades

The goal:

Only re-render what MUST re-render.

Tools used:

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:

were too heavy to run synchronously.

Fixes:

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:


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

Qualitative


Why This Matters

Rendering performance is often underestimated in data-heavy apps.

In DeFi UIs, the combination of:

makes performance engineering critical.

Fixing rendering is almost always a 10× ROI operation:

This case study shows how much performance depends on understanding:

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.