Performance Postmortem: Mapbox Studio

Eli Fitch

Eli Fitch

October 30, 2018

Mapbox Studio empowers designers and developers to create beautiful map designs, kind of like photoshop for maps. And just like photoshop — okay, probably a little less than photoshop — it’s complex. Very, very complex.

Studio is a large React - Redux app that renders data to a WebGL canvas with the help of Mapbox GL. Studio provides a suite of tools for managing ones map styles, datasets, and tilesets (composable map chunks), allows users to draw shapes on a map and add data to those shapes to create data to visualize, and of course it has a robust style editor with a suite of tools to make a gorgeous, expressive map of their very own.

We have an interpolation curve editor to make colors, fonts, sizes, and anything else change on-the-fly depending on your data, other styles, and the current zoom level. We have a formula editor that gives you tools to create fancy mathematical expressions. We’ve got icon and font upload & management tools. We’ve got a special data filtering and inspection mode to let you visualize every bit of data in your map style at once.

Mapbox Studio isn’t new; it’s had new concepts built on the bones of old ideas for four years running. Studio is complex because maps are complex. Map design is complex. Much of Studio’s complexity isn’t going anywhere. Honestly laying it out like that makes me feel proud that the app is as trim and reliable as it is, but I digress.

As I’m sure you can guess by now, like many large, mature, and complex applications, Mapbox Studio had a problem: it was slow. We’d been focused on features over fundamentals for a long time, and we had some real performance debt to pay down. This is the story of how we approached solving this problem, or at least making it less bad.

How we knew we had a problem

It took 5+ seconds for the app to render anything of consequence, so that seemed bad! We didn’t need to conduct exhaustive tests to discover our performance problems. We knew this didn’t even meet our own expectations.

We knew it was bad. Finding room in our priorities to address these performance problems is another matter. Studio is a (very) large application, but our team is not. We’re 4 developers & designers and one manager; that’s it. We have goals to meet, and it can be challenging to make room for initiatives like refactors and performance improvements.

I made a case by drawing a connection between our performance problems, and parts of the codebase that were difficult to interact with as a developer, and time wasted waiting for the app to refresh during development. That way we can treat this as a refactor and developer productivity improvement as well as a UX improvement.

Our approach

We are lucky working on Mapbox Studio that we can cheat. By “cheat” I mean that since it’s “photoshop-for-maps” we don’t support devices with small displays, and don’t have to worry about particularly slow connection speeds. Moreover, the vast majority of our users are on relatively high-end hardware, so JS parse times are less of a concern than they otherwise would be. We knew from conducting tests with Chrome performance tools that our JS bundle was downloaded and parsed long before our First Meaningful Paint, due to us holding on a spinner until a bevvy of API calls came back.

What does all of that mean? Reducing bundle size wasn’t the first thing on our list. Downloading and parsing our javascript bundle was only a fraction of our total loading time, and reducing that size would have been far more work for far less gain than figuring out ways to unblock our rendering. We came away with dual priorities: cutting our bundle size as much as possible without doing brain surgery, and making changes to allow us to render content before all these API calls return.

🛠️ Step one: Refreshing our build process

Our first step was switching from Browserify and a custom build script to using a Webpack powered build system in use on other Mapbox apps. We call it “Chunk Light”, but it’s basically a shared solution for generating a Webpack config. Making this switch was an important step, because we wanted to have the freedom to easily split code — though we’re not yet — and be able to leverage the Webpack ecosystem for bundle analysis and tree shaking some of our larger libraries (such as Lodash). Standardizing with other Mapbox apps was a nice bonus too.

With a switch to this build system, we cut our bundle size by a couple hundred KB. This was just due to the us tree shaking some big imports, most notably Lodash, Moment & Ramda. Furthermore, we now publish two types of Javascript bundles: one of dependencies that are unlikely to change frequently — a vendor bundle — and can thus be cached more aggressively, and an app bundle which is less likely to be cached. We also publish two classifications of these bundles: modern and “retro”. The modern bundles have less aggressive Babel transpilation & polyfilling to take advantage of modern browser features. We have a tiny inline script in our html that determines whether a browser gets the modern or retro bundles. The combination of these things led to an improvement of ~500ms in time-to-first-meaningful-paint for users that had our vendor bundle cached. Solid first step!

✋ Step two: Deferring requests

We knew from conducting some analyses with Chrome’s performance tools that our Javascript was downloaded & parsed long before all our data was loaded and we replaced our loading spinner with rendered content. Making matters worse, this blocking of renders would happen not just on map startup, but also occasionally when moving from view-to-view

After some investigation, we discovered some older code that was waiting for these requests to resolve before mounting the root component for the route. We were essentially waiting for application state to hydrate before rendering any components associated with a particular view. The sheer number of these requests made for a quite involved — read: tedious — process of enabling individual components to render empty and then request the data they need to populate content.

We decided to focus our work on the three most-used interfaces: the landing page, styles list and map style editor. After getting these views in a state where they were no longer blocking renders until data loaded, we encountered a problem: these components never needed to have an empty state before. They looked janky without any content to display. Enter step three 👇

☠️ Step three: Skeleton screens

Since we had never needed to render components without data before, we had no UI state to accomodate this. We needed to display something to indicate content was loading, and we decided to go with an animated skeleton screen pattern. We ended up making a handful of these skeleton components to match the layout of some of our more common data-displaying components.

Our finished ☠️

👩‍🎤 Step four: Cultural change

Our focused performance push was limited in terms of time and resources, so we took a very pragmatic approach, concentrating our efforts in the areas we felt would give us maximum improvement for minimum time & effort. What was essential to make sure we didn’t regress from here was creating a cultural change in our front end engineering org in order to raise awareness of performance’s importance and to make performant solutions the default solution.

We did that by shipping some changes to our build system to produce smaller JS bundles, doing a company-wide deep dive into the approach we took, and giving special ground for performance discussion in our front end office hours. Oh, and adding a Calibre subscription didn’t hurt either. Now we can much more easily track performance trouble across Mapbox front end products. For example, we were able to catch a major performance regressions with the redesigned homepage and made sure that video optimization was as much a part of our process as image optimization.

Nurturing cultural awareness and enthusiasm for building fast, snappy, responsive, tactile products is arguably the most effective performance improvement you can make, but can be the most challenging, and requires the most ongoing attention. Taking this step at the end of a performance sprint was a good move for us, because we could point to results and talk through actionable steps, rather than talking about the importance of performance in the abstract.

🌎 What’s on the horizon for Studio performance

This performance sprint was a big step in the right direction. We got time-to-first-meaningful-paint down from ~4.7s to just ~1.9 seconds on a connection representative of our users! That’s a big improvement, and I’m happy with our work, but 1.9 seconds is still a lot longer than we’d like to see, and there’s still more that we can do.

Here’s what we’ve got lined up next to push that number down even further and improve in other ways.

Structured cross-product caching Mapbox has an interesting architecture when it comes to managing our CSS. Because Mapbox Assembly, our in-house CSS framework, is architected around the principle of functional CSS — also called atomic, or utility-first CSS — we have the ability to style all our apps using the same relatively small CSS file. If we can ensure all our apps are on the same version of Assembly, we can effectively cache CSS and, perhaps more importantly, fonts from product to product.

Creating a system to make keeping our apps on the same version of Assembly more convenient – to leverage the full benefits of cross product caching – is a goal. If you’d like to learn a bit more about how we manage our styles for a suite of large applications at Mapbox, check out my talk from Scotland CSS.

Code splitting by view Some of Mapbox Studio’s largest dependencies and most complex code are isolated to a handful of views. If we split code by view, the views that don’t need that logic would become markedly faster. There is some legacy code structures that would make this difficult, but it’s a goal for us moving forward.

⚖️ An exercise in pragmatism

As a wise man – Jem Young to be exact – once said, “software architecture is the art of making intentional tradeoffs”. We focused our efforts on the most visited views, the most used features, and relentlessly chased the bargain solutions: trying to get the best bang for buck, the most improvement for minimum time spent.

Do I wish we could have done more? Sure. But resources are finite and performance is one priority among several. As it stands, a ~2.8 second improvement in time-to-first-meaningful-paint is a result we’re proud of, to say nothing of the increased cultural awareness of and focus on performance in Mapbox’s front end engineering org. If I were to offer one piece of advice to you, dear reader, when conducting your own performance sprint, it would be to stay pragmatic. If you identify your most impactful improvements up front and diligently limit your scope, you’ll reap the benefits.

Mapbox use Calibre to stay on top of performance. You can too! Start a Free 15-day trial now.

Eli Fitch

Eli Fitch

Eli is a Senior Frontend Engineer at Mapbox. Find him on Twitter.

Become a site speed expert

Join thousands of subscribers keeping up-to-date with web performance news and advice.

The best performance newsletter I’ve come across. Highly recommended.

Addy Osmani

Addy Osmani

Engineering Manager at Google Chrome