Performance Optimization: Laggy Scrolling on Ultimate Renderer Test Page

Resolved 💬 3 comments Opened Dec 8, 2025 by mhmdawwad22 Closed Feb 8, 2026

Problem Summary

The Ultimate Renderer test page at http://localhost:3000/ exhibits laggy scrolling performance due to inefficient rendering patterns and lack of optimization techniques.

Performance Metrics

Current State:

  • DOM Nodes: 822 (acceptable)
  • Buttons: 103 (excessive for single viewport)
  • Card Components: 125 (all rendered simultaneously)
  • Console Warnings: React prop validation errors, unrecognized DOM props
  • User Experience: Noticeable scroll lag, delayed interactions

Browser Analysis:

  • All 11 test sections render at once (no lazy loading)
  • Multiple component registrations on initial load
  • Background polling from DataSourceManager
  • No virtualization for long lists

Root Causes

1. No Virtualization

File: platform-ui/packages/ultimate-renderer/src/components/PageLayout.tsx:57-68

All components render synchronously regardless of viewport visibility. With 125+ components across 11 sections, this creates unnecessary DOM overhead.

const renderComponents = (comps: AppComponentDefinition[]) => {
  return comps.map((comp, idx) => (
    <ComponentRenderer
      key={comp.id || `component-${idx}`}
      component={comp}
      // ... all components render immediately
    />
  ));
};

2. Missing React.memo on ComponentRenderer

File: platform-ui/packages/ultimate-renderer/src/components/ComponentRenderer.tsx

ComponentRenderer lacks memoization, causing cascade re-renders when parent state changes. Every action context update re-renders all 125+ components.

3. Always-Mounted Registries

File: platform-ui/packages/ultimate-renderer/src/components/AppRenderer.tsx:723-745

DialogRegistry and DrawerRegistry mount all dialogs/drawers upfront, even when closed:

{app.dialogs && Object.keys(app.dialogs).length > 0 && (
  <DialogRegistry
    dialogs={app.dialogs}  // All dialogs mount immediately
    // ...
  />
)}

4. Inefficient Action Context

File: platform-ui/packages/ultimate-renderer/src/components/AppRenderer.tsx:549-568

Action context recreated on every render due to unstable dependencies, triggering full component tree re-renders.

5. Background Polling

File: platform-ui/apps/ultimate-renderer-test/src/app/page.tsx:75-85

DataSourceManager runs polling loops even when datasources aren't actively used:

useEffect(() => {
  const manager = new DataSourceManager(globalDataSources);
  // Polling starts immediately for all datasources
}, []);

6. No Code Splitting

File: platform-ui/apps/ultimate-renderer-test/src/config/pages/main/page.ts:26

All 11 test sections load synchronously instead of on-demand:

export const mainPage: PageDefinition = {
  components: allSections  // All sections loaded at once
};

7. Prop Validation Warnings

Console: Multiple validation errors causing performance overhead

[WARNING] [PropsValidator] Select prop validation errors: [Prop "options" should be a string, got array]
[ERROR] Warning: React does not recognize the prop on a DOM element

Optimization Plan

Phase 1: Critical Optimizations (Immediate Impact)

1.1 Implement Virtual Scrolling

Target: PageLayout.tsx

  • Add react-window or @tanstack/react-virtual for component virtualization
  • Only render visible components + buffer zone
  • Expected: 60-70% reduction in initial render time

Implementation:

import { useVirtualizer } from '@tanstack/react-virtual';

// Virtualize component list
const virtualizer = useVirtualizer({
  count: mainComponents.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 200, // Average component height
  overscan: 3 // Render 3 extra items above/below viewport
});
1.2 Memoize ComponentRenderer

Target: ComponentRenderer.tsx

  • Wrap ComponentRenderer with React.memo
  • Add custom comparison function for props
  • Expected: 40-50% reduction in unnecessary re-renders

Implementation:

export const ComponentRenderer = React.memo(({ component, ...props }) => {
  // Component logic
}, (prevProps, nextProps) => {
  return (
    prevProps.component.id === nextProps.component.id &&
    prevProps.mode === nextProps.mode &&
    JSON.stringify(prevProps.component.props) === JSON.stringify(nextProps.component.props)
  );
});
1.3 Lazy Mount Dialog/Drawer Registries

Target: AppRenderer.tsx:723-745

  • Only mount dialog/drawer when first opened
  • Clean up when all instances closed
  • Expected: Reduced initial bundle size and mount time

Implementation:

{openDialogs.size > 0 && app.dialogs && (
  <DialogRegistry /* Only mount when needed */ />
)}
1.4 Intersection Observer for Off-Screen Components

Target: New hook useIntersectionObserver.ts

  • Defer rendering of off-screen sections
  • Load on scroll into viewport
  • Expected: 50% faster initial paint

Implementation:

const LazyComponent = ({ component, threshold = 0.1 }) => {
  const [isVisible, setIsVisible] = useState(false);
  const ref = useIntersectionObserver(() => setIsVisible(true), { threshold });
  
  return <div ref={ref}>{isVisible ? <ComponentRenderer /> : <Placeholder />}</div>;
};

Phase 2: State & Context Optimization

2.1 Stabilize Action Context

Target: AppRenderer.tsx:549-568

  • Memoize dialog/drawer controllers with proper dependencies
  • Use useCallback for notify/navigate functions
  • Expected: Eliminate unnecessary child re-renders

Implementation:

const dialogController = useMemo(() => ({
  open: useCallback((id, props) => setOpenDialogs(prev => new Map(prev).set(id, props)), []),
  close: useCallback((id) => setOpenDialogs(prev => { /* ... */ }), []),
  // ...
}), []); // Stable reference
2.2 Granular State Subscriptions

Target: New hook useComponentState.ts

  • Subscribe to specific state slices, not entire tree
  • Prevent re-renders when unrelated state changes
  • Expected: 70% reduction in state-triggered re-renders

Implementation:

// Instead of subscribing to all state
const useComponentState = (stateKey: string) => {
  const stateManager = useStateManager();
  return useSyncExternalStore(
    (callback) => stateManager.subscribe(stateKey, callback),
    () => stateManager.get(stateKey)
  );
};
2.3 Debounce State Updates

Target: action-executor.ts

  • Debounce rapid state updates (e.g., input onChange)
  • Batch multiple updates into single render
  • Expected: Smoother UI during rapid interactions

Phase 3: Bundle & Loading Optimization

3.1 Code-Split Component Sections

Target: config/pages/main/sections/index.ts

  • Lazy load each test section
  • Load on-demand when section becomes visible
  • Expected: 40% reduction in initial bundle size

Implementation:

const sections = {
  card: lazy(() => import('./01-card-section')),
  button: lazy(() => import('./02-button-section')),
  // ...
};
3.2 Lazy Load Heavy Components

Target: Component registry

  • Defer loading of DataTable, Charts, Complex Forms
  • Load when first rendered
  • Expected: Faster app initialization
3.3 Bundle Size Analysis

Target: package.json

  • Add webpack-bundle-analyzer
  • Set size budgets for packages
  • Monitor regressions in CI/CD

Implementation:

{
  "scripts": {
    "analyze": "ANALYZE=true next build"
  }
}

Expected Improvements

Performance Gains

  • Initial Render Time: 60-70% reduction (from ~2s to ~600ms)
  • Scrolling FPS: Achieve smooth 60fps (currently ~30-40fps)
  • Memory Footprint: 40% reduction (fewer mounted components)
  • Time to Interactive: 50% faster
  • Bundle Size: 40% reduction in initial load

User Experience

  • ✅ Smooth, responsive scrolling
  • ✅ Instant UI interactions (button clicks, state updates)
  • ✅ Faster page loads
  • ✅ No janky animations
  • ✅ Better mobile performance

Acceptance Criteria

Must Have (Phase 1)

  • [ ] Scrolling achieves 60fps on test page (measure with Chrome DevTools)
  • [ ] Initial render completes in < 800ms (Lighthouse performance > 90)
  • [ ] Virtual scrolling implemented for component lists > 20 items
  • [ ] ComponentRenderer properly memoized with comparison function
  • [ ] Dialog/Drawer registries lazy mount only when opened
  • [ ] No React prop validation warnings in console

Should Have (Phase 2)

  • [ ] Action context stable (doesn't trigger re-renders)
  • [ ] State updates batched and debounced where appropriate
  • [ ] Granular state subscriptions prevent unnecessary re-renders
  • [ ] Memory usage reduced by 40% (measure with Chrome DevTools)

Nice to Have (Phase 3)

  • [ ] Component sections code-split and lazy loaded
  • [ ] Bundle size reduced by 40% (measure with next build)
  • [ ] Bundle size monitoring in CI/CD pipeline
  • [ ] Heavy components (tables, charts) lazy loaded

Testing Plan

  1. Performance Benchmarks (Before/After)
  • Lighthouse performance score
  • Chrome DevTools FPS during scrolling
  • Time to Interactive (TTI)
  • Memory profiling
  1. User Experience Testing
  • Scroll through entire test page smoothly
  • Rapid state updates (button clicks, form inputs)
  • Dialog/drawer opening performance
  • Mobile device testing (low-end devices)
  1. Regression Testing
  • All existing tests pass
  • No functional regressions
  • Visual regression testing (screenshots)

Implementation Notes

  • Start with Phase 1 (highest impact, lowest risk)
  • Measure performance after each optimization
  • Add performance budgets to prevent regressions
  • Document optimization patterns for future development
  • Consider extracting optimizations into shared hooks/HOCs

Related Files

  • platform-ui/packages/ultimate-renderer/src/components/PageLayout.tsx
  • platform-ui/packages/ultimate-renderer/src/components/ComponentRenderer.tsx
  • platform-ui/packages/ultimate-renderer/src/components/AppRenderer.tsx
  • platform-ui/apps/ultimate-renderer-test/src/app/page.tsx
  • platform-ui/apps/ultimate-renderer-test/src/config/pages/main/page.ts

---

Priority: High
Complexity: Medium
Estimated Effort: 3-5 days (phased implementation)
Labels: performance, optimization, rendering, ultimate-renderer

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗