Virtual Scrolling Implementation Decision

Architectural Analysis for ng-vui-grid Component

Overview

This document explains why we chose to implement manual virtual scrolling instead of using the existing VirtualScrollDirective in the ng-vui-grid component. Updated for Angular 20+ standalone architecture.

Architecture Update

With the migration to standalone components and signal-based state management, our virtual scrolling implementation has been further optimized for:

  • Better change detection performance
  • Reduced memory footprint
  • Improved TypeScript type safety
  • Enhanced compatibility with Angular 20+ features

The VirtualScrollDirective

What It Is

The VirtualScrollDirective is a custom directive located at:

/lib/common-lib/src/ng-vui-grid/directives/virtual-scroll.directive.ts

How It's Designed to Work

@Directive({
  selector: '[ngVirtualScroll]',
  standalone: true
})
export class VirtualScrollDirective {
  @Input() items: any[] = [];
  @Input() itemHeight: number | ((item: any, index: number) => number) = 40;
  @Input() bufferSize = 5;
  @Input() enableSmoothScrolling = true;
  @Output() viewportChange = new EventEmitter<VirtualScrollViewport>();
}

Intended Usage

<div ngVirtualScroll
     [items]="dataArray"
     [itemHeight]="48"
     [bufferSize]="5"
     (viewportChange)="onViewportChange($event)">
</div>

Why We Didn't Use VirtualScrollDirective

1. Grid-Specific Requirements

Problem

The directive was designed for generic virtualization, but grids have unique needs.

Our Requirements

  • Column-based rendering - Grids render rows with multiple columns
  • Header synchronization - Headers must stay aligned with data columns
  • Row selection - Complex selection state management
  • Cell templates - Custom cell rendering with templates
  • Horizontal scrolling - Both vertical AND horizontal virtualization
VirtualScrollDirective Limitation:
// The directive assumes simple item rendering
// It doesn't handle complex grid structures like:
<div class="grid-row">
  <div class="grid-cell" *ngFor="let column of columns">
    <!-- Complex cell content with templates -->
  </div>
</div>

2. Integration with Data Streaming

Problem

The directive doesn't integrate with our data streaming service.

Our Streaming Integration

onVirtualScroll(event: Event): void {
  const target = event.target as HTMLElement;
  const scrollTop = target.scrollTop;
  const rowHeight = this.getRowHeight();

  // Calculate visible range
  const startIndex = Math.floor(scrollTop / rowHeight);
  const endIndex = Math.ceil((scrollTop + viewportHeight) / rowHeight);

  // 🎯 Critical: Update streaming service
  if (this._isStreamingEnabled()) {
    this.streamingService.updateViewport(startIndex, endIndex);
  }
}
VirtualScrollDirective Missing:
  • No hooks for streaming integration
  • Can't trigger chunk loading based on scroll position
  • No coordination with server-side operations

3. Performance Control

Problem

The directive abstracts away performance-critical details.

What We Need to Control

  • Exact DOM element count - Limit to 15-20 elements max for 10M records
  • Transform calculations - Hardware acceleration with translate3d
  • Render cycle timing - When to trigger re-renders
  • Memory management - Coordinate with chunk cleanup
getVirtualVisibleItems(): VirtualScrollItem[] {
  const scrollTop = this._virtualScrollTop();
  const viewportHeight = this._virtualViewportHeight();
  const rowHeight = this.getRowHeight();
  const bufferSize = this.getVirtualBufferSize();

  // 🎯 Precise control over what gets rendered
  const startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - bufferSize);
  const endIndex = Math.min(
    this._rowData().length - 1,
    Math.ceil((scrollTop + viewportHeight) / rowHeight) + bufferSize
  );

  // Return only exactly what we need
  return this.createVirtualItems(startIndex, endIndex);
}

4. Template Flexibility

Problem

The directive controls the rendering internally.

Our Grid Template Needs

<!-- ✅ Our flexible approach -->
@for (virtualItem of getVirtualVisibleItems(); track virtualItem.index) {
  <div class="grid-row" [style.transform]="getTransform(virtualItem.index)">

    <!-- Selection column -->
    <div class="grid-cell" *ngIf="enableSelection">
      <input type="checkbox" [checked]="isRowSelected(virtualItem.data)">
    </div>

    <!-- Dynamic columns -->
    <div class="grid-cell"
         *ngFor="let column of visibleColumns(); trackBy: trackByField">

      <!-- Custom templates -->
      @if (column.cellTemplate) {
        <ng-container [ngTemplateOutlet]="column.cellTemplate"
                      [ngTemplateOutletContext]="getContext(virtualItem, column)">
        </ng-container>
      } @else {
        {{ formatCellValue(virtualItem.data, column) }}
      }
    </div>
  </div>
}
VirtualScrollDirective Limitation:
<!-- ❌ Limited template control -->
<div ngVirtualScroll [items]="data">
  <!-- Directive controls rendering - can't customize grid structure -->
</div>

5. Server-Side Integration

Problem

The directive doesn't understand server-side operations.

Our Integration

// Manual implementation allows deep integration
private setupVirtualScrolling(): void {
  if (this._isVirtualScrollEnabled()) {
    // 🎯 Coordinate with multiple services

    // 1. Server-side operations
    if (this._isServerSideEnabled()) {
      this.serverSideService.configure({...});
    }

    // 2. Data streaming
    if (this._isStreamingEnabled()) {
      this.streamingService.configure({...});
    }

    // 3. Memory management
    this.setupMemoryMonitoring();
  }
}

6. Debugging and Maintenance

Problem

Directive abstracts away implementation details.

Benefits of Manual Implementation

  • Full visibility into render cycles
  • Performance profiling at each step
  • Custom optimizations for specific use cases
  • Easier debugging when issues occur
// 🎯 Detailed performance monitoring
getVirtualScrollStats() {
  return {
    visibleItems: this.getVirtualVisibleItems().length,
    scrollTop: this._virtualScrollTop(),
    renderTime: this.lastRenderTime,
    memoryUsage: this.getMemoryUsage(),
    fps: this.getCurrentFPS()
  };
}

Feature Comparison

Aspect VirtualScrollDirective Manual Implementation
Ease of Use ✅ Simple to implement ⚠️ More complex setup
Grid Integration ❌ Generic, limited ✅ Perfect for grids
Performance Control ❌ Abstracted away ✅ Full control
Streaming Integration ❌ No support ✅ Native integration
Template Flexibility ❌ Limited ✅ Complete freedom
Debugging ❌ Black box ✅ Full visibility
Memory Management ❌ Basic ✅ Advanced coordination
Server-Side Ops ❌ No integration ✅ Seamless integration
Enterprise Features ❌ Limited ✅ All features supported

Alternative Approach: Enhanced Directive

If we wanted to use the directive approach, we would need to significantly enhance it:

@Directive({
  selector: '[ngGridVirtualScroll]', // Grid-specific
  standalone: true
})
export class GridVirtualScrollDirective {
  // Grid-specific inputs
  @Input() columns: ColumnDefinition[] = [];
  @Input() enableSelection = false;
  @Input() streamingService?: DataStreamingService;
  @Input() serverSideService?: ServerSideOperationsService;

  // Enhanced outputs
  @Output() rowClick = new EventEmitter<any>();
  @Output() cellClick = new EventEmitter<any>();
  @Output() streamingUpdate = new EventEmitter<any>();

  // Grid-specific methods
  renderGridRow(item: any, columns: ColumnDefinition[]): HTMLElement {
    // Complex grid row rendering
  }

  integrateWithStreaming(): void {
    // Streaming service integration
  }

  handleServerSideOperations(): void {
    // Server-side coordination
  }
}

But this would essentially recreate what we built manually, with added abstraction overhead.

Conclusion

We chose manual virtual scrolling implementation because:

  1. Grid-Specific Optimization - Designed specifically for data grid needs
  2. Performance Control - Direct control over every aspect of rendering
  3. Integration Capabilities - Seamless integration with streaming and server-side services
  4. Enterprise Requirements - Supports all advanced features needed for 10M+ records
  5. Debugging & Maintenance - Full visibility for troubleshooting and optimization

The VirtualScrollDirective is a good general-purpose solution, but for enterprise-grade data grids handling massive datasets, manual implementation provides the control and integration capabilities we need.

Usage Recommendations

Use VirtualScrollDirective for:

  • Simple lists
  • Basic virtualization needs
  • Rapid prototyping
  • Non-grid components

Use Manual Implementation for:

  • Data grids
  • Complex rendering
  • Streaming integration
  • Enterprise applications
  • Performance-critical scenarios
Success Metric: Our manual implementation successfully handles 10+ million records with smooth performance, which validates this architectural decision.