DEV Community

Cover image for The Silent Killer, Angular Memory Leaks: How Dynamic Components Are Slowly Destroying Your Angular App's Performance
Rajat
Rajat

Posted on

The Silent Killer, Angular Memory Leaks: How Dynamic Components Are Slowly Destroying Your Angular App's Performance

Why your perfectly working Angular app suddenly starts crawling—and how to fix it before users notice

Have you ever deployed an Angular app that worked flawlessly in development, only to have users complain about it getting slower and slower over time? You're not alone. There's a sneaky culprit that many Angular developers overlook: memory leaks from dynamic components.

Before we dive into the examples, a quick note: the code snippets provided here use syntax from earlier Angular versions.

Here's the thing that'll make you check your code right now: every time you create a dynamic component without properly cleaning it up, you're essentially creating a digital zombie that consumes memory forever. And unlike other bugs that crash your app immediately, memory leaks are silent assassins—they slowly drain performance until your users start abandoning your app.

What you'll learn by the end of this article:

  • Why dynamic components cause memory leaks (with real examples)
  • The exact cleanup pattern that prevents these leaks
  • How to write bulletproof unit tests for your cleanup logic
  • A bonus tip that most Angular devs miss

Quick question before we dive in: Have you ever noticed your Angular app getting sluggish after users interact with it for a while? Drop a comment—I'm genuinely curious about your experience!

The Problem: Dynamic Components That Never Die

Let's start with a real-world scenario. You're building a modal service that dynamically creates components:

@Injectable()
export class ModalService {
  private componentRef: ComponentRef<any>;

  openModal(component: any, data?: any) {
    // This creates a new component instance
    this.componentRef = this.viewContainerRef.createComponent(component);

    // Set up the component
    if (data) {
      this.componentRef.instance.data = data;
    }

    // Listen for close events
    this.componentRef.instance.onClose.subscribe(() => {
      this.closeModal();
    });
  }

  closeModal() {
    // The WRONG way - this is where the leak happens!
    this.componentRef.destroy();
  }
}

Enter fullscreen mode Exit fullscreen mode

Can you spot the problem?

Here's what's happening behind the scenes:

  1. You create a component dynamically
  2. You set up subscriptions and event listeners
  3. You call destroy() on the component
  4. BUT - the component reference itself is never cleared!

This means even though the component is "destroyed," JavaScript's garbage collector can't free up the memory because there's still a reference to it.

The Solution: Proper Cleanup Pattern

Here's the correct way to handle dynamic component cleanup:

@Injectable()
export class ModalService {
  private componentRef: ComponentRef<any> | null = null;

  openModal(component: any, data?: any) {
    // Clean up any existing modal first
    if (this.componentRef) {
      this.closeModal();
    }

    this.componentRef = this.viewContainerRef.createComponent(component);

    if (data) {
      this.componentRef.instance.data = data;
    }

    // Store subscription for cleanup
    const subscription = this.componentRef.instance.onClose.subscribe(() => {
      this.closeModal();
    });

    // Store the subscription reference for cleanup
    this.componentRef.instance._subscription = subscription;
  }

  closeModal() {
    if (this.componentRef) {
      // Clean up subscription first
      if (this.componentRef.instance._subscription) {
        this.componentRef.instance._subscription.unsubscribe();
      }

      // Destroy the component
      this.componentRef.destroy();

      // Clear the reference - THIS IS THE KEY!
      this.componentRef = null;
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

The magic happens in these two lines:

this.componentRef.destroy();
this.componentRef = null; //  This prevents the memory leak!

Enter fullscreen mode Exit fullscreen mode

Let's See It in Action: Complete Example

Here's a complete working example that shows both the problem and the solution:

// Dynamic component we'll create
@Component({
  selector: 'app-dynamic-modal',
  template: `
    <div class="modal-overlay" (click)="close()">
      <div class="modal-content" (click)="$event.stopPropagation()">
        <h3>{{title}}</h3>
        <p>{{message}}</p>
        <button (click)="close()">Close</button>
      </div>
    </div>
  `
})
export class DynamicModalComponent {
  @Input() title: string = '';
  @Input() message: string = '';
  @Output() onClose = new EventEmitter<void>();

  close() {
    this.onClose.emit();
  }
}

// Service that manages dynamic components
@Injectable()
export class DynamicModalService {
  private componentRef: ComponentRef<DynamicModalComponent> | null = null;

  constructor(
    private viewContainerRef: ViewContainerRef,
    private componentFactoryResolver: ComponentFactoryResolver
  ) {}

  openModal(title: string, message: string) {
    // Clean up existing modal
    this.closeModal();

    // Create new component
    const factory = this.componentFactoryResolver.resolveComponentFactory(DynamicModalComponent);
    this.componentRef = this.viewContainerRef.createComponent(factory);

    // Set up data
    this.componentRef.instance.title = title;
    this.componentRef.instance.message = message;

    // Listen for close event
    this.componentRef.instance.onClose.subscribe(() => {
      this.closeModal();
    });
  }

  closeModal() {
    if (this.componentRef) {
      this.componentRef.destroy();
      this.componentRef = null; // Prevent memory leak
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Writing Unit Tests for Memory Leak Prevention

Here's how to write unit tests that ensure your cleanup logic works correctly:

describe('DynamicModalService', () => {
  let service: DynamicModalService;
  let viewContainerRef: ViewContainerRef;
  let fixture: ComponentFixture<TestComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [TestComponent, DynamicModalComponent],
      providers: [DynamicModalService]
    });

    fixture = TestBed.createComponent(TestComponent);
    viewContainerRef = fixture.componentInstance.viewContainerRef;
    service = TestBed.inject(DynamicModalService);
    service['viewContainerRef'] = viewContainerRef;
  });

  it('should clear component reference after closing modal', () => {
    // Arrange
    service.openModal('Test Title', 'Test Message');
    expect(service['componentRef']).toBeTruthy();

    // Act
    service.closeModal();

    // Assert - This is the key test!
    expect(service['componentRef']).toBeNull();
  });

  it('should not throw error when closing non-existent modal', () => {
    // This tests defensive programming
    expect(() => service.closeModal()).not.toThrow();
  });

  it('should clean up previous modal when opening new one', () => {
    // Arrange
    service.openModal('First Modal', 'First Message');
    const firstComponentRef = service['componentRef'];
    spyOn(firstComponentRef!, 'destroy');

    // Act
    service.openModal('Second Modal', 'Second Message');

    // Assert
    expect(firstComponentRef!.destroy).toHaveBeenCalled();
  });
});

// Test component for testing
@Component({
  template: '<div></div>'
})
class TestComponent {
  constructor(public viewContainerRef: ViewContainerRef) {}
}

Enter fullscreen mode Exit fullscreen mode

If this testing approach just saved you hours of debugging, hit that clap button!

The Advanced Pattern: Using OnDestroy Hook

For more complex scenarios, implement the OnDestroy hook in your dynamically created components:

@Component({
  selector: 'app-advanced-dynamic',
  template: `<!-- your template -->`
})
export class AdvancedDynamicComponent implements OnDestroy {
  private subscriptions = new SubSink(); // Using SubSink library

  ngOnInit() {
    // Add all your subscriptions to SubSink
    this.subscriptions.add(
      this.someService.getData().subscribe(data => {
        // Handle data
      })
    );
  }

  ngOnDestroy() {
    // This automatically unsubscribes from all subscriptions
    this.subscriptions.unsubscribe();
  }
}

Enter fullscreen mode Exit fullscreen mode

Bonus Tip: Memory Leak Detection

Want to catch memory leaks during development? Here's a trick most Angular devs don't know:

// Add this to your service for development
private debugComponentCount = 0;

openModal(component: any, data?: any) {
  this.debugComponentCount++;
  console.log(` Active components: ${this.debugComponentCount}`);

  // ... rest of your code
}

closeModal() {
  if (this.componentRef) {
    this.componentRef.destroy();
    this.componentRef = null;
    this.debugComponentCount--;
    console.log(`🧹 Cleaned up. Active components: ${this.debugComponentCount}`);
  }
}

Enter fullscreen mode Exit fullscreen mode

If your debugComponentCount keeps growing, you've got a memory leak!

Recap: Your Action Plan

Here's what you need to do right now:

  1. Audit your dynamic components - Search your codebase for .createComponent() calls
  2. Check your cleanup logic - Make sure you're setting component references to null
  3. Add unit tests - Test that your cleanup logic actually works
  4. Use the debug tip - Add component counting during development
  5. Implement OnDestroy - For complex components with multiple subscriptions

The bottom line: Memory leaks from dynamic components are silent killers, but they're incredibly easy to prevent once you know the pattern. The key is always setting your component reference to null after destroying it.


I want to hear from you!

What did you think? Have you encountered memory leaks in your Angular apps? Share your war stories in the comments—I love hearing about real-world debugging adventures!

Want more Angular tips like this? I share practical insights every week that can save you hours of debugging. Follow me to get notified when I publish new articles!

Found this helpful? If this article helped you catch a potential memory leak (or just taught you something new), smash that clap button so other developers can discover it too. Every clap helps!

Action point for you: Go check your codebase right now. Search for .createComponent() and see if you're properly cleaning up. Then come back and tell me what you found!


Follow Me for More Angular & Frontend Goodness:

I regularly share hands-on tutorials, clean code tips, scalable frontend architecture, and real-world problem-solving guides.

  • 💼 LinkedIn — Let’s connect professionally
  • 🎥 Threads — Short-form frontend insights
  • 🐦 X (Twitter) — Developer banter + code snippets
  • 👥 BlueSky — Stay up to date on frontend trends
  • 🌟 GitHub Projects — Explore code in action
  • 🌐 Website — Everything in one place
  • 📚 Medium Blog — Long-form content and deep-dives
  • 💬 Dev Blog — Free Long-form content and deep-dives
  • ✉️ Substack — Weekly frontend stories & curated resources
  • 🧩 Portfolio — Projects, talks, and recognitions
  • ✍️ Hashnode — Developer blog posts & tech discussions
  • ✍️ Reddit — Developer blog posts & tech discussions

Top comments (0)