Angular v11 was released a couple of weeks ago. One of the highlights in this release is making it easier to enable Hot Module Replacement (HMR) during the development of our apps. All we need to do is use the --hmr
flag:
ng serve --hmr
To quote the release post:
Now during development the latest changes to components, templates and styles will be instantly updated into the running application. All without requiring a full page refresh. Data typed into forms are preserved as well as scroll position providing a boost to developer productivity.
I was excited to try it! I quickly installed the newest Angular CLI and generated a fresh new app.
My initial reaction was quite positive. HMR works like magic!
But then I began to wonder how will a more complex app behave with HMR enabled? I asked this question in the Angular’s Discord channel and got a really good explanation by Lars Gyrup Brink Nielsen. To quote:
If the application hasn’t been built with Hot Module Replacement in mind from the beginning, it might need some work. The issue with HMR is when application state gets stale or memory leaks occur. This can happen for application- and platform-wide dependencies. We usually don’t think about cleaning up resources such as RxJS subscriptions, open Websockets, and so on at this level. But when we use HMR, the AppModule and all singleton services are asked to be destroyed. If the code doesn’t account for this, the same side effects can be triggered/active multiple times which causes different things to get out of sync.
Really good point!
Enabling HMR requires a different mindset. It emphasizes the need to be careful with long-lived RxJS subscriptions, setInterval
functions, WebSockets connections, etc., while developing our apps. On top of that, we must also keep in mind that this behaviour occurs only in development.
Let’s illustrate the problem.
Say I have this code in AppComponent
(which is a long-lived component that doesn’t get destroyed throughout the “live” of the app):
@Component({ ... })
export class AppComponent {
ngOnInit() {
interval(1000).subscribe(value => {
console.log('value', value);
});
}
}
Running the app with --hmr
enabled will result in this:
Here I have an RxJS subscription that logs values to the console. The subscription is not cleared but that shouldn’t be a problem since the component is never going to get destroyed. So far everything works as expected.
Now, if I change the code a bit and save the file, the app will not rebuild again and force a full page refresh in the browser, as we’re used to. Rather, it will only rebuild the parts that were modified and replace them in the running app:
But now the console shows logs from multiple subscriptions. Why is that? It is because of old subscriptions that are still active in the background, effectively creating a memory leak. This would not have been a problem without HMR because the app would’ve been rebuild again and forced full browser page refresh (which in turn destroys all previous subscriptions).
It’s important to emphasize here again that the code above will run as expected in production. There will be only one active subscription. This problem occurs only in development with HMR turned on.
To fix the issue, we must remember to clear the subscription in the ngOnDestroy
hook for that component.
@Component({ ... })
export class AppComponent {
sub: Subscription | undefined;
ngOnInit() {
this.sub = interval(1000).subscribe(value => {
console.log('values', value);
});
}
ngOnDestroy() {
this.sub?.unsubscribe();
}
}
After this change, saving the file multiple times doesn’t result in old subscriptions logging to the console because they are properly cleared.
Summary
I love HMR!
It’s exciting, works great and improves the developer experience. However, it doesn’t come without a cost. Enabling HMR requires a slight change in mindset when developing our applications. We must remember to:
- clear long-lived RxJS subscriptions
- clear
setInterval
functions - close WebSocket connections
- properly manage app- and platform-wide dependencies (like componens and services)
Failing to do so, might result in unexpected results and memory leaks, which can be hard to debug.
Is there something else we should be aware of when HMR is turned on?
Photo by Philip Brown on Unsplash
Top comments (9)
Thanks Stephen.
I agree that HMR is a great feature. I've been working on an Angular app with HMR enabled for a few months now. HMR has probably saved me some valuable time. In terms of doing something because of HMR, I can remember a couple of places where I had to clear a subscription myself in long-lived component(s)/service(s) in order to not introduce memory leaks in development. So that is something one needs to be aware of and actively do something about it. Other than that, as long as subscriptions are handled either by the
async
pipe or some other way, everything should be working just fine.Something else I've noticed is that there's some issue when HMR is enabled in a app that uses Angular Material and NgRx but I haven't fully figured it out yet. It's related to those components that use input fields like the date picker. It could as well be an issue on my end. Don't know yet 🤷♂️🙂
What do u think . If I use a subscribe in service?
There's no problem with that. Services have
ngOnDestroy
hook as well. You can use it for clean up. coryrylan.com/blog/using-ngondestr...That's good advice. Just don't try to use ngOnInit. It doesn't work for services. Instead, use the service constructor or an Angular initializer.
I actually think that HMR is good way to make sure code has been properly crafted. If there are memory leaks, this is not the fault of HMR, but the fault of poorly built code.
I see HMR going both ways. It's definitely a good idea to properly clean subscriptions but in some cases it's done only to please the tooling and not because of a bigger benefit. But I'm definitely enabling HMR in all my apps. It has saved me a lot of hours which would've went waiting for my app to simply reload.
Thanks dzhavat.
You can use HMR with lazy modules?
Hey Luiz,
Yes, it's working with both lazy and non-lazy modules. But keep in mind that HMR is only used during development to make the developer experience (DX) better.
Great post, just what I was looking for. I'm gonna give HMR a try myself, looks interesting and when you have complex flows, it can be a time-saver not having to reload all the time.
Thanks!