Performance Optimization: Laggy Scrolling on Ultimate Renderer Test Page
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-windowor@tanstack/react-virtualfor 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
- Performance Benchmarks (Before/After)
- Lighthouse performance score
- Chrome DevTools FPS during scrolling
- Time to Interactive (TTI)
- Memory profiling
- 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)
- 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.tsxplatform-ui/packages/ultimate-renderer/src/components/ComponentRenderer.tsxplatform-ui/packages/ultimate-renderer/src/components/AppRenderer.tsxplatform-ui/apps/ultimate-renderer-test/src/app/page.tsxplatform-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
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗