Solving Performance Issues with a Multi-Frame Application

Chris de la Iglesia
2020-12-15T21:13:00Z

The Problem to Solve

Salesforce Anywhere is a new communication app, built by Quip, to provide a place for communication and collaboration inside of Salesforce. As part of this new product, we planned to build new chat features into the Salesforce Lightning experience itself. Since Salesforce Anywhere runs on the Quip backend and tech stack, which is separate from the Salesforce tech stack, this required some special design decisions on our part.

Initially, we built a new React app to run Salesforce Anywhere that would re-use client-side code from Quip and connect to the Quip backend. Our first version ran an instance of this app in each chat window inside of an iframe, which communicated using messages with the main Salesforce app to create new chat windows or open up links.

This caused a few technical issues. First of all, it used up a lot of memory. When opening up a page with the chat header and five chat windows, the Salesforce Anywhere app consumed upwards of 500MB of Javascript VM memory alone. While our powerful development machines could easily handle that amount, many of our customers would have trouble if running other intensive applications.

In addition, loading up a full React app plus copies of our data management and other duplicate code takes time. In our initial version, it took roughly two to three seconds to open up a new chat, which is borderline unusable from a productivity perspective. We would need to get that load time down below a second, hopefully below half a second, before we could ship.

The Solution: A Multi-Frame Application

To solve these problems, we decided to rearchitect our app to use one instance of our React app that controlled each of the iframes on the page. Since React can easily render in multiple places, one app could be loaded that would render into each iframe every time a new chat window was opened. This would also solve the data duplication issue, as the same data management could be used for each application.

Normally, cross-frame functionality can only be done using messages sent back and forth between the different sets of Javascript code. However, if every frame comes from the same origin, then even if the app is embedded inside another webpage, each frame can directly access the Javascript memory of every other frame of the same origin. This allowed our code to run basically as-is, only using different window objects.

We chose to use the chat header (shown below) to be the controller frame that would render each other frame (called view frames). This is because the chat header is loaded up when Salesforce Lightning first loads, and so is always reliably loaded. Upon loading of the main Salesforce app, Salesforce Anywhere loads into a hidden iframe normally with no multi-frame shenanigans involved.

When a chat window opens up, it loads a minimal bundle of Javascript and CSS that only handles initialization. Upon loading, this “view frame” sends a message to the controller frame (i.e. the chat header), telling it which frame it is from and to start rendering. The controller frame then renders the React app into the view frame.

This change required a significant amount of refactoring to certain parts of our codebase, as some parts were built with only a single frame in mind. Thankfully, we already had some support code for multiple frames for our desktop app, since certain popovers and modals run in separate frames for a desktop-like experience. This code would allow us to create and manage window-scoped objects for each frame (such as our focus stack, which manages the focus inside each window). Every part of our codebase would need to be frame-agnostic, so we were able to use this support code to separate the logic of the code from the frame that it was operating on.

One unique point is the cross-frame messages sent from Salesforce Anywhere to the main Salesforce app. In our initial app, each frame individually sent messages to communicate with Salesforce. This meant that, from the Salesforce side, messages came from every frame and not just the single controller frame. We wanted to maintain this interface between Anywhere and Salesforce, as it is simpler and easier to understand, but in the new multi-frame world all of the view frames only had a small amount of code to send an initialization message to the controller frame. Thankfully, we had already encapsulated the message sending/receiving handlers inside a convenient bridge object. Each view frame would create its own bridge object that would send messages originating from the view frame but could be used from the controller frame. When the controller frame needed to send a message for a view frame, it would find the corresponding bridge object and send the message using that.

The Result

After implementing this system, we saw large improvements to memory usage and chat window load times. Each chat window went from taking up 40MB of memory per window to <10MB of memory per window, dramatically lowering overall memory usage when users have many windows opened. In addition, load times for each chat window decreased from three seconds to less than a second, improving usability and enjoyability.

As part of this investigation, several lessons became clear. First, measuring exactly what you want to improve is very important. We did a large amount of profiling and investigation to determine what was using up memory and why load times were so bad, which allowed us to proceed with confidence and get the results that we wanted. Second, building a complete but inefficient version of your app first can provide tons of valuable information on what will end up using the most resources in the final product.