Chasing 60fps in the Figma canvas

Figma’s collaboration features let teams leave feedback directly on design files, but as comment counts grew, so did the performance cost of panning and zooming with those comments visible. The engineering team set a target of rendering the editor at a consistent 60fps, regardless of how many comment threads a file contained. The project that followed improved scrolling FPS by three-fold by fixing two distinct inefficiencies in the comment rendering path.

The architecture behind comment pins

Figma’s editor runs on a stack that has been described as “a browser inside a browser,” using WebGL and WebAssembly for the core rendering. Parts of the UI, however, are built in TypeScript and React. Comments are one such part, and they behave differently from typical static React UI because they are anchored to the canvas and must move as the user pans and zooms.

Each comment pin needs constant updates about the viewport to calculate its on-screen position. Those viewport updates were stored in Redux, and comment components would retrieve them and recompute their positions accordingly. The overhead came from how that data was distributed.

Finding the render bottleneck

Investigating with Chrome performance tools and React Profiler, the team found that most of the frame time—about 68ms per frame on a page with 30 comments—was spent on JavaScript execution, rather than rendering or painting. React Profiler revealed why: while the comments view itself only needed about 1.8ms to re-render, far more time was spent re-rendering fixed-position components like the left panel, toolbar, and properties panel. These components had no reason to react to viewport changes, yet they were being re-rendered anyway.

The cause was in the data flow. Whenever an action was dispatched, Redux runs every middleware and executes mapStateToProps for every connected component. In this case, viewport information was stored in the Redux store, so every connected component was receiving and processing that update, and the data was being passed down through multiple component layers before reaching the comments view. Anonymous functions passed as props forced some components to re-render repeatedly as well.

Removing viewport data from Redux

The first fix was to stop storing viewport information in Redux entirely. The team implemented a custom event emitter in the React codebase to broadcast viewport changes instead. They also migrated legacy components to functional components and took advantage of React Hooks to memoize expensive computations so they would only run when the relevant information actually changed.

This approach eliminated the dispatch-to-Redux cycle for viewport updates, which in turn stopped running mapStateToProps for every connected component and prevented data from being threaded through multiple layers. Components that don't care about viewport state no longer re-render. The result was a significant jump, from roughly 15fps to 50fps with 50 comment pins visible.

From O(n) to O(1) transforms

Still short of the 60fps target, the team noticed that performance degraded linearly as the number of comment pins increased. Each pin was individually computing its position and performing a CSS transform on every viewport change. With n comment threads, each frame triggered an O(n) operation—a negligible cost for a few comments, but a growing problem in files with many threads.

The solution was to invert the approach: instead of moving every pin, move a single overlay container. The team created an overlay container that encloses the top-left and bottom-right comment pins, gave the pins static positions within that container, and then used CSS translate to reposition the whole container in a single computation per viewport change. This reduced the per-frame work from O(n) to O(1).

The tradeoff is that whenever a new comment is added, the boundary box around the outermost pins must be recalculated. That cost is acceptable because adding comments happens far less frequently than panning the canvas, and the boundary computation occurs while the canvas is stationary.

Results and next steps

Within the project’s scope—maintaining 60fps for files with up to 150 comments—the work was successful. The scrolling interaction is smoother, and a side benefit is that the React codebase has been modernized with functional components and hooks.

The team acknowledges this is not a permanent finish line. Performance work at Figma is treated as an ongoing process of setting new goals and hunting down the next bottleneck as the product scales.