Fix: Additional memory leaks and performance issues in plugins#4211
Fix: Additional memory leaks and performance issues in plugins#4211
Conversation
- Clear throttleTimeout in EnvelopePlugin destroy to prevent callbacks after destruction - Store and remove dblclick listener from Polyline SVG element - Store and remove touch event listeners (touchstart, touchmove, touchend) - Clear pressTimer on Polyline destroy to prevent pending callbacks Fixes memory leaks in envelope plugin (issues #16, #21, #22)
- Listener was added to container but removed from wrapper - Now correctly removes listener from container element Fixes issue #18
- Refactored to use one scroll listener instead of one per notch - Store notch metadata in a Map for batch visibility updates - Significantly improves scroll performance with many notches - Previously created N listeners for N notches (e.g., 60 for a 60-second audio) Fixes issue #19
- Rename 'reject' variable to 'rejectFn' for clarity - Clear both timeout and reject function in onClear - Add explicit typing and comments for better maintainability - Ensure proper cleanup of pending delays Fixes issue #26
- Add documentation noting WebAudioPlayer doesn't manage blob URLs - Clarify that Player class handles blob URL lifecycle - Helps prevent memory leaks when using WebAudioPlayer standalone Addresses issue #27
- Call media.load() before media.remove() instead of after - Ensures media element is properly reset before DOM removal - More logical cleanup sequence Fixes issue #30
There was a problem hiding this comment.
Pull Request Overview
This PR addresses 15 memory leaks, performance issues, and bugs primarily affecting plugins in long-running applications. The changes focus on proper cleanup of event listeners, timeouts, and references to prevent memory accumulation.
- Timeline plugin refactored to use single scroll listener instead of one per notch for significant performance improvement
- Memory leaks fixed in envelope, hover, zoom, and minimap plugins through proper event listener cleanup
- Safety improvements added to decoder with input validation and proper AudioContext cleanup
Reviewed Changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/webaudio.ts | Added documentation about blob URL management responsibility |
| src/renderer.ts | Improved createDelay logic with clearer variable naming and cleanup |
| src/plugins/zoom.ts | Fixed wheel listener removal from correct element |
| src/plugins/timeline.ts | Refactored to use single scroll listener and batch notch updates |
| src/plugins/minimap.ts | Added race condition prevention and container cleanup |
| src/plugins/hover.ts | Fixed event listener cleanup and reduced memory usage |
| src/plugins/envelope.ts | Added comprehensive cleanup for touch events and timers |
| src/player.ts | Reordered destroy operations for logical cleanup sequence |
| src/decoder.ts | Added input validation and proper AudioContext cleanup |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| if (this.lastPointerMove) this.onPointerMove(this.lastPointerMove) | ||
| if (this.lastPointerPosition) { | ||
| // Recreate a minimal PointerEvent-like object | ||
| this.onPointerMove(this.lastPointerPosition as PointerEvent) |
There was a problem hiding this comment.
The type assertion as PointerEvent is unsafe since lastPointerPosition only contains clientX and clientY properties. Consider creating a proper interface or passing the required properties explicitly to avoid potential runtime errors if onPointerMove expects other PointerEvent properties.
src/decoder.ts
Outdated
| if (!channelData[0] || channelData[0].length === 0) { | ||
| throw new Error('channelData must contain non-empty channel arrays') | ||
| } | ||
|
|
||
| // If a single array of numbers is passed, make it an array of arrays | ||
| if (typeof channelData[0] === 'number') channelData = [channelData as unknown as number[]] | ||
|
|
There was a problem hiding this comment.
This validation occurs after the type coercion check on line 45, but the validation assumes channelData[0] exists as an array. If channelData[0] is a number (which gets converted to an array later), this check will incorrectly fail since numbers don't have a length property. Move this validation after the type coercion logic.
| if (!channelData[0] || channelData[0].length === 0) { | |
| throw new Error('channelData must contain non-empty channel arrays') | |
| } | |
| // If a single array of numbers is passed, make it an array of arrays | |
| if (typeof channelData[0] === 'number') channelData = [channelData as unknown as number[]] | |
| // If a single array of numbers is passed, make it an array of arrays | |
| if (typeof channelData[0] === 'number') channelData = [channelData as unknown as number[]] | |
| if (!channelData[0] || channelData[0].length === 0) { | |
| throw new Error('channelData must contain non-empty channel arrays') | |
| } |
| if (!this.wavesurfer) return | ||
| const width = element.clientWidth | ||
| const isVisible = start >= scrollLeft && start + width < scrollRight | ||
| const notchData = this.notchElements.get(element)! |
There was a problem hiding this comment.
Using the non-null assertion operator ! without a prior existence check is risky. Consider adding a guard clause to handle the case where the element is not found in the map to prevent potential runtime errors.
| const notchData = this.notchElements.get(element)! | |
| const notchData = this.notchElements.get(element) | |
| if (!notchData) { | |
| // If the element is not found in the map, do not proceed | |
| return | |
| } |
Deploying wavesurfer-js with
|
| Latest commit: |
ae548c9
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://33a0bd2d.wavesurfer-js.pages.dev |
| Branch Preview URL: | https://fix-additional-memory-leaks.wavesurfer-js.pages.dev |
- Validate channel length after converting flat array to array of arrays - Prevents false validation errors for valid flat array inputs - Fixes failing e2e tests for pre-decoded waveform and blob loading
Summary
This PR fixes 15 additional memory leaks, performance issues, and bugs discovered in a follow-up analysis of the codebase. These issues primarily affect plugins and could cause memory leaks in long-running applications.
Issues Fixed
Critical Performance Issues
Memory Leaks Fixed
Envelope Plugin (Presetting markers #16, Cursor reached a mark #21, Mobile Browser Performance #22):
Hover Plugin (Assorted features and fixes #17, Fixed webAudio initializer to use global Audio Context. #24):
Zoom Plugin (Refactored seeking for the same behavior when clicking as calling skip() #18):
Minimap Plugin (Load new wave? #20, Fixed webAudio initializer to use global Audio Context. #25):
Safety & Validation Improvements
Code Quality Improvements
Testing Recommendations
Impact
These fixes will prevent memory leaks in applications that:
🤖 Generated with Claude Code