-
Notifications
You must be signed in to change notification settings - Fork 187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add section on avoiding memory leaks to events.md #1240
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -98,6 +98,32 @@ disconnectedCallback() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
See the MDN documentation on using custom elements [lifecycle callbacks](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#Using_the_lifecycle_callbacks) for more information on `connectedCallback` and `disconnectedCallback`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
### Avoiding Memory Leaks | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Avoid mixing and matching adding and removing event listeners between the [Lit reactive lifecycle methods](/docs/components/lifecycle/#reactive-update-cycle) and custom element [lifecycle callbacks](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#Using_the_lifecycle_callbacks). These two lifecycles, in some cases, operate independently of one another. For example, just because `disconnectedCallback` has been called does not mean that `shouldUpdate`, `willUpdate`, `update`, `render`, and `updated` will not be called again if a [reactive property](/docs/components/properties/) is changed after the element is removed from the DOM. This can result in memory leaks when listeners attached to active objects contain back references to the component that set them. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would love to see a description of the problematic structure so that readers can develop a mental model of when event listeners will hold on to the element. Something like: Event listeners can sometimes cause memory leaks if they're not removed, depending on the objects or elements they're added to and the objects they reference in the body of the event handler. Listener functions hold a JavaScript reference to variables closed over in the function body. This behaves as if you added a property to the event target that references the objects used in the handler. When this can often cause a leak is when a listener is added to an object outside of the custom element or it's children, that references back to the element: class LeakyElement extends LitElement {
constructor() {
super();
// This will cause LeakyElement instances to never be GC'ed if not removed:
window.addEventListener('mousemove', this.#onMouseMove);
}
#onMouseMove = (e) => { /* ... */ };
} To prevent a memory leak, you should added these listeners in class LeakyElement extends LitElement {
connectedCallback() {
super.connectedCallback();
window.addEventListener('mousemove', this.#onMouseMove);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('mousemove', this.#onMouseMove);
}
#onMouseMove = (e) => { /* ... */ };
} Note that the reference to the event listener needs to be the same in the Event listeners added by an element to itself or a child do not usually cause a leak because they create a cycle that GCs can collect: class LeakyElement extends LitElement {
constructor() {
super();
// This will *not* cause a leak:
this.addEventListener('click', this.#onClick);
}
#onClick = (e) => { /* ... */ };
} |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
```js | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
connectedCallback () { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
super.connectedCallback(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.listeningForResize = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
disconnectedCallback() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
window.removeEventListener("resize", this.handleWindowResize); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
super.disconnectedCallback(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
handleWindowResize = (event) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.style.width = "880px"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
willUpdate(changedProperties) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!this.listeningForResize) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
window.addEventListener("resize", this.handleWindowResize); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.listeningForResize = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+106
to
+122
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit around code style – single quotes for strings and underscore for private method and unused param. Also inline comment to make it clear this shouldn't be done.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
The above example creates a memory leak if the reactive update cycle triggers again and holds the element (and its subtree) in memory as a Detached HTMLElement because `window` now has a reference to this element class through the event handler. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
### Optimizing for performance | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Adding event listeners is extremely fast and typically not a performance concern. However, for components that are used in high frequency and need a lot of event listeners, you can optimize first render performance by reducing the number of listeners used via [event delegation](#event-delegation) and adding listeners [asynchronously](#async-events) after rendering. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be blasphemous but we do a single space after periods.