DEV Community

전규현 (Jeon gyuhyeon)
전규현 (Jeon gyuhyeon)

Posted on

Making Real-Time Gantt Charts: 10,000 Tasks at 60fps

"Real-time Gantt charts on the web? Will performance be good?"

A question I received 2 years ago. Answer: "YES, but..."

Today, I'll share how we implemented a Gantt chart that renders 10,000 tasks at 60fps.

Technology Stack Selection: Why SVG?

We debated whether to draw with Canvas, SVG, or DOM.

Canvas is fast but interaction is difficult and accessibility is almost none.
DOM supports native events but takes 2 seconds even with just 1000 items.
SVG is in between. Fast at around 200ms while supporting native events.

SVG is optimal for Gantt charts.

SVG Optimization Strategy

But SVG is also slow if used as-is. Optimization is key.

class OptimizedSVGGantt {
  private taskPool: Map<string, SVGRectElement> = new Map();

  // 1. Minimize DOM manipulation with Object Pooling
  getTaskBar(taskId: string): SVGRectElement {
    if (this.taskPool.has(taskId)) {
      return this.taskPool.get(taskId)!;
    }

    const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
    this.taskPool.set(taskId, rect);
    return rect;
  }

  // 2. Use CSS Transform (GPU acceleration)
  moveTask(element: SVGElement, x: number, y: number) {
    element.style.transform = `translate(${x}px, ${y}px)`;
    element.style.willChange = 'transform';
  }
}
Enter fullscreen mode Exit fullscreen mode

Virtual Scrolling: Handling 10,000 Tasks

Rendering all 10,000 tasks exceeds 20MB memory.
Solution? Only render what's visible on screen.

class VirtualGantt {
  private itemHeight = 30;
  private buffer = 5;

  calculateVisibleRange(scrollTop: number, viewportHeight: number, totalTasks: number): [number, number] {
    const startIndex = Math.floor(scrollTop / this.itemHeight);
    const endIndex = Math.ceil((scrollTop + viewportHeight) / this.itemHeight);

    // Add buffer (smooth scrolling)
    const bufferedStart = Math.max(0, startIndex - this.buffer);
    const bufferedEnd = Math.min(totalTasks, endIndex + this.buffer);

    return [bufferedStart, bufferedEnd];
  }
}
Enter fullscreen mode Exit fullscreen mode

Only 40 of 10,000 are actually rendered.
Memory usage drops 95%.

WebSocket + CRDT: Real-Time Synchronization

What happens when multiple people modify the same task simultaneously?
CRDT (Conflict-free Replicated Data Type) is the answer.

import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';

class RealtimeGanttSync {
  private yDoc: Y.Doc;
  private yTasks: Y.Map<any>;

  initialize() {
    this.yDoc = new Y.Doc();
    this.yTasks = this.yDoc.getMap('tasks');

    // WebSocket connection
    new WebsocketProvider('wss://api.plexo.work/gantt', 'project-123', this.yDoc);

    // Detect changes
    this.yTasks.observe(this.handleRemoteChange);
  }

  // Auto-merge without conflicts!
  handleRemoteChange = (event: Y.YMapEvent<any>) => {
    event.changes.keys.forEach((change, key) => {
      if (change.action === 'update') {
        const task = this.yTasks.get(key);
        this.updateGanttUI(key, task);
        this.showNotification(`${task.modifiedBy} modified`);
      }
    });
  };
}
Enter fullscreen mode Exit fullscreen mode

Yjs automatically resolves conflicts.
A changes start date and B changes end date, no problem.

Performance Measurement Results

For 10,000-task project:

  • Initial rendering: 1.2 seconds
  • Scroll FPS: 55-60fps
  • Real-time update delay: 50ms
  • Memory usage: 50MB

Before optimization, even 1000 took 10 seconds.
Now tested up to 50,000.

Practical Implementation Tips

Profiling is the Answer

class PerformanceMonitor {
  measure(name, fn) {
    performance.mark(`${name}-start`);
    const result = fn();
    performance.mark(`${name}-end`);

    performance.measure(name, `${name}-start`, `${name}-end`);

    const measure = performance.getEntriesByName(name)[0];
    console.log(`${name}: ${measure.duration}ms`);

    return result;
  }
}
Enter fullscreen mode Exit fullscreen mode

Open Chrome DevTools Performance tab and measure.
You can know exactly where the bottleneck is.

Gradual Improvement

No need to be perfect from the start.

Step 1: Implement basic features (slow is OK)
Step 2: Apply Virtual Scrolling
Step 3: Separate heavy calculations with WebWorker
Step 4: Introduce caching strategy
Step 5: Rewrite core algorithms with WebAssembly

User Experience First

Even if slow, response must be immediate.

async handleUserAction(action) {
  // Immediate feedback
  this.showLoadingIndicator();

  // Heavy work asynchronously
  await this.processInBackground(action);

  // Update after completion
  this.hideLoadingIndicator();
}
Enter fullscreen mode Exit fullscreen mode

Conclusion: Technology is Just a Means

No matter how technically perfect, if users don't use it, it's meaningless.

What we focused on:

  • Fast Response: Must move immediately on click
  • Smooth Scrolling: Won't use if laggy
  • Real-Time Sync: Collaboration is key
  • Intuitive Interface: OK without manual

The most rewarding moment was when a user said "Wow, this is really fast!"

Planning to build web-based Gantt charts?
Hope this article helps.


Experience high-performance real-time Gantt charts. Plexo

Top comments (0)