At any given time, Slack has many product teams working on different features. This allows us to build in parallel and quickly release new features reliably. But it also means the architecture of our applications can differ. And it’s not always easy to share the knowledge gained and the tough lessons learned across teams.
An important lesson we’ve learned over the last five years of developing Slack is to align on both what we create and how we create it. Early in our rewrite of the Slack desktop application, we came across an opportunity to do just that: We’d prototyped a new boot flow capable of launching our client much more quickly than before and realized that if we could create a framework from this proof-of-concept that met the booting requirements of all Slack product experiences, we could align teams to better streamline product development, improve productivity, and have all applications automatically adopt our best performance practices.
Internally, this fast-booting framework became known as Gantry, and five Slack products now use it to bootstrap themselves. In this post, we’ll explore the technical decisions made, and the benefits we’re seeing from using a unified boot framework.
Learning from Slack’s historical frontend decisions
A lot of early architectural decisions at Slack were made with smaller workspaces in mind than what we support today. This had a great deal of impact on the way our desktop client behaved:
- Different teams built their products in different ways. The main client, Calls, and Documents, for instance, each had its own unique architecture. Apart from the duplicative effort of reinventing the wheel each time, this also made it difficult for engineers to support each other across teams.
Slack workspaces rapidly outgrew these early assumptions and our frontend architecture had to scale accordingly. When we began prototyping a new client, we quickly found that the new boot architecture was also a good fit for other teams. It saved them from recreating what we had already done, allowing them to concentrate on product development instead.
By starting from a working prototype and making it flexible, we were able to build a boot framework that was reusable, whilst still being targeted to the needs of all projects using it. This, in turn, enabled all teams to move quickly, and in the same direction. Improvements made by one team could be shared among all the other projects using the Gantry framework.
The core concepts of Gantry
Gantry is tailored to address the shortcomings of our legacy framework. We identified areas for improvement in our applications and looked to solve those specifically. For that reason, Gantry aims to achieve the following five goals:
1. Fetch data on demand
Rather than loading everything upfront, Gantry apps fetch data incrementally, and only when absolutely needed.
This has multiple benefits: by moving to an incremental data model, we’ve been able to cut down on the number of initial queries that need to resolve before we can get Slack on a user’s screen. Breaking this data into multiple requests means we can request smaller chunks of data in parallel; minimizing the amount we are downloading results in less data being parsed by the main thread. By spending less time blocking on these calls, we can get a first meaningful paint on screen sooner.
2. Render the application client-side
We intentionally render our application client-side and forego any server-side rendering. This has allowed us to fetch everything we need to boot Slack from an edge server, cutting down round-trip request times.
Network latency can have a big impact on boot performance. By caching our data at the edge via Flannel (our application edge cache) and our assets on a CDN, we’re able to minimize latency as much as possible.
This is especially impactful for our customers located outside of the U.S. as all assets and data required to boot Slack are available at an edge location closer to them.
3. Front-load API requests and lazy load assets
Whilst profiling our boot process, we identified our initial API requests as a major bottleneck. To combat this, we dispatch them as soon as possible. By utilizing code splitting and dynamic imports in webpack, we are able to create a small boot payload capable of knowing how to start these API requests and download the rest of our application in parallel.
4. Cache assets and data locally
Gantry takes advantage of the powerful Service Worker API to cache assets and data on the user’s local machine. This has allowed us to eliminate network requests on subsequent boots, speeding up boot time further and even allowing customers to use Slack while offline.
5. Enable streamlined development and deployment
When starting new projects, we weren’t just seeing engineers create new boot architectures; we were seeing duplicative effort spent setting up the same dev tooling, profiling, metrics, and integration into our deployment pipeline. Differences in architecture become barriers to entry when engineers want to jump into another codebase. Gantry looks to remove this mental overhead by standardising development and deployment. By running a single command in a terminal, engineers can start a new Gantry application with developer tooling, profiling, and deployment built into it. This allows engineers to concentrate on what’s important to them (product development) rather than tooling, security, browser compatibility, and so on.
Benefits of a shared framework
Engineers are sometimes hesitant to couple products together, worrying about flexibility. Given that Gantry was tailor-made for the places it is used today, we have yet to run into this roadblock. Instead, we’ve found there to be some surprising upsides.
Applications are all built using the same webpack configuration, which means common code can be shared. After the first load of one Gantry application, subsequent loads of any other application will be much faster thanks to the way we use Service Workers and the browser’s cache. We only have to download the delta of missing assets to get another application up and running, and this is usually only a fraction of what’s already been downloaded.
Another advantage to sharing a base framework between products is around engineering productivity. In a fast-growing company such as Slack, multiple engineers support different initiatives and new engineers join the company weekly. Engineers can develop familiarity with a new codebase more quickly when there’s a common architecture to follow. They share best practices, and anyone can jump into any part of the codebase and find their way around easily. This has truly enabled our teams to move quickly and do their best work.
Gantry’s successful rollout internally came down to us taking a concept that was already working and abstracting it for general consumption. The intersection of multiple product requirements meant we covered most use-cases that a Slack application might need, and this has kept Gantry flexible enough to support many other applications since its inception.
Designing a shared framework for quick boots has enabled us to develop more rapidly and share our lessons with one another. As more teams start new projects with Gantry, they find they already have a wealth of knowledge at their disposal. This benefit cannot be emphasized enough: New products and improvements are developed and launched at a faster rate, making our customers lives simpler, more pleasant, and more productive every single day.
Gantry has changed the way we build our product but it’s far from finished. If this type of work excites you, come and join us! There’s so much we still want to do, and we’d love to have you along for the ride.
A massive thank you to Rowan Oulton for spending time editing this blog post. This would not have been possible without your dedication!