Performance Optimization: Reducing FCP and LCP by Over 50%

As part of an initiative to enhance user experience, I focused on enhancing the web performance of our application. Initially, the web app had the following performance metrics:

  • First Contentful Paint (FCP) : 8.1 seconds (on mobile)
  • Contentful Paint (LCP): 17.0 seconds (on mobile)

As it's often said, there's no need to worry too much about the mobile score in PageSpeed Insights. The score can change with each measurement, and mobile tests are not measured under optimal network conditions or device specifications. Also, the score doesn't affect SEO directly.
However, these values indicated significant room for improvement, especially when considering that optimal web performance targets for FCP are under 2.5 seconds, and LCP should ideally be under 2.5 seconds as well.

Results

- Moblie Metrics

Before
After

- Desktop Metrics

Before
After

Detailed Breakdown of the Optimization Process

The web app is built using the following technologies.

  • TypeScript for type safety and improved code maintainability.
  • Nuxt.js (SSR - Server-Side Rendering) for efficient page rendering.
  • Vue.js for the front-end framework.
  • Vuetify, a Vue.js component library, for building modern and responsive user interfaces.
  • Chart.js for data visualizations and charts.
  • GraphQL for querying data.
  • Sentry for error tracking.
  • Material Icons for icons (originally using fonts, then switched to SVGs).
  • Google Tag Manager for managing and deploying marketing tags.

- Identifying the Bottlenecks

To pinpoint the areas slowing down the page load, I used several tools to analyze the performance bottlenecks:

  • Lighthouse
  • PageSpeed Insights
  • Chrome Performance Tab
  • nuxi analyze

These tools helped me identify issues like large JavaScript bundles, render-blocking resources, and inefficient image handling. With a clearer understanding of where the performance was lagging, I set out to implement a series of optimizations.

Key Optimizations Implemented

- Optimizing Material Icons: Font vs SVG

One of the main bottlenecks I identified during performance optimization was related to Material Icons. Initially, we were using the Material Icons font in our Nuxt.js app, and the Google Fonts library was preloaded to ensure quick loading of the icons. At first glance, this didn’t seem like a major issue since Chrome's Performance tab showed that loading the font took around 581ms. While 581ms is a noticeable delay, I didn't think it would be a major contributor to the overall 8-second load time.

However, despite the seemingly minor load time, the actual impact on performance was significant. While it didn’t show up as a major issue in the performance tool metrics, the font rendering was blocking other important resources, slowing down the overall page load. This issue was subtle and not immediately obvious, making it one of the more hidden bottlenecks.

After further investigation and testing, I decided to replace the Material Icons font with SVG icons. The SVG format offers several advantages, especially for icons:

  • No network request for font files: Unlike fonts, which require an additional request to fetch and render, SVGs are often inlined directly in the HTML or loaded from a lightweight source, reducing HTTP requests.
  • Faster rendering: SVGs are rendered directly by the browser and don’t rely on the browser's font rendering engine, which can add delays.

After switching to SVGs, the results were impressive. The page load time improved significantly, reducing FCP (First Contentful Paint) by approximately 3 seconds. This was a substantial improvement, considering that the initial font rendering, which seemed insignificant, was actually a major contributor to slow load times.

For those interested in implementing SVGs for Material Icons, I followed the approach outlined in Vuetify’s guide on icon fonts, which provides a straightforward way to switch from icon fonts to SVGs without complicating the implementation.

- Dynamically Importing and Rendering Heavy Components

Our app primarily uses SSR (Server-Side Rendering), but certain components, such as those using Chart.js, were quite heavy and significantly impacted the initial load time. On mobile, Chart.js added around 300ms to the load time and 30KB to the initial JavaScript bundle, negatively affecting overall performance.

Since Chart.js uses Canvas, which doesn’t impact SEO (Search Engine Optimization), I switched Chart.js to CSR (Client-Side Rendering) by dynamically importing it only when needed, after the initial page load. This ensures that Chart.js and other heavy components don't block the initial SSR render, reducing the upfront JavaScript payload and improving perceived performance.

By offloading these components to client-side rendering, I was able to speed up the page load and keep the app's initial rendering fast and efficient, without sacrificing functionality.

- Reducing the Initial JS Bundle Size

To improve our app's performance, I focused on reducing the size of the initial JavaScript bundle. One area I optimized was the integration of Sentry, which was importing the entire module by default, even though we only needed specific functionality for error tracking. While this wasn’t the biggest contributor to the bundle size, it added around 30KB to the initial load.

To address this, I imported only the necessary parts of the Sentry module. This reduced the initial bundle size, improving the app's loading speed. While the reduction wasn’t huge, every bit counts when optimizing performance. This small improvement helped reduce the upfront JavaScript payload.

- Preloading LCP Images

One of the major contributors to the Largest Contentful Paint (LCP) was an image that was fetched dynamically based on the screen width. This dynamic fetching prevented me from preloading the image, causing unnecessary delays in loading.

After discussing the issue with the marketing team, we agreed to stop switching the image based on screen width. This change allowed us to preload the image, significantly improving load times. As a result, we reduced the LCP by as much as 500ms, greatly enhancing the app’s overall performance.

Additionally, by ensuring the image was properly preloaded and avoiding layout shifts, we reduced the Cumulative Layout Shift (CLS) to 0, eliminating layout instability during page load and further improving the user experience.

- Lazy-Loading Non-Essential Images

I implemented lazy loading for all images except those related to LCP. This ensured that images outside of the viewport wouldn't block the rendering of critical content.

- Switching from Gzip to Brotli

As part of the optimizations, I switched from Gzip compression to Brotli for compressing stylesheets and script files. Brotli is a more efficient compression algorithm that typically results in smaller file sizes, helping to reduce the time required to load these resources. While the change may not have the largest impact, it still contributes to improving both FCP and LCP by reducing data transfer times.

- Refactoring Functions to Remove Unnecessary Nested Loops

Some functions were performing unnecessary computations due to inefficient loops, resulting in an O(n²) time complexity. By refactoring these functions to eliminate nested loops, I reduced the overall execution time of the app.

Results: Significant Performance Gains

After implementing these optimizations, I was able to significantly improve the web app’s performance metrics:

  • FCP was reduced from 8.1 seconds to 3.5 seconds.
  • LCP was reduced from 17.0 seconds to 4.4 seconds.
  • Cumulative Layout Shift (CLS) was reduced from 0.118 seconds to 0 seconds, resulting in a more stable layout and a smoother visual experience for users.

- Bundle sizes

Before
After

- how UX changed in slow environment

While it may not be immediately clear and seems to change dynamically at first glance, we can tell that the CLS now takes 0 seconds and it doesn't have to wait for google fonts to load. The screen no longer feels laggy and is much smoother now.

*Both are deployed in staging environments with slow network settings, and the HTML file is not compressed, making them significantly slower than the production app.

Before

After

Conclusion

Through these optimizations, the web app now provides a faster, smoother user experience, with performance metrics significantly improved, though still working toward ideal thresholds. This project reinforced the importance of performance-driven development and showed how even small, targeted changes can lead to substantial improvements. Whether it's reducing JavaScript bundle sizes, optimizing images, or rethinking resource loading strategies, every optimization contributes to creating a better, faster web.