At Duolingo, we're dedicated to making education accessible to everyone, regardless of their device or location. However, our metrics showed that millions of learners were quitting the app due to slow load times on entry-level devices, directly undermining our mission. To solve this problem, we assembled a team to optimize our Android app performance and improve the overall user experience.
Identifying Key Metrics for A/B Testing
Our approach to optimization is rooted in experimentation and metrics-driven decisions. We identified key conversion metrics that measure the percentage of users completing critical steps within the app, such as opening the app and reaching the home screen. By focusing on these metrics, we can directly influence daily active user (DAU) growth.
We prioritized conversion across three critical user journeys: app open, session start, and session end, with app open taking precedence given its top-of-funnel impact. We also established app open conversion as a company-wide guardrail metric to prevent other product experiments from introducing performance regressions.
Using and Building the Right Tools
Our typical approach to diagnosing app performance issues involves identifying slow user journeys, annotating relevant code sections with Trace markers, capturing system traces, and visualizing and analyzing the trace in Perfetto. We built an automated method tracing tool that transformed our workflow by dynamically tracing entire categories of methods without code changes.
When analyzing traces, we focus primarily on the main thread since it directly reflects what users perceive. Two patterns consistently flag optimization opportunities: idle gaps, where the main thread stays idle, often indicating slow background work; and extended blocks, long-running chunks of work on the main thread, prime suspects for causing frozen frames and ANRs.
Strategies for Optimizing App Performance
Removing or Deferring Non-Critical Tasks
We ran an audit of the app startup trace and found several non-essential tasks that weren't critical for getting users to their first interaction. By removing or postponing these tasks, we achieved great results with minimal effort.
Case study: deferring ads loading. We used to immediately load our ads libraries when the app started, but this initialization process was eating up processing time on the main thread! By deferring it until after the app was fully loaded, we cut our app startup time by 1.5 seconds and saved 20,000 learners per day from quitting before entering the app.
Trim the Fat: Request Only the Data You Need Now
Instead of fetching an entire dataset upfront, only request the specific portion the app immediately needs. Minimizing data size reduces CPU processing time, memory usage, and network bandwidth consumption.
Case study: sectioning Duolingo's course. We sectioned our ever-growing course models to reduce data size and improve performance. This allowed us to fetch only the relevant piece of the course, reducing load times and increasing DAU.
Optimizing Network Requests
Network requests may be invisible to users, but they can silently kill app performance. Our team discovered several optimizations that delivered significant DAU wins:
- Speed up requests by collaborating with backend teams to reduce latency.
- Improve multi-threading and parallelize CPU-intensive workloads.
- Optimize data serialization and deserialization for faster data transfer.
By optimizing our Android app performance, we've improved the user experience, increased DAU growth, and made education more accessible to everyone.