How we made our Angular application four times faster

There are many factors affecting the performance of a web application. What I'll be describing here is how we architected our Angular Application and then how we improved the performance of our web application.

Our Angular Application Architecture:

We treat the URL as a single source of the truth. Here, the single source of truth means that all data required to render a particular UI can be derived from the URL itself. We have route resolvers which are used to fetch the data required to render UI for the URL.

For every routed component, we have a container component which reads data from the URL and passes this data further down to presentational components. Every time the URL changes, the resolvers are executed again. This makes sure that we always have fresh data.

Whenever we need to update the displayed data, we re-navigate to the same URL.

No alt text provided for this image

We saw that this particular architecture is easy to reason and simpler (compared to state management, ngrx, akita, etc) to implement which resulted in a cleaner codebase. The problem with this approach is that whenever we need data, we fetch it directly using the HTTP rest call, which made us do many redundant rest calls.

One obvious solution we see to this problem is introducing some global state so that the data is fetched once and then accessed by all components. It does work but state management introduces a level of indirection which we wanted to avoid. So, to keep things simple, we decided to introduce application-level caching.

Introducing Ifn-Cake

We developed an angular library called?ifn-cake for client-side HTTP caching. This library is inspired by?swr?and?react-query?but is idiomatic to Angular.

We create an in-memory cache Using Map. The key for the Map is generated by hashing the arguments passed to a Query decorated method. The cache also has an expiration time for cached values.

No alt text provided for this image
No alt text provided for this image

@IfnCache, @IfnQuery and @IfnMutations are decorators exported by our ifn-cake library.?

We implement the caching at the service level where we divide the methods into two parts.

  • Queries (get, getAll, getById, findAll etc)
  • Mutations (create, update, patch, delete, put, etc)
  • All queries are cached by default.
  • The cache of a particular service is invalidated whenever we call any method decorated as @Mutation.
  • We automatically fetch the new data and update the cache for a particular query whenever that query is invalidated.

In this example, we have used a couple of decorators to implement caching.

@IfnCache('post')

This decorator tells ifn-cake that this service should be cached and the unique name for this service is "post".

@IfnQuery()

This decorator is doing quite a bit; it tells ifn-cake that this method is a FETCH request and the result should be cached. It turns the returning observable into the observable stream. What it means is that the returning observable will:?

  • If the data is already cached then emit the cached data
  • If it's not cached then do the actual rest call, cache it, and emit the cached value
  • If any mutation/invalidation happens then it should automatically do a new rest call to fetch the updated data, update the cache and the emit the cached value

@IfnMutation()

This decorator tells ifn-cake that it should invalidate the cache of PostService whenever the decorated method is called. Internally, this decorator creates a proxy of the decorated method and calls the original method only if we don't have the data cached for the supplied arguments.

It could happen that a mutation in one service also invalidates the data of another service.

For example:

If you add a comment to a post then the post itself should be invalidated.

For such scenarios, we can use @IfnCache decorator to also specify dependent services.

No alt text provided for this image

It will ensure that any Mutation in CommentService is also invalidating the PostService. Specifying all these dependent services could be challenging and there is a chance that we miss a case which could result in stale data being visible on the UI.?

To avoid such scenarios, we developed another library?ifn-cake-visualizer?which helps us visualize the dependencies between services.

No alt text provided for this image

Impact

To measure the impact of caching on our UI, I wrote an e2e test which goes through all the UIs in our application. Running the test with caching enabled was on average four times faster than running the test without caching.

Cache Storage

Our default implementation of cache storage stores the data in the memory itself.

The cache has a hierarchical structure which is implemented using nested Maps. We also store the timestamp of the query so that we invalidate the cache after the expiration period.

No alt text provided for this image
No alt text provided for this image
No alt text provided for this image

This structure helps us cache and invalidate queries granularly.

We hope you found this article informative and insightful. Do leave a comment letting us know your thoughts!?

-

Created by Shivam Dwivedi

Pushkar Adhikari

Backend Dev @Shape Games | Master's @DTU | Java | Spring | Kotlin | Angular

2 年

Kudos Shivam! An insightful approach, and the article is pretty crisp and well written. ?? Philipp has an interesting question in the comments and I look forward to the part 2 of the article in this series addressing it. ??

Philipp Oliver

Engineer and Co-founder of Intellilog CSV Specialist

2 年

Shivam Dwivedi, You mention that the caches of dependent services will be invalidated if a method is called, which is decorated with @IfnMutation(). I understand the reasoning behind it, as the mutation changes the DB. However, I am wondering how you deal with changes that are performed by "others". E.g other users or even batch processes.

回复

要查看或添加评论,请登录

Neptune Ubicom Pvt. Ltd.的更多文章

社区洞察

其他会员也浏览了