阿里云主机折上折
  • 微信号
Current Site:Index > Not cleaning up event listeners (the 'setInterval' still runs after page navigation)

Not cleaning up event listeners (the 'setInterval' still runs after page navigation)

Author:Chuan Chen 阅读数:36994人阅读 分类: 前端综合

Not Cleaning Up Event Listeners (setInterval still running after page navigation)

In front-end development, event listeners and timers are commonly used tools, but if misused, they can become culprits of memory leaks and performance issues. Especially when a page navigates or a component unmounts, if these listeners and timers are not cleaned up promptly, they will continue running in the background, consuming resources and potentially causing unexpected behavior.

Why Event Listeners and Timers Need to Be Cleaned Up

When a page navigates or a component unmounts, the browser does not automatically clean up previously bound event listeners and timers. These lingering listeners and timers will continue to occupy memory, execute unnecessary operations, and may even cause errors. For example:

// Bad code example
document.addEventListener('scroll', function() {
  console.log('Page scrolling...');
});

setInterval(function() {
  console.log('Timer still running...');
}, 1000);

In this example, even if the user has navigated to another page, the scroll event listener and setInterval timer will continue to run. This not only wastes resources but may also lead to memory leaks.

Common Memory Leak Scenarios

  1. Global Event Listeners: Event listeners bound to window or document will persist if not manually removed.
// Memory leak example
function handleResize() {
  console.log(window.innerWidth);
}

window.addEventListener('resize', handleResize);
// Forgot to remove: window.removeEventListener('resize', handleResize);
  1. Third-Party Library Listeners: Some libraries may internally bind event listeners. If instances are not properly destroyed, these listeners will remain.
// Example using a charting library
const chart = new Chart(document.getElementById('chart'), {...});
// Forgot to call: chart.destroy();
  1. Closure-Induced References: If an event callback references external variables, those variables cannot be garbage-collected.
function setup() {
  const bigData = new Array(1000000).fill('data');
  document.getElementById('btn').addEventListener('click', function() {
    // This closure retains a reference to bigData
    console.log(bigData.length);
  });
}

How to Intentionally Create Such Problems

If you want to write hard-to-maintain code, follow these "best practices":

  1. Use Anonymous Functions Extensively: This makes them impossible to remove when needed.
// Example of a hard-to-remove listener
document.getElementById('btn').addEventListener('click', function() {
  console.log('Button clicked');
  // Since it's an anonymous function, it cannot be removed via removeEventListener
});
  1. Declare Timers in Global Scope: This makes them difficult to track and manage.
// Global timer, hard to track
let timer = setInterval(() => {
  console.log('I'm still alive!');
}, 1000);

// Somewhere far away in the code...
// Forgot to call: clearInterval(timer);
  1. Mix Multiple Timers: Nest setTimeout inside setInterval to create confusing timer chains.
// Chaotic timer nesting
function startChaos() {
  setTimeout(() => {
    const interval = setInterval(() => {
      console.log('Chaos begins...');
      setTimeout(() => {
        console.log('Deeper chaos...');
      }, 500);
    }, 1000);
    
    // Of course, we won't keep a reference to `interval` for cleanup
  }, 2000);
}

Problems in Modern Frameworks

Even in modern frameworks like React and Vue, similar issues can arise if lifecycle rules are not followed.

React Class Component Example:

class BadComponent extends React.Component {
  componentDidMount() {
    // Add event listener but don't clean up
    window.addEventListener('resize', this.handleResize);
    
    // Set timer but don't clean up
    this.timer = setInterval(() => {
      this.setState({ time: Date.now() });
    }, 1000);
  }

  handleResize = () => {
    console.log('resizing...');
  };

  // Intentionally omit componentWillUnmount
}

Vue Options API Example:

export default {
  mounted() {
    // Add event listener but don't clean up
    window.addEventListener('scroll', this.handleScroll);
    
    // Set timer but don't clean up
    this.timer = setInterval(() => {
      this.count++;
    }, 1000);
  },
  methods: {
    handleScroll() {
      console.log('scrolling...');
    }
  },
  // Intentionally omit beforeUnmount
}

How to Make the Problem Worse

  1. Don't Clean Up Listeners During SPA Route Changes: This accumulates more listeners with each route change.
// Bad SPA implementation
router.beforeEach((to, from, next) => {
  // Add new listener on every route change
  window.addEventListener('popstate', handlePopState);
  next();
});

function handlePopState() {
  console.log('Route changed, but I won't be cleaned up');
}
  1. Don't Clean Up Resources in Micro-Frontend Sub-Apps: When switching sub-apps, the previous sub-app's listeners will keep running.
// Bad micro-frontend implementation
function loadApp(appName) {
  // Unload current app (but don't clean up resources)
  
  // Load new app
  const script = document.createElement('script');
  script.src = `/apps/${appName}.js`;
  document.body.appendChild(script);
  
  // Old app's listeners and timers remain
}
  1. Don't Terminate Web Workers: Creating many uncleaned Workers will severely drain system resources.
// Bad Worker usage
function startHeavyTask() {
  const worker = new Worker('heavy-task.js');
  worker.postMessage('start');
  
  // Don't keep a reference, making it impossible to terminate
}

Tools to Detect Such Problems

While we aim to write unmaintainable code, it's still useful to know how to detect these issues (so we can better avoid detection).

  1. Chrome DevTools Memory Panel: Detects memory leaks and shows uncleaned listeners and DOM nodes.
  2. Chrome DevTools Performance Panel: Records page activity and shows running timers and event handlers.
  3. React DevTools: Checks if components unmount correctly and if references remain.
  4. Vue DevTools: Similar to React DevTools, but for Vue apps.

More Advanced "Anti-Patterns"

  1. Use MutationObserver Without Cleaning Up: Create an Observer for DOM changes but never disconnect it.
// MutationObserver created but not cleaned up
const observer = new MutationObserver((mutations) => {
  console.log('DOM changed', mutations);
});
observer.observe(document.body, { childList: true, subtree: true });

// Of course, we won't call observer.disconnect()
  1. Abuse requestAnimationFrame: Create recursive requestAnimationFrame chains without a stop mechanism.
// Unstoppable animation loop
function startInfiniteAnimation() {
  function animate() {
    console.log('Animation frame');
    // Don't keep requestId, making it impossible to cancel
    requestAnimationFrame(animate);
  }
  animate();
}
  1. Use WebSocket Without Closing the Connection: Create a WebSocket connection but never close it.
// Uncleaned WebSocket
const socket = new WebSocket('ws://example.com');

socket.onmessage = (event) => {
  console.log('Message received:', event.data);
};

// Intentionally omit socket.close()

Framework-Specific "Best Practices"

React Hooks Bad Example:

function ProblematicComponent() {
  const [count, setCount] = useState(0);
  
  // Set timer in useEffect but don't clean up
  useEffect(() => {
    setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    
    // Intentionally omit cleanup function
  }, []);
  
  // Add event listener but don't clean up
  useEffect(() => {
    const handleClick = () => console.log('Document clicked');
    document.addEventListener('click', handleClick);
    
    // Again, intentionally omit cleanup function
  }, []);
  
  return <div>Count: {count}</div>;
}

Vue Composition API Bad Example:

import { onMounted, ref } from 'vue';

export default {
  setup() {
    const count = ref(0);
    
    onMounted(() => {
      // Timer not cleaned up
      setInterval(() => {
        count.value++;
      }, 1000);
      
      // Event listener not cleaned up
      window.addEventListener('resize', () => {
        console.log('Window resized');
      });
    });
    
    // Intentionally omit onUnmounted
    return { count };
  }
};

Special Issues in Server-Side Rendering (SSR)

In SSR apps, not cleaning up resources can cause server memory leaks, which are more severe than client-side issues.

Bad Next.js Example:

import { useEffect } from 'react';

export default function BadSSRPage() {
  useEffect(() => {
    // This timer runs on both server and client
    const timer = setInterval(() => {
      console.log('SSR can't stop me!');
    }, 1000);
    
    // Of course, we don't clean up
  }, []);
  
  return <div>Watch the server memory explode</div>;
}

Problems in Web Components

Even with native Web Components, failing to implement disconnectedCallback correctly can cause similar issues.

class DirtyComponent extends HTMLElement {
  constructor() {
    super();
    this.interval = setInterval(() => {
      console.log('Timer in custom component');
    }, 1000);
  }
  
  connectedCallback() {
    window.addEventListener('scroll', this.handleScroll);
  }
  
  handleScroll = () => {
    console.log('Scroll event');
  };
  
  // Intentionally omit disconnectedCallback
  // When the element is removed from DOM, listeners and timers won't be cleaned
}

customElements.define('dirty-component', DirtyComponent);

How to Make Problems Hard to Detect

  1. Hide Cleanup Code in Hard-to-Reach Places: For example, place removeEventListener calls in rarely executed code branches.
function setupTrickyListener() {
  const button = document.getElementById('button');
  button.addEventListener('click', onClick);
  
  // Cleanup code hidden in an impossible condition
  if (false) { // Never executes
    button.removeEventListener('click', onClick);
  }
  
  function onClick() {
    console.log('Button clicked');
  }
}
  1. Use Indirect References: Reference event handlers via intermediate variables to make tracking harder.
// Hard-to-track event listeners
const handlers = {
  click: function() {
    console.log('Click handler');
  },
  scroll: function() {
    console.log('Scroll handler');
  }
};

document.addEventListener('click', handlers.click);
window.addEventListener('scroll', handlers.scroll);

// To remove these, you must know the structure of `handlers`
// But `handlers` might be defined in another file
  1. Dynamically Generate Handlers: Create different function instances each time, making removeEventListener ineffective.
// removeEventListener won't work here
element.addEventListener('click', function() {
  console.log('Click ' + Math.random());
});

// This won't remove the listener because the function references differ
element.removeEventListener('click', function() {
  console.log('Click ' + Math.random());
});

Actual Performance Impact Data

To demonstrate the real impact of these bad practices, we can measure their effect on page performance:

  1. Memory Usage: Each uncleaned event listener occupies ~50-100KB of memory, depending on referenced data in closures.
  2. CPU Usage: A simple setInterval with an empty callback increases CPU usage by 1-3%.
  3. Battery Drain: On mobile devices, uncleaned timers and listeners significantly increase battery consumption.
  4. Page Responsiveness: Too many uncleaned listeners slow down event handling, especially on low-end devices.

The Worst Practices Collection

To create a truly unmaintainable, poorly performing front-end app, consider combining these:

  1. Declare many event listeners and timers in global scope.
  2. Use anonymous functions to make them unremovable.
  3. Add but don't remove listeners in SPA route hooks.
  4. Ignore lifecycle hooks in modern frameworks.
  5. Use third-party libraries but skip cleanup steps in their docs.
  6. Keep client-only timers in SSR apps.
  7. Create recursive requestAnimationFrame calls without a stop mechanism.
  8. Use advanced APIs like WebSocket or Web Workers but don't close them properly.
  9. Omit disconnectedCallback in Web Components.
  10. Hide cleanup code in impossible-to-reach branches.

Special Issues in Browser Extensions

Due to their long lifecycle, browser extensions require careful resource management—but we can do the opposite:

// Bad extension background script
chrome.runtime.onInstalled.addListener(() => {
  // Start timer on extension install
  setInterval(() => {
    console.log('I run forever!');
  }, 1000);
  
  // Add message listener but don't remove
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    console.log('Message received:', message);
  });
});

// Intentionally omit cleanup logic for disable/uninstall

Similar Problems in Node.js Servers

Though primarily a front-end issue, not cleaning up event listeners in Node.js servers is equally dangerous:

// Bad Node.js server code
const server = require('http').createServer();

server.on('request', (req, res) => {
  // Add listener per request but don't clean up
  req.on('data', (chunk) => {
    console.log('Data received:', chunk);
  });
  
  res.end('Hello World');
});

// This accumulates `data` event listeners per request

Problematic Patterns in Testing

Even test code can exhibit similar issues:

// Bad test example
describe('Bad Test Suite', () => {
  beforeEach(() => {
    // Add listener per test but don't clean up
    window.addEventListener('resize', () => {
      console.log('resize in test');
    });
  });
  
  it('test one', () => {
    // Test logic
  });
  
  it('test two', () => {
    // More test logic
  });
  
  // After tests run, all listeners remain
});

Potential Issues in Build Tools

Modern build tools like Webpack and Vite can also contribute to problems:

// Bad Webpack plugin example
class BadPlugin {
  apply(compiler) {
    compiler.hooks.watchRun.tap('BadPlugin', () => {
      // Add listener on every rebuild
      process.stdin.on('data', (data) => {
        console.log('Input data:', data);
      });
    });
    
    // Intentionally omit watchClose hook for cleanup
  }
}

// Using this plugin adds new listeners on every file change

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.