React with C++: Building the Quip Mac and Windows Apps

This post is about how we built Quip Desktop. Quip is a modern productivity suite designed for team collaboration. If you haven’t tried it already, download the app.

Introduction

From the beginning, we built Quip to work natively on multiple platforms. When we launched, we supported the Web, iPhone, and iPad. With the launch of Quip Desktop today, Quip is available on 8 platforms: Web, Mac, Windows, iPhone, iPad, Android phones, Android tablets, and Apple Watch.

Since Quip is a consciously lean company — we support those 8 platforms with just 13 engineers — we spend a lot of time thinking about how to achieve a polished native experience on all platforms while maximizing code reuse. For Quip Desktop specifically, we knew we wanted to have the web site and desktop app share as much UI code as possible since the experiences are so similar. Yet we also knew we wanted to build a native app that was as good or better than the legacy productivity suites.

We ended up designing a unique architecture based on a custom C++ library and a React-based UI that is shared between our web and desktop apps. We’re happy with the productivity gains from the code reuse, and we’re really happy with the user experience of the desktop app. This post is a high-level overview of our approach.

Architecture

Our goals for Quip Desktop were straightforward:

  • Fast — Data should be stored locally and synced passively. Loading a document shouldn’t require a network roundtrip and should be as fast as opening as file on your hard drive.
  • Offline Support — The app should work offline or online. No primary operation (editing, sending messages, sharing) should require a network connection.
  • Share Code — The app should share as much code with the desktop web site as possible. We don’t want to maintain three different versions of the app across Windows, Mac, and Web.

Around the same time we were designing the desktop app, we were discussing porting our web site to React. A major part of that port was transitioning our web site from sending down HTML to sending down raw data that the client would render in JavaScript. We quickly realized this new, data-based approach to our UI would work equally well whether the data was being retrieved from a remote server or from a local database.

After a few prototypes, we settled on an architecture based on React for our user interface. We designed a uniform data model and API based on Protocol Buffers that powers the React user interface. And then we implemented the API twice: once as a set of AJAX-based server handlers for our web app, and once as a set of function calls in a C++ library for our desktop and mobile apps.

So when you’re using the Quip Mac app, you’re running the same JavaScript code that you’re using in your browser. But when you perform an action like editing a document, the Mac app makes a call into a C++ library backed by a local database. When you edit a document in your web browser, the client makes that exact same API call, but sent via AJAX to Quip’s web servers.

Here’s a rough diagram of the architecture:

The C++ library (which we call the Syncer) is backed by a LevelDB database. It stores all the data you have access to in Quip, from messages to documents to the names of your colleagues. Whenever you make changes to your local database, we passively synchronize that with Quip’s servers — so your local instance is always up-to-date, but it doesn’t require a network connection to function correctly.

In the web app, where we don’t have access to a local database, we use a simple in-memory version of the Syncer interface to store data we receive from the server.

Data API

In Quip, every API call that mutates data or performs an action is called a Handler (similar in spirit to Flux actions). To make a new Handler, you write a Protocol Buffer message that defines its request and response. Here’s a pseudocode version of the Handler to send a message:

message SendMessageHandler {
message Request {
optional string text = 1;
optional int64 sent_usec = 2;
repeated File file_attachments = 3;
}
message Response {
optional Message sent_message = 1;
}
}

When you compose a message in Quip, we construct a SendMessageHandler.Request object in JavaScript. We serialize the protocol buffer to JSON, and we invoke the appropriate API for your platform.

In the Mac or Windows app, we send the serialized request to our C++ library via a custom JavaScript-to-Native bridge. The library writes the new message to the LevelDB database and returns a response immediately, so the change you make is reflected in the UI instantly — without waiting for a network roundtrip:

bool Handlers::SendMessage(
const SendMessageHandler::Request& request,
SendMessageHandler::Response* response) {
Message* message = response->mutable_sent_message();
// ...
string serialized;
message->SerializeToString(&serialized);
return level_db_->Put(message->id(), serialized).OK()
}

In your browser, we send the serialized request to our AJAX API, i.e., we make an HTTP request to https://quip.com/-/call-handler on our Python servers. That server handler looks a lot like the C++ function above, but it saves the response to our MySQL database, and it returns the response in the HTTP body:

def send_message_handler(request):
response = SendMessageHandler.Response()
# ...
backend.write_message_to_database(response.sent_message)
self.write_pb_json(response)

As mentioned above, the C++ library passively synchronizes changes in the LevelDB database with the server, so that message we wrote to the local LevelDB database would make it to the server almost instantly if you’re on WiFi. If not, it would be synchronized the next time you have internet access.

Data Model

Since Quip is designed for real-time collaboration, you get lots of real-time updates when you’re using the app. The desktop and web apps both connect to a WebSocket, and updates stream in as your colleagues edit documents or send messages. (They also stream in from your own usage if you’re using Quip on multiple devices.)

We wanted a data model in React that made it easy to respond to changes from the network without writing a lot of custom code for each data type in the app. (We currently have 19 data types, such as Document, Message, and User.) We came up with an approach based on two simple abstractions — Objects and Indexes — that enable our React components to listen to changes in a uniform way.

Objects

Every object in Quip is defined by a protocol buffer that encapsulates all the data the UI needs to display it. Every object has a globally unique ID and a bunch of custom fields:

message Document {
optional string id = 1;
optional string title = 2;
optional string created_by_id = 3;
// ...
}
message Message {
optional string id = 1;
optional string author_id = 2;
optional string text = 3;
// ...
}

Our MySQL databases on our servers and LevelDB databases on our native clients store (roughly) the same objects, which makes API calls and synchronization easy.

When you want to write a React component that renders an object, you make the object a property of your component and use a special React mixin called ModelObjectListener to ensure your component gets re-rendered whenever the object changes:

var MessageBubble = React.createClass({
mixins: [ModelObjectListener],
propTypes: {
message: React.PropTypes.instanceOf(Message).isRequired
},
render: function() {
return <div class="message-bubble">
{this.props.message.getText()}
</div>;
},
modelObjectsForProps: function(props) {
return [props.message];
}
}

The ModelObjectListener mixin registers the component to listen for changes to the object. When we get an update (whether from a WebSocket, or the results of a Handler invocation), we iterate through all the objects in the update, look up all components registered via ModelObjectListener, and re-render the relevant components.

Indexes

Every list of objects in the app, e.g., the list of messages next to a document or the list of documents in a folder, is called an Index. An index is just a unique name and a sorted list of object IDs:

message Index {
optional string index_id = 1;
  message Entry {
optional string object_id = 1;
optional string sort_value = 2;
};
repeated Entry entries = 2;
}

So the ID of a message thread index might be Messages/<document_id>. And the index for the sections in a document might be Sections/<document_id>.

Writing a React component to render an index looks a lot like the object example above:

var MessageList = React.createClass({
mixins: [ModelIndexListener],
propTypes: {
messageIndex: React.PropTypes.instanceOf(Index).isRequired
},
render: function() {
return <div class="message-list">
{this.props.messageList.forEach(function(messageId) {
return <MessageBubble message={Message.get(messageId)}/>
})}
</div>;
},
modelIndexesForProps: function(props) {
return [props.messageIndex];
}
}

The ModelIndexListener mixin registers the component to listen for changes to the index. So whenever a change to the messages index streams in from the WebSocket, we simply insert the changed entries into the index, and our list automatically re-renders.

Updates

Every React component in Quip is built on objects and indexes. Given this uniformity, every update from the server or C++ library — whether in response to an API call or an update from a WebSocket — has the exact same structure: a list of changed objects and a list of changed indexes:

message Changes {
repeated Object changed_objects = 1;
repeated Index changed_indexes = 2;
}

The main controller of the app is simply a dispatcher that accepts this data structure, does some caching, and re-renders components whenever updates come in. It’s a simple model that makes all components in our app update in real-time and removes the need to write special code to deliver updates for any of our 19 data types.

Performance

The application’s performance has exceeded the expectations of even our most optimistic engineers. Some of our beta testers told us the app loads documents so quickly, it’s jarring.

We did some benchmarks to quantify how fast the new architecture really is. In the benchmark below, we measured the time it takes to load a big document (in this case, the CRAY-1 Hardware Reference Manual) in Quip Desktop, Quip Web, Microsoft Word, and Google Docs on the same machine (complete methodology below), and the results are telling:

Quip Desktop loads almost twice as fast as Quip Web (even on a fast internet connection), and it loads 2.6 times faster than Microsoft Word. Google Docs wasn’t even close — it took about 5 times longer to load. (Surprisingly, Google Docs offline is even slower.)

Methodology

  • An HTML version of the CRAY-1 Hardware Reference Manual was imported into Microsoft Word 2011 (Version 14.5.2) and cleaned up slightly
  • The resulting content was repeated 10 times, to better simulate the original document (the HTML version was only of the first 3 chapters)
  • The resulting file (see CRAY-1-HRM x10.docx attachment) was imported into Quip as CRAY-1 Computer System®.
  • The document is 599 pages long and approximately 3.4MB in size (PDF).
  • The document was opened with the respective desktop apps (Word, Quip) already running (but without them having previously loaded the document in the session)
  • Loading times were measured by recording the screen with ScreenFlow and counting the frames (at 30 fps) between the user gesture to open the document and the contents appearing (without pictures being fully loaded).
  • Documents were opened from the in-app document browsing UI in Quip and Google Docs, and from the “Open…” panel in Microsoft Word
  • Recordings were done 5 times for each application and the results were averaged.
  • Tests were conducted on a MacBook Pro (Retina, 15-inch, Mid 2012 2.7 GHz Core i7, 16 GB of RAM) running Mac OS X 10.9.5. Chrome 43.0.2357.132 with a new profile was used for Quip Web and Google Docs.

Download

Quip Desktop runs on both Mac (version 10.8+) and Windows (version 7+), and it’s free to use. To download the app, visit quip.com/download on your laptop.

Technical Links

Show your support

Clapping shows how much you appreciated Bret Taylor’s story.