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.
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 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.
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?
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.
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.
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
- 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.
- 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.
- 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.
- 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.
- 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
registerNavigationRoutedidn’t work for us and we had to write a custom handler to match the navigation requests to the correct app shell.
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?
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.
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.
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
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.
Appendix: Some Resources That We’ve Found Incredibly Useful
- Designing for Performance
- Observing your web app
- PWA Case Studies
- React Performance Fixes on Airbnb Listing Pages
- Redux modules and code-splitting
- Start Performance Budgeting
- The entire section on Performance in Google’s Web Fundamentals series
- The future of loading CSS
- The State of the Web
- Twitter Lite and High Performance React Progressive Web Apps at Scale