Enhancing single-page application performance: Strategies for improved user experience

Freshworks developed the web application Freshchat as a single-page application (SPA). SPAs are designed to initially fetch a document and then sequentially download all their associated assets.

This design choice can lead to suboptimal application performance, as the rendering speed relies heavily on various factors within the client’s web browser, such as the CPU, GPU, network latency, and more.

There are several strategies to address this performance challenge, with the most prominent one being to minimize the amount of code sent to the browser. Some other effective approaches include:

  1. Code splitting and dynamic imports of third-party libraries: Break down the code into smaller, manageable chunks and only load the necessary parts when they are required, particularly for third-party libraries.
  2. Utilizing lightweight libraries as an alternative to heavier ones: Opt for smaller and more efficient libraries rather than those that are bloated or resource-intensive.
  3. Leveraging browser-native web APIs instead of polyfills: Instead of relying on polyfills to bridge compatibility gaps, use the native web APIs supported by modern browsers.
  4. Configuring build tools to ship ES6 code: Ensure that your build tools are set up to deliver code in the ES6 format, taking advantage of its improved efficiency and features.

By implementing these strategies, Freshchat enhanced its performance and responsiveness, providing a smoother user experience while optimizing resource utilization.

Ways to reduce the bundle or asset size of an application

Code splitting

Within an SPA, assets are categorized into two types: application code and vendor code. These two files are typically downloaded and executed sequentially, causing the user interface to block rendering, which degrades the user experience. This leads to increased first contentful paint (FCP) and time to first byte (TTFB).

In our efforts to reduce the size of these assets, we’ve leveraged code splitting, which enables breaking down the code into smaller, more manageable segments. This approach allows us to transmit only the code required for the initial rendering of pages. For subsequent changes in routes, additional code chunks are retrieved from the server. As a result, this strategy significantly enhances both FCP and TTFB, ultimately improving the user experience.

In our Freshchat Messaging widget, we were able to achieve this code splitting with the help of Embroider from the Ember framework. Below are the results.

Before code splitting

code splitting, user experience, freshworks engineering

After code splitting

code splitting, user experience, freshworks engineering

Dynamic imports

Dynamic import is an ECMA Stage 4 feature that enables on-demand loading of scripts.

In our system, specific third-party libraries are loaded during the initial rendering process. This not only consumes a significant amount of bandwidth but also extends the rendering time. By implementing the Ember auto-import add-on, we harness the capabilities of dynamic imports, which enhance rendering speed by loading third-party assets only when they are needed.

This approach has proven highly effective in reducing asset sizes within both the agent portal and web widget components.

In our agent portal, we have reduced the Vendor JS size by around 250 KB by dynamically importing libraries like Highcharts JS, Medium Editor, Lunr JS, and internalization language files.

Outcome of dynamic loading:

No. Library Size reduced in Vendor JS 
1. Highcharts 295 KB (95 KB compressed)
2. en-us language file for internalization  470 KB (125 KB compressed)
3. Medium Editor 102.4 KB (26.KB compressed)
4. Lunr 40.96 KB (8.76 KB compressed)

Using lightweight third-party libraries

Both of our applications contain libraries that are sizable and require substitution.

Moment JS

Moment JS, a valuable date-time library that has been in use for a decade, has played a crucial role. However, it comes with a sizable footprint of 70 KB, contributing to increased bundle sizes.

To address this, we’ve made the transition from Moment to Day JS, a lightweight alternative that weighs in at just 2 KB. This shift has significantly reduced bundle sizes.

We also incorporated INTL native web APIs whenever they are needed for specific date manipulation tasks.

To format a date with specific locale, we can do something like this:

JavaScript:

console.log(new Intl.DateTimeFormat('en', {weekday: 'short'}).format(date))

// This will print current day in shorter format

JavaScript:

console.log(new Intl.NumberFormat('en-US').format(100000))

// This will print '100,000' according to en-us format

jQuery

In the days of Internet Explorer, jQuery emerged as a lifeline for web developers. This DOM utility library simplified the process of writing cross-browser compatible code, shielding developers from the intricacies of different browsers’ implementations.

However, as we transitioned into the modern browser era, the necessity for jQuery diminished. Modern browsers offer comprehensive native support for all DOM-related functions.

This transition to native functionality is not only a welcome change but also results in a significant reduction in bundle size, shedding 30 KB from the application.

Lodash

Lodash, a utility library, provides an array of functions such as cloning, mapping, and currying. It’s worth noting that this library can be substituted with plain JavaScript code, offering an alternative approach where Lodash is not required.

Lodash is quite substantial in size and should be used judiciously, as it has the potential to significantly increase the bundle size. While Lodash does support tree shaking with its ES6 modules, it’s still preferable to rely on native ES6 standards readily available in modern browsers, eliminating the need for this heavy library.

We are currently in the process of transitioning away from Lodash in our applications. As an initial step, we’ve implemented ESLint rules to discourage the use of Lodash in favor of plain functions.

Using browsers’ native web APIs

Polyfills represent code snippets designed to extend modern functionality to older browsers that lack native support for these features. However, this convenience comes at the expense of increased bundle size.

For instance, we can leverage native Fetch for handling HTTP requests instead of relying on a polyfill. Internally, we’ve crafted a native Fetch wrapper class that streamlines the process, adding essential headers and options for simplifying API calls.

Similarly, for date manipulation tied to specific locales, we can tap into the native internationalization APIs (INTL) instead of resorting to polyfills.

In our application, we strongly recommend the use of native web APIs while avoiding unnecessary polyfills. This reduces code bloat that is transmitted to the browser.

Using build tools configuration

Within our web applications, we use Ember CLI for application compilation, and it offers the flexibility to target either modern or evergreen browsers.

By selecting the appropriate targets, we can deliver the most up-to-date ES6 code to the browser. This has several benefits:

  1. Reduced code shipment: The omission of polyfill support for older browsers results in a more concise codebase, leading to reduced file sizes
  2. Enhanced parsing efficiency: Fewer imperative code constructs expedite parsing time within the browser. A notable example is the use of async/await. In cases where support for older browsers is necessary, Babel typically employs regenerator-runtime to handle generators and async/await, leading to code bloat and slower parsing times in the client browser
  3. Improved developer experience (DX): The resulting code is more readable and intuitive, as it eliminates the need for jump and loop statements generated by regenerator-runtime. This enhances the overall developer experience

build tools configuration, user experience, freshworks engineering

Updating caniuse package

While this might be considered a relatively simple optimization, it often remains in the shadow of other performance-related challenges within our application. By ensuring regular updates to the caniuse database, our build tools can access up-to-date statistics on browser usage and adjust asset compilation accordingly.

This proactive approach yields significant benefits, resulting in bundle size reductions from 10 to 100 KB. The frequency of these savings is determined by how regularly these packages are updated.

In conclusion, Freshchat’s journey in addressing performance challenges within its SPA has been marked by a proactive commitment to optimizing asset delivery. 

Freshchat’s dedication to these strategies exemplifies its commitment to providing a more responsive and efficient SPA, showcasing a relentless pursuit of excellence in web application development.