How we made Carousell’s mobile web experience 3x faster

A 6-months retrospective on building our Progressive Web App

Carousell is a mobile classifieds marketplace made in Singapore and growing in a number of South-East Asian countries, including Indonesia, Malaysia, and Philippines. We released a Progressive Web Application for selected mobile web users early this year.

In this write-up, we share (1) our motivations for wanting to build a faster web experience, (2) how we built it, (3) the impact it had on our users, and (4) what helped us move fast.

🖼 The PWA at https://mobile.carousell.com 🔎

Why a faster web experience?

Our app was created for the Singapore market, and we were used to our users having above-average smartphones and high-speed internet. However, as we expanded to more countries in the region, such as Indonesia and Philippines, we faced challenges in providing a similarly delightful and fast web experience. This was because the median device and internet speed in these places tend to be slower and less reliable than what our app was designed for.

As we read more about performance and started auditing our app with Lighthouse, we realised that we needed a faster web experience if we wanted to grow in these new marketplaces. Having a web page take more than 15s to load over 3G (as ours did) is unacceptable if we wanted to acquire and retain our new users.

🌩 The Lighthouse performance score that was a wake up call 🏠

The web is frequently the first place where our new users would discover and learn about Carousell. We wanted to give them a delightful experience from the start because performance is user experience.

To do this, we designed a new, performance-first web experience. When we were deciding which pages to work on first, we chose our Product Listing Page and Home Page since insights from Google Analytics indicated that these pages had the largest amount of organic traffic.


How We Did It

Starting with a Real-world Performance Budget

The first thing we did was to draft a performance budget to avoid making the mistake of unchecked bloat (an issue in our previous web application).

Performance budgets keep everyone on the same [page]. They help to create a culture of shared enthusiasm for improving the lived user experience. Teams with budgets also find it easier to track and graph progress. This helps support executive sponsors who then have meaningful metrics to point to in justifying the investments being made.
Can You Afford It?: Real-world Web Performance Budgets.

Since “there are multiple moments during the load experience that can affect whether a user perceives it as ‘fast’”, we based our budget on a combination of metrics.

Loading a web page is like a film strip that has three key moments. There’s: Is it happening? Is it useful? And, is it usable?
The Cost Of JavaScript In 2018

We decided on setting an upper limit of 120KB for critical-path resources, and a 2s First Contentful Paint and 5s Time-to-Interactive limit on all pages. These numbers and metrics were based on Alex Russell’s sobering write-up on Real-world Web Performance Budgets and Google’s User-centric Performance Metrics.

🔼 Our performance budget 🌟

To stick within the budget, we were deliberate in choosing the initial set of libraries to use (react, react-router, redux, redux-saga, unfetch).

We also integrated bundlesize checks into our PR process to enforce our budget for critical-path resources.

⚠️ bundlesize blocking a PR that exceeded the budget 🚫

Ideally, we would have automated checks for our First Contentful Paint and Time-to-Interactive metrics too. But, we haven’t done this because we wanted to release the initial pages first. We figured we could get away with this with our small team size by auditing our releases with Lighthouse every week to ensure that our changes are within budget.

Suboptimal, but setting up a performance monitoring framework is next on our backlog.

How we made it (seem) fast

  1. Adopting part of the PRPL pattern. We send the minimal amount of resources for each page request (using route-based code-splitting) and precache the rest of the app bundle using workbox. We also split out unnecessary components. For example, if a user is already logged in, the app would not load the login and sign up components. At present, we’re still deviating from the PRPL pattern in a couple of ways. First, the app has more than one app shell due to older pages that we haven’t had the time to redesign. Secondly, we haven’t explored generating separate builds for different browsers.
  2. Inlining critical CSS. We use webpack’s mini-css-extract-plugin to extract and inline each page’s critical CSS to improve Time to First Paint. This is to give the user the perception that something is happening.
  3. Lazy loading images not in the viewport. And progressively loading them when they are. We created a scroll observer component, based on react-lazyload, that listens to the scroll event and starts loading an image once it’s calculated to be inside the viewport.
  4. Compressing all images to reduce data transferred over the network. This came free with our CDN provider’s automatic image compression service. If you don’t use a CDN, or simply curious about performance for images, Addy Osmani created an amazing guide on how to automate image optimization.
  5. Using service workers to cache network requests. This reduces data usage for APIs that didn’t change often, and improved our app’s load times for subsequent visits. We found The Offline Cookbook helpful in deciding on which caching strategies to adopt. Since we had multiple app shells, Workbox’s default registerNavigationRoute didn’t work for us and we had to write a custom handler to match the navigation requests to the correct app shell.
⚙️ Using a network-first strategy with a 3s timeout for all our app shells 🐚

Throughout these changes, we relied heavily on Chrome’s “mid-tier mobile” simulation (with the network throttled to 3G speeds), and created multiple Lighthouse audits to evaluate the impact of our work.

Results: How did we do?

🎉 Before and after comparison of the mobile web metrics 🎉

Our new PWA listing page loads 3x faster than our old listing page. After releasing this new page, we’ve had a 63% increase in organic traffic from Indonesia, compared to our our all time-high week. Over a 3 week period, we also saw a 3x increase in ads click-through-rates and a 46% increase in anonymous users who initiated a chat on the listing page.

Before and after comparison of our Listing Page on a Nexus 5 with fast 3G. Update: WebPageTest’s “easy” report for this page. ⏭

Moving Fast, With Confidence

A consistent Carousell Design System

While we were working on this, our design team was also simultaneously creating a standardised design system. Since our PWA was a new project, we took the chance to create a standardised set of UI components and CSS constants based on the design system.

Having consistent designs allowed us to iterate fast. We built each UI component just once, and reused it in multiple places. For example, we have a ListingCardList component that shows a feed of listing cards and triggers a callback to prompt its parent component to load more listings when scrolled to the end. We use it in our Home Page, Listing Page, Search Page, and Profile Page.

We also worked with our designers to determine the appropriate performance tradeoffs in the app design. This allowed us to maintain our performance budget, change some old designs to conform to the new ones, and skip fancy animations if they were too expensive.

Going with the Flow

We opted to make Flow typings a requirement for all our files because we wanted to reduce annoying null value or type bugs (I was also a huge fan of gradual typing, but why we chose Flow instead of TypeScript is a topic for another time).

Adopting Flow proved to be super helpful as we developed and created more code. It gave us confidence in adding or changing code, making major code refactoring uncomplicated and safe. This allowed us to move fast without breaking things.

Additionally, the Flow typings have also been useful as documentation for our API contracts and shared library components.

A positive side-effect of having to write out the typings for our Redux actions and React components is that it helped us consider how we’d like to design our APIs. It also served as an easy way to start early PR discussions with the team.


Recap

We created a lightweight PWA to serve our users with unreliable internet speeds, released it page by page, and this improved our business metrics and user experience.

What helped us stay fast

  • Having and enforcing a perfomance budget
  • Reducing critical rendering path down to the minimum
  • Auditing often with Lighthouse

What helped us move fast

  • Having a standardised design system and its corresponding library of UI components
  • Having a fully typed codebase

Closing Thoughts

Looking back on what we’ve done over the past two quarters, we’re incredibly proud of our new mobile web experience and we’re working hard on making it even better. This is our first platform that’s heavily focused on speed, with thought also put into the loading journey of a page. The improvements in business and user metrics from our PWA has helped convince more people within the company of the importance of app performance and load times.

We hope that this article has inspired you to consider performance when designing and building your web experience.

Huge shout out to the people who worked on this project: Trong Nhan Bui, Hui Yi Chia, Diona Lin, Yi Jun Tao, and Marvin Chin. Also, to Google, especially Swetha and Minh, for their advice on this project.

Thanks to Bui, Danielle Joy, Hui Yi, Jingwen Chen, See Yishu, and Yao Hui Chua for their input and reviews.