Engineering

Data-driven web performance optimization

Oct 27, 2025

At Cabify, the Help section of our app centralizes all support requests. It’s a critical moment to deliver real assistance and reinforce brand trust. A slow or unresponsive page at this stage can quickly lead to frustration.

With over 1M monthly accesses from our Rider app and 600K from our Driver app, this section serves as the primary support channel for our users. Notably, 85% of these users are located across LATAM, often accessing our Help section while on the move with varying network conditions.

Whether you’re optimizing a desktop web app or a web view inside a mobile app like ours, performance is essential. In this post, we’ll share how we audited and improved load times in our Help section using a data-driven approach, the tools we used, and the key changes that created a faster, smoother user experience.

Architecture and Tech Stack

The Help section in the Cabify app is a separate application shown as a Web View in both Android and iOS apps. You can also access it on Cabify websites like Cabify Business and the Cabify Help Center. This setup ensures a consistent user experience across all platforms while provides the following benefits:

  • Release independence: We can update the Help section without changing the mobile apps. This speeds up our release cycles and reduces dependencies.
  • Code reusability: One codebase works for all platforms. This lowers maintenance needs and keeps everything consistent.

Separating the Help section into its own app presents some challenges. Since it’s part of other apps, we need fast load times. This way, users see it as part of the app, not as a separate or slower experience. We must also keep the JavaScript code compatible across all platforms. While we won’t discuss this in detail, we recommend using @babel/preset-env with Browserlist for easier compatibility setup.

Next, we will explore the user-centric performance metrics we used to understand how users experience our app. We utilized tools such as:

  • Grafana Faro: For monitoring and visualizing real-time performance metrics.
  • Google DevTools: To analyze and find areas for improving load times and resource usage.
  • Webpack: We used it to apply techniques like code splitting, tree shaking, minification, and compression. The webpack-bundle-analyzer plugin helps us visualize and analyze the bundle contents. Vite is a great alternative, too.

User-centric Performance Metrics

Performance optimization begins with how users experience your application. Users may see performance differently based on their network speed and device capabilities.

To tackle this variability, we use measurable performance metrics that show real user experiences. One key metric is Largest Contentful Paint (LCP). It measures how long it takes for the largest visible content element on a page to load.

Google’s Core Web Vitals defines LCP as follows:

“LCP reports the render time of the largest image, text block, or video visible in the viewport, relative to when the user first navigated to the page.”

Google suggests that to ensure a good user experience, sites should aim for an LCP of 2.5 seconds or less. This threshold should be measured at the 75th percentile of page loads for both mobile and desktop devices. This means at least 75% of your users should see an LCP of 2.5 seconds or less.

LCP thresholds: A good LCP value is 2.5 seconds or less Figure 1: LCP thresholds: A good LCP value is 2.5 seconds or less.

Measuring Performance

Grafana Faro lets us monitor performance in real time. This helped us make a rapid assessment of our optimizations’ impact.

Here’s the measured LCP before optimization:

24-hour LCP visualization with an average of around 5 seconds and a range between 7 seconds and 3.8 seconds Figure 2: Grafana Panel showing the evolution of LCP during 24 hours.

Short-interval LCP data can be noisy. We aggregate and segment it by percentiles to reveal trends and outliers. The next graph shows the LCP distribution before optimization, focusing on the 50th (p50), 75th (p75), and 90th (p90) percentiles:

LCP during 1 week before the optimization segmented into percentiles with an average of 3.5 seconds for p50, 5 seconds for p75 and 8.5 seconds for p90 Figure 3: LCP distribution before optimization segmented by percentiles over 1 week.

At Cabify, we pay special attention to the 90th percentile (p90) or higher. It highlights the experience of our slowest users, a critical segment when operating at scale.

The gap between p50 and p90 shows the consistency of user experience. A large gap means some users face significant delays, even if the median is acceptable.

We also track the standard deviation of LCP. A high standard deviation signals a wide gap between the best and worst user experiences. Reducing this variability is key to delivering a consistently fast experience for everyone.

LCP during 1 week before the optimization with an average of 4 seconds and a standard deviation between 2 and 6 seconds Figure 4: LCP average and standard deviation statistics before optimization.

By focusing on p90, the gap to p50, and the standard deviation, we identify and prioritize improvements. This benefits not only the median user but also those at the edges of the experience spectrum.

Understanding the Problem

After we found performance issues in our metrics, we looked for root causes.

Our data reveals that the vast majority of our Help section users (85%) are located in LATAM, accessing support while on the move. These users often experience spotty mobile connections as they travel through urban areas with varying coverage or congested networks. This reality makes performance optimization not just a nice-to-have but a critical business requirement.

Chrome DevTools helps us test various network conditions. It also captures performance metrics. This helps us understand what affects loading times for our users.

An image displaying the Chrome Dev Tools' Performance tool simulating a 4G connection before the optimization Figure 5: Chrome DevTools Performance analysis showing bottlenecks on 4G connection.

In this snapshot, we see a bottleneck in downloading JavaScript and CSS assets, especially the vendor chunk. This insight gives us a clear target for optimization: we should improve the download of web assets. Now, let’s take action to boost performance!

Making Things Faster

To optimize the download of resources, we applied several strategies:

  1. Reduce the size of the resources.
  2. Split the resources to take advantage of parallelization.
  3. Apply minification and compression.

Reducing the bundle size

As mentioned before, the vendor chunk slows down the entire page render. To cut its size, we can replace large dependencies with smaller ones.

We used webpack-bundle-analyzer to see the size and content of different chunks. This helped us spot which libraries contributed the most to the overall size.

Content of the bundle before the optimization Figure 6: Bundle analyzer showing content and size distribution before optimization.

From this analysis, we found several areas for improvement:

  • Heavy Polyfills: The @formatjs polyfill is no longer needed for our supported devices and platforms.
  • Heavy Libraries: We can replace moment.js and marshal-formularious (including frequency_list) with lighter options like date-fns and react-hook-form.
  • Libraries at startup: Libraries such as Grafana, Rollbar, and Amplitude provide valuable insights. We should evaluate whether they need to be loaded at startup.

We made some changes that cut the total bundle size from 3.71 MB to 2.05 MB. The vendor chunk, which greatly impacts LCP, dropped from 1.98 MB to 1.23 MB. We also removed a large dynamic chunk, 65.929b21f2ce4d76117ca0.js, sized at 800 KB. This change does not affect the initial load.

Content of the javascript bundle after removing some dependencies Figure 7: Bundle analyzer showing reduced bundle size after dependency optimization.

Parallelization

Parallelization can cut down the time needed to download resources.

Two techniques use parallelization:

  • Split Chunks: We divide an asset into chunks and reassemble them after downloading.
  • Dynamic Loading: If our app has many routes, we load only the code for the first page initially. The rest downloads on demand.

These techniques are effective, but they have trade-offs. Splitting a bundle adds extra bytes from headers and metadata needed for reassembly. It’s crucial to measure performance after these changes to ensure loading times improve.

Another point to consider is the increased complexity from these methods. For chunk splitting, we need more configuration. Dynamic loading changes how we declare components, using dynamic import syntax based on the JavaScript library you are using.

In our case, the application needs most dependencies at startup. So, dynamic loading offers no benefits. For chunk splitting, tests showed no significant improvement, even on slower connections. This led us to discard this approach in the end.

Minification and Compression

At first, compression wasn’t enabled in our setup. We lost it during a package migration (oops! 🙈) and didn’t notice until we checked performance. The key takeaway? Measuring system performance is vital. But we also need alerts to notify us of unexpected changes in metrics. Lesson learned!

Check if assets are compressed using the Network Tab in Chrome DevTools. In the Size column, you’ll see two numbers: the encoded size (compressed) and the decoded size (uncompressed). If these numbers match, the assets aren’t compressed. The first graph below shows this issue clearly.

Minification is not enabled while serving resources Figure 8: Network tab showing uncompressed resources (minification disabled)

After applying minification, the content shrinks by around 75%.

An image displaying how we can see that minification is enabled properly Figure 9: Network tab showing compressed resources with minification enabled.

We configured Brotli as compression algorithm because it offers superior compression rates compared to gzip. Brotli is supported by most modern browsers and can improve compression rates by 15% to 25% for JavaScript, HTML, and CSS files. This results in faster loading times overall, especially for users on slower connections. This setting can be configured using a plugin like compression-webpack-plugin.

Here’s how we configured chunk splitting and minification in our Webpack setup:

// webpack.config.js
  optimization: {
    emitOnErrors: false,
    minimize: true,
    moduleIds: 'named',
    splitChunks: {
      cacheGroups: {
        cabify_vendor: {
          chunks: 'initial',
          name: 'cabify_vendor',
          test: /node_modules\/(@product)(\/\w)*.*\.js$/,
        },
        styles: { chunks: 'all', name: 'app', test: /\.css$/ },
        vendor: {
          chunks: 'initial',
          name: 'vendor',
          test: /node_modules\/(?!(@product)).*\.js$/,
        },
      },
    },
  },
  plugins: [
    new CompressionPlugin({
      algorithm: 'brotliCompress',
      filename: '[path][base].br',
      minRatio: 0.8,
      test: /\.(js|css|html|svg)$/,
      threshold: 1024 * 244,
      compressionOptions: {
        level: 11,
      },
    }),
  ],

The Final Numbers

After we made the improvements (see the blue lines below), the user experience got better. This was especially true for users with weak connections. Here are the stats:

  • Dispersion decreased, pulling p75 and p90 closer to the median. The standard deviation is now under 2 seconds.
  • The average is now closer to the median (p50). This means a more consistent experience for users.
  • Peaks from p90 from 9s to 6s (-33%) and from p75 from 6s to 4.5s (-25%).

LCP during 1 week after the optimization segmented into percentiles with an average of 3 seconds for p50, 4.2 seconds for p75 and 5.5 seconds for p90 Figure 10: LCP distribution after optimization showing improved performance across percentiles.

Average and standard deviation of LCP during 1 week before the optimization with an average of 3.5 seconds and standard deviation stable at 2 seconds Figure 11: LCP average and standard deviation statistics after optimization showing improved consistency.

Here’s a summary of the changes in Largest Contentful Paint (LCP) metrics before and after optimization:

Metric Before Optimization (s) After Optimization (s) Improvement (%)
p50 (Median) 3,5 3 14% 📉
75th Percentile 5 4,2 16% 📉
90th Percentile 8,5 5,5 35% 📉 🔥
Average 4.5 3.5 22% 📉
Standard Deviation 3 - 9 2 67% 📉 🔥

These improvements show how optimizations lower LCP at different percentiles. They also boost performance consistency.

Finally, the next graph shows the improvement in the Grafana Faro LCP chart. This change matches the 75th percentile recommended by Google Web Core Metrics. Sure, the stats shared earlier give a better picture of the gains, but hey, this graph is still pretty cool.

LCP percentile-75 with a step of hours after with a step after applying the optimizations due to the average and volatility reduction Figure 12: Grafana Faro LCP chart showing the 75th percentile improvement after optimization.

What’s Next

Here are some actions we may consider for the future:

  • Defer loading of heavy libraries: We can delay loading libraries that aren’t needed at the start. This dynamic loading will improve the initial loading time.
  • Utilize a CDN: A Content Delivery Network (CDN) offers standard configurations for compression. It also enhances caching and provides shorter roundtrips, no matter where they are.
  • Analyze unused JavaScript and CSS: Chrome DevTools includes a tool called Coverage. It helps identify where tree shaking is ineffective or where we can import functionality dynamically. Removing unused code will reduce asset size, enhancing load times and runtime performance.
  • Switch to Server Side Rendering (SSR) to improve performance. It pre-renders HTML on the server before sending it to the client. This approach cuts down on the time needed for the browser to display the largest visible content, improving metrics like LCP.

Conclusions

This article outlines Cabify’s method for improving the performance of a Single Page Application (SPA) in mobile and web apps. Here are the key steps we took in our optimization journey:

  • Implement Metrics and Monitoring Early: Focus on early monitoring, not just optimization. Start collecting logs and metrics right away, even if performance seems fine. This helps create a baseline and spot potential issues before they affect users.
  • Establish SLOs and Automated Alerts: While publishing metrics is valuable, establish Service Level Objectives (SLOs) and automated alerts to eliminate manual checking after each deploy. Consider “shifting left” by monitoring asset sizes in CI/CD pipelines to catch performance regressions before they reach production.
  • Understand Your Users: Learn about your users and their challenges. Be careful with average data, as it can hide problems faced by some users. Use segmentation techniques, like percentile analysis, to find these hidden issues.
  • Audit Performance: Perform a detailed performance audit by choosing metrics that match the problems you want to solve. This ensures that your efforts target the most critical areas.
  • Implement Improvements and Measure Results: After making changes, check their impact on performance. Assess if the new complexity is worth the performance improvements. This keeps a balance between optimization and maintainability.

By using data, we solved immediate performance issues and set the stage for ongoing improvement. As technology and user needs change, so should our approach. Keep measuring, keep learning, and let data drive your next step!

Carlos Torres

Senior Software Engineer

Choose which cookies
you allow us to use

Cookies are small text files stored in your browser. They help us provide a better experience for you.

For example, they help us understand how you navigate our site and interact with it. But disabling essential cookies might affect how it works.

In each section below, we explain what each type of cookie does so you can decide what stays and what goes. Click through to learn more and adjust your preferences.

When you click “Save preferences”, your cookie selection will be stored. If you don’t choose anything, clicking this button will count as rejecting all cookies except the essential ones. Click here for more info.

Aceptar configuración