Effortless Angular Optimization: Insights from Halodoc Web Development

Web Development Jan 6, 2025


In the world of modern web development, performance is critical to the success of any application. Users expect fast and responsive experiences, and even a slight delay can lead to high bounce rates and low engagement. Angular, while a powerful framework, requires deliberate performance optimisation to ensure applications run efficiently. In this blog, we will explore the following techniques used at Halodoc to optimise Angular web applications:

  • Deferrable View
  • Standalone Components & SSR Hydration
  • Methods to optimise Bundle Size
  • Management of Change Detection
  • Optimisation of CSS and Styling
  • Usage of Web Workers
  • Lazy Loading

1. Deferrable View

Introduced in Angular 17, the @defer directive is designed to enhance performance by deferring the loading of components that are not immediately visible or required. It can be used to load parts of the UI after the initial view is rendered or based on user interaction, such as scrolling.

Components must be standalone, as non-standalone dependencies cannot be deferred and will be eagerly loaded, even if included within @defer blocks. Additionally, they cannot be referenced outside of @defer blocks within the same file. Any references made outside the @defer block or within @ViewChild queries will result in the dependencies being eagerly loaded.

How to use @defer:

Deferred View Implementation

Configure @defer to load components conditionally:

  • When in view: Trigger loading when the component scrolls into the viewport.
  • On interaction: Load components when the user performs an action, such as clicking a button.
Deferrable View implemented in Halodoc Website: https://www.halodoc.com/

Why it helps: This approach minimises the time it takes to render the initial UI by deferring non-essential content until it is needed. This reduces the time-to-interactive (TTI) metric, improving user-perceived performance. More detailed implementation can be found here.

Impact observed: Improvement in First Contentful Paint (FCP), Time to Interactive (TTI), Cumulative Layout Shift (CLS)

2.  Standalone Components & SSR Hydration

Standalone components refer to components that are not dependent on other components or modules for their functionality. These components are self-contained and can be loaded independently, which makes them ideal for performance optimization strategies such as lazy loading or the @defer directive.

Why standalone components matter:
Standalone components are essential for optimizing the loading of Angular applications. Non-standalone components, which rely on other components or services, can hinder deferred loading because they can't be independently loaded. If such components are placed inside a @defer block, they are still eagerly loaded, negating the performance benefits of deferring their initialization.

Implementation of Standalone Component

Key benefits:

  • Improved performance: Standalone components can be lazily loaded, reducing the initial load time and improving application responsiveness.
  • Simplified maintenance: As standalone components are decoupled from other parts of the application, it becomes easier to maintain and test them individually.
  • Better user experience: By loading only essential components upfront, you reduce the app's time-to-interactive (TTI), providing a smoother experience for users.

Hydration enhances application performance by eliminating the need to recreate DOM nodes. Instead, Angular attempts to match the existing DOM elements with the application's structure at runtime, reusing DOM nodes whenever feasible.

How to implement SSR Hydration:

  • Enable SSR: First, ensure that SSR is enabled for your Angular application.
  • Add provideClientHydration(): This function should be added to the provider list in app.config.ts or app.module.ts to initiate the hydration process.
  • Ensure valid HTML structure: Make sure the server-rendered HTML is valid and matches the structure expected by the Angular application.
  • Handle Zone.js dependencies: Zone.js plays an essential role in Angular’s change detection and must be configured correctly to avoid issues during hydration.
  • Configure transfer cache (optional): You can configure a transfer cache to pass API responses from the server to the client, reducing redundant data fetching.
  • Use ngSkipHydration: For components that cannot be hydrated (such as those with dynamic content or complex states), use ngSkipHydration to skip hydration for those components.

Why it helps: Hydration eliminates flickering, reduces time to interactivity, and reuses server-rendered DOM, improving user experience. While challenges like DOM mismatches and unsupported i18n exist, proper testing ensures seamless implementation. More details can be found here.

Impact observed: Improvement in Largest contentful Paint (LCP), Total Blocking Time (TBT), First Input delay (FID)

3. Optimise Bundle Sizes

One of the main culprits of poor performance in Angular applications is large JavaScript bundles. When too much code is shipped to the client, it can slow down loading and increase processing times. Here are a few strategies to reduce bundle sizes:

  • Tree Shaking: Angular's build system automatically removes unused code during production builds. To maximise the benefits of tree shaking, avoid importing unnecessary libraries or entire libraries when only a few functions are needed.
    Example: Instead of importing the entire lodash library, only import the specific function you need.
  • Use Differential Loading: Differential loading allows you to ship modern JavaScript (ES2015+) to browsers that support it and a smaller ES5 bundle to legacy browsers. This reduces the bundle size for modern browsers, leading to faster load times.
  • AOT Compilation: AOT compiles your Angular templates during the build process, reducing the size of the app and improving performance.
    By default, Angular applications use Just-in-Time (JIT) compilation during development, where templates and components are compiled in the browser. While JIT is convenient for development, it increases the size of your JavaScript bundles in production.
    From Angular 9 onwards, AOT is enabled by default for production builds, bat it may need to be manually enabled for development builds if required.
Explicit configuration of AOT in angular.json
  • Angular Material and CDK: If you're using Angular Material or the Angular CDK, import only the components you need instead of the entire library.

4. Change Detection Management

Angular’s powerful change detection system is one of the core features of the framework. However, inefficient management of change detection cycles can lead to performance bottlenecks, especially in complex applications. By default, Angular checks for changes throughout the entire component tree whenever an event occurs.

This can be optimised in a few ways:

  • OnPush Change Detection Strategy: The OnPush strategy tells Angular to only check for changes in a component when one of its inputs changes. This reduces the number of change detection cycles, particularly in components with complex data structures.  
    Example:
implementation of onPush
Implementation of onPush
  • Detaching Change Detection: For certain scenarios where you need fine-grained control, you can manually detach and reattach change detection. This is useful for components that update infrequently.
    Example:
Detach and Reattach Change Detection manually

Impact observed: Improvement in Component Render Time, Frame Rate, Re-render Frequency, Response Time

5. Optimise CSS and Styling

  • Minimise CSS with PurgeCSS: Unused CSS can be removed from a project using tools like PurgeCSS, which is particularly effective when working with utility-first frameworks such as Tailwind CSS.
    PurgeCSS analyses the content and CSS files, matching the selectors used in the files with those present in the content files. It eliminates unused selectors from the CSS, resulting in smaller and more optimized CSS files.
purgecss.config.js
purgecss configuration in package.json
  • Use Tailwind CSS for Utility-First Styling: Tailwind CSS minimizes the need for custom CSS, optimizing the CSS bundle size and improving reusability. Additional information about migrating from Angular Flex to Tailwind at Halodoc is available here.
  • Scope Styles Carefully: Avoid global CSS as much as possible. Use Angular’s component-level styling to ensure CSS is scoped and isolated, reducing potential conflicts and reflows.

Impact observed: Improvement in Page Load Time, First Contentful Paint (FCP), Largest Contentful Paint (LCP)

6. Using Web Workers

Web Workers enable the execution of computationally intensive tasks in a background thread, keeping the main UI thread free and responsive. This is particularly beneficial for applications that require heavy CPU processing.

Example use cases: Complex calculations, data processing, or heavy animations.

Fetch Products API call from Service Worker
Fetch Products API call to server

How It Works:

  • The service worker first returns a cached response (if available) to ensure quick loading.
  • Simultaneously, it makes a network request to fetch fresh data, which is then updated in the cache for future use.

Why it helps: A service worker helps while fetching products of a specific category by acting as a middle layer between the application and the server, enabling optimized data retrieval, faster responses, and enhanced user experiences.
Refer  Angular Web Worker  for more details on web worker.

Impact observed: Improvement in Main Thread Responsiveness, Frame Rate (FPS), JavaScript Execution Time, CPU Utilisation

7. Implementing Lazy Loading for Modules

Lazy loading is one of the most effective ways to improve the initial load time of Angular applications. By breaking down the application into smaller chunks and loading them only when needed, the time taken for the app to be interactive can be reduced significantly.

How it works:

  • Instead of loading all modules upfront, you define which modules should be loaded lazily based on user navigation.
  • Angular's router supports lazy loading out of the box, using the loadChildren property in route definitions.

Implementation:

Lazy Loading


Why it helps: Lazy loading ensures that only the necessary code is loaded initially, reducing the size of the main bundle and speeding up the rendering of critical components. This is especially important for large-scale applications with many features or complex UIs.

Impact observed: Improvement in Initial Load Time, First Contentful Paint (FCP), Bundle Size.

Conclusion

Performance optimisation is an ongoing process, but by implementing these strategies, you can significantly improve the responsiveness and scalability of your Angular applications. The techniques discussed in this blog—such as lazy loading, the @defer directive, bundle size optimization, and efficient change detection—are powerful strategies used at Halodoc to enhance Angular web applications. By adopting these practices, developers can ensure that their applications load faster, respond more efficiently, and provide a better overall experience for users.

References

Join us

Scalability, reliability and maintainability are the three pillars that govern what we build at Halodoc Tech. We are actively looking for engineers at all levels and  if solving hard problems with challenging requirements is your forte, please reach out to us with your resumé at careers.india@halodoc.com.

About Halodoc

Halodoc is the number 1 all around Healthcare application in Indonesia. Our mission is to simplify and bring quality healthcare across Indonesia, from Sabang to Merauke. We connect 20,000+ doctors with patients in need through our Tele-consultation service. We partner with 3500+ pharmacies in 100+ cities to bring medicine to your doorstep. We've also partnered with Indonesia's largest lab provider to provide lab home services, and to top it off we have recently launched a premium appointment service that partners with 500+ hospitals that allow patients to book a doctor appointment inside our application. We are extremely fortunate to be trusted by our investors, such as the Bill & Melinda Gates Foundation, Singtel, UOB Ventures, Allianz, GoJek, Astra, Temasek, and many more. We recently closed our Series D round and In total have raised around USD$100+ million for our mission. Our team works tirelessly to make sure that we create the best healthcare solution personalised for all of our patient's needs, and are continuously on a path to simplify healthcare for Indonesia.

Monika K L

SDE </> at Halodoc