Loading image...
Scaling a React Frontend to 150k Users

Scaling a React Frontend to 150k Users

frontendreactperformancescalingdesign-systemswomp
Three things that mattered most when scaling Womp's frontend — component architecture built with designers, virtualizing heavy lists, and getting event listeners under control.

Scale changes what you can ignore

At a few thousand users, a React app can get away with a lot. Redundant renders, untracked event listeners, ad-hoc component patterns — none of it is visible. The app feels fast and the codebase feels manageable.

At 150,000 users, the same app behaves differently. Long sessions slow down. Lists with complex data feel sluggish. Engineers slow down because nobody knows where anything lives or which components to reuse. The things you could ignore become the things you spend your days fixing.

At Womp, scaling from a small user base to 150k meant confronting each of these in turn. Three problems stood out.

Component architecture built with designers

Early on, Womp had two engineers building the frontend. With a small team, it's tempting to just build what you need in the moment — create a new component, make it work, move on. The problem is that two engineers building independently without a shared system quickly end up with subtle variations of the same thing everywhere. Two slightly different button styles, three different ways to handle a dropdown, inconsistent spacing throughout.

We sat down with the designers and did something that had a disproportionate impact: we defined the component set together before building it. Not in code first, but as a shared vocabulary. What are all the button variants we actually need? What does a list item look like across all the places it appears? What are the spacing units we're working with?

From that conversation came a component library that both engineers pulled from. One Button, one Input, one way to build a modal. When you needed something, it already existed — you imported it and moved on. When it didn't exist, you added it to the library rather than creating a one-off.

The unexpected benefit was how much it changed the designers' workflow. Because the components were well-defined and documented, designers could spec new features using components they knew were already built. In some cases, they could write enough JSX themselves to lay out a UI without needing an engineer. The distance between a Figma file and a working screen shrank dramatically.

This compounded over time. Every feature built on top of the component library was faster to build, more consistent visually, and easier to update. When we changed a spacing value or tweaked a color, it propagated everywhere automatically. Without the library, that same change would have required hunting down every place it was hardcoded.

Virtualizing heavy lists

Womp is a 3D creative tool. The sidebar contains lists — layers, objects, materials, groups, imported assets. As users built more complex scenes, these lists grew. A detailed scene might have hundreds of layers, each with nested objects, each carrying its own state, thumbnail, visibility toggle, and interaction handlers.

Rendering all of them as real DOM nodes simultaneously is expensive. The browser builds and maintains a DOM node for every item, even the ones nowhere near the viewport. For small lists, the overhead is invisible. For lists of hundreds of items with non-trivial components per row, it becomes a real performance problem — initial render is slow, scrolling is janky, and memory usage climbs.

Virtualization solves this by only rendering the items currently visible in the viewport plus a small buffer above and below. As the user scrolls, items entering the viewport are rendered and items leaving are unmounted. The DOM stays small regardless of how many total items exist in the list.

Virtualizing Womp's lists had a noticeable impact on perceived performance in complex scenes. The panels that previously lagged on scroll became instant. Initial render of large scenes — where all those lists populated at once — became significantly faster. The DOM node count for a heavy scene dropped by an order of magnitude.

Combined with memoization on each list item — ensuring a row only re-renders when its own data changes, not when an unrelated item in the same list updates — the list performance at scale became comparable to what it felt like with a small scene. The user building a 500-object scene got the same responsive UI as the user with 20 objects.

Getting event listeners under control

As the Womp codebase grew, event listeners accumulated. Keyboard shortcuts, canvas interactions, drag handlers, window resize listeners — these were attached across the codebase in different ways, by different engineers, at different times.

The problem surfaced gradually. Mystery bugs that only appeared after extended use. Interactions that triggered the wrong handler. Handlers firing multiple times for a single event. Memory usage that climbed over a long session without an obvious cause.

When we audited the codebase, the pattern was clear: listeners were being added without corresponding removal logic. A component would attach a listener on mount and never remove it on unmount. When that component re-mounted, it attached another. Over a long session, with components mounting and unmounting repeatedly, the listener count grew silently in the background.

We made a strict rule going forward: every event listener had to live inside a useEffect with a cleanup function that removed it. No exceptions. Attaching a listener imperatively outside of React's lifecycle was flagged immediately in code review.

js

This pattern guaranteed cleanup automatically. When a component unmounts, React runs the cleanup function. The listener is removed. No manual tracking required, no way to forget.

We went through the existing codebase and migrated every listener to this pattern. The mystery bugs went away. Long session performance stabilized. And crucially, the rule was simple enough that it never slowed development down — every engineer knew exactly how event listeners worked in the codebase.

Takeaways

Component architecture, virtualization, and event listener discipline are different problems with a common thread: they all become invisible until scale makes them visible.

A small app with ad-hoc components, unvirtualized lists, and loose listener management works fine. The same patterns at scale produce a slow, buggy, hard-to-maintain product. The work of scaling a frontend isn't discovering new techniques — it's applying known patterns before the cost of not applying them becomes obvious.

The teams that do this well are the ones that establish the patterns early, while the codebase is still small enough to refactor cheaply, rather than after scale has made the problem hard to untangle.

Contact

Talk to me about engineering, fast cars, or anything that gets the adrenaline going.

Loading image...
Contact

Theme

Accent color

Gray color

Appearance

Radius

Scaling

Panel background