In short: Our org chart froze the browser at 10k employees (1 min 45 s to expand, 5.5 GB of memory) and crashed outright at 240k. Over two years I replaced per-node Vue instances with a ~200-line renderer, rendered only what's in the viewport, and moved hierarchy building to the backend. A 10k org now renders in under 5 seconds with 1.1 GB, and a 240k org loads in 0.8 GB.
At Agentnoon we build org chart software for large enterprises. Our customers range from 5,000 to 240,000 employees. This is what I learned scaling the frontend over ~2 years.
Mid-2022, I integrated d3-org-chart as the foundation. It handled layout, expand/collapse, zooming. We mounted small Vue instances as custom elements inside each node. It was a necessary workaround step since d3 only supported string templates as the individual elements, so tiny custom html elements were our approach necessary for this implementation.
This worked fine for a few thousand employees. At 20k it got sluggish. At 240k the browser just died.
For every org chart node we were: – creating a custom element via document.createElement – mounting a fresh Vue instance inside it – setting up reactive dependency tracking – rendering the component in its own scope, disconnected from the parent app
With 10k visible nodes: – 1 min 45 sec to expand the full chart – 5.5 GB memory – browser frozen
The d3-org-chart library was 2,000+ lines of code we’d heavily patched. Drag & drop libraries couldn’t understand the shadow DOM scope. Every new feature was duct tape on top of duct tape.
I was implementing drag & drop for position planning and hit a wall — libraries couldn’t work within d3-org-chart’s shadow DOM. I had three options: ignore it, write a custom implementation, or rewrite the chart as a native Vue component.
I went with option 3. Had a hunch it would be quick for this case — no expand/collapse needed, I knew how to extract d3’s tree layout calculations, and we only needed root + one level of children.
It took 4-5 hours. The result was ~200 lines. A template iterating over data, setting transform attributes on SVG nodes and lines. No custom elements, no shadow DOM, no separate Vue instances.
The key: let d3 calculate the layout, let Vue render the DOM. The old approach used d3 for both, then hacked Vue on top.
| Metric | d3-org-chart | Native Vue | | --- | --- | --- | | Time | 1 min 45 sec | < 5 seconds | | Memory | 5.5 GB | 1.1 GB | | Code | 2,000+ lines | ~200 lines |
Next bottleneck: custom fields. User-defined attributes stored in PostgreSQL. For a 30k org with 20 types, that’s 600k rows. We loaded all of them on startup.
What I found: – most features didn’t need ALL values – filters only needed unique values per type — one SQL query, sub-second even for 600k rows – org chart cards only needed values for visible nodes
So I built two backend endpoints (unique values + filtered employees) and used TanStack Query to fetch values per person as they enter the viewport. Cache policy handles the rest.
| Metric | Before | After | | --- | --- | --- | | Directory memory (30k org) | 400-500 MB | 200-300 MB | | Full app path | 1,400 MB | 900 MB |
The big one. We were fetching the entire people collection on app load. For 240k employees that meant decompressing and parsing everything before anything could render.
I moved JSON decompression to a Web Worker first (kept the UI responsive but didn’t reduce memory). Then rethought the whole approach:
