Files
marco/.agents/skills/ember-best-practices/rules/component-memory-leaks.md

4.7 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Prevent Memory Leaks in Components HIGH Avoid memory leaks and resource exhaustion memory, cleanup, lifecycle, performance

Prevent Memory Leaks in Components

Properly clean up event listeners, timers, and subscriptions to prevent memory leaks.

Incorrect (no cleanup):

// app/components/live-clock.gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

class LiveClock extends Component {
  @tracked time = new Date();

  constructor() {
    super(...arguments);

    // Memory leak: interval never cleared
    setInterval(() => {
      this.time = new Date();
    }, 1000);
  }

  <template>
    <div>{{this.time}}</div>
  </template>
}

Correct (proper cleanup with registerDestructor):

// app/components/live-clock.gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { registerDestructor } from '@ember/destroyable';

class LiveClock extends Component {
  @tracked time = new Date();

  constructor() {
    super(...arguments);

    const intervalId = setInterval(() => {
      this.time = new Date();
    }, 1000);

    // Proper cleanup
    registerDestructor(this, () => {
      clearInterval(intervalId);
    });
  }

  <template>
    <div>{{this.time}}</div>
  </template>
}

Event listener cleanup:

// app/components/window-size.gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { registerDestructor } from '@ember/destroyable';

class WindowSize extends Component {
  @tracked width = window.innerWidth;
  @tracked height = window.innerHeight;

  constructor() {
    super(...arguments);

    const handleResize = () => {
      this.width = window.innerWidth;
      this.height = window.innerHeight;
    };

    window.addEventListener('resize', handleResize);

    registerDestructor(this, () => {
      window.removeEventListener('resize', handleResize);
    });
  }

  <template>
    <div>Window: {{this.width}} x {{this.height}}</div>
  </template>
}

Using modifiers for automatic cleanup:

// app/modifiers/window-listener.js
import { modifier } from 'ember-modifier';

export default modifier((element, [eventName, handler]) => {
  window.addEventListener(eventName, handler);

  // Automatic cleanup when element is removed
  return () => {
    window.removeEventListener(eventName, handler);
  };
});
// app/components/resize-aware.gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import windowListener from '../modifiers/window-listener';

class ResizeAware extends Component {
  @tracked size = { width: 0, height: 0 };

  handleResize = () => {
    this.size = {
      width: window.innerWidth,
      height: window.innerHeight,
    };
  };

  <template>
    <div {{windowListener "resize" this.handleResize}}>
      {{this.size.width}}
      x
      {{this.size.height}}
    </div>
  </template>
}

Abort controller for fetch requests:

// app/components/data-loader.gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { registerDestructor } from '@ember/destroyable';

class DataLoader extends Component {
  @tracked data = null;
  abortController = new AbortController();

  constructor() {
    super(...arguments);

    this.loadData();

    registerDestructor(this, () => {
      this.abortController.abort();
    });
  }

  async loadData() {
    try {
      const response = await fetch('/api/data', {
        signal: this.abortController.signal,
      });
      this.data = await response.json();
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('Failed to load data:', error);
      }
    }
  }

  <template>
    {{#if this.data}}
      <div>{{this.data.content}}</div>
    {{/if}}
  </template>
}

Using ember-resources for automatic cleanup:

// app/components/websocket-data.gjs
import Component from '@glimmer/component';
import { resource } from 'ember-resources';

class WebsocketData extends Component {
  messages = resource(({ on }) => {
    const messages = [];
    const ws = new WebSocket('wss://example.com/socket');

    ws.onmessage = (event) => {
      messages.push(event.data);
    };

    // Automatic cleanup
    on.cleanup(() => {
      ws.close();
    });

    return messages;
  });

  <template>
    {{#each this.messages.value as |message|}}
      <div>{{message}}</div>
    {{/each}}
  </template>
}

Always clean up timers, event listeners, subscriptions, and pending requests to prevent memory leaks and performance degradation.

Reference: Ember Destroyable