Performance
Bundle size, memory usage, and long-session stability for long-running apps. Covers tree-shaking, subpath imports, memory budgeting, and verifying there are no leaks.
Bundle size
What ships by default
| Package | Core size (min+gzip) | With all built-in plugins |
|---|---|---|
nomercy-player-core | ~18 kB | ~32 kB (core + all adapters) |
nomercy-video-player | ~28 kB | ~95 kB (incl. hls.js, DesktopUiPlugin, SubtitleOverlayPlugin) |
nomercy-music-player | ~24 kB | ~60 kB (incl. WebAudioBackend, LyricsPlugin, EqualizerPlugin) |
These are rough estimates, and the actual size depends on which plugins you register and how your bundler tree-shakes.
Tree-shake by importing only what you use
Named imports let bundlers eliminate unused code:
// Good: subpath imports let the bundler drop plugins you don't use:
import { nmplayer } from '@nomercy-entertainment/nomercy-video-player';
import { DesktopUiPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins';
// Bad: namespace import defeats tree-shaking:
import * as NMVideoPlayer from '@nomercy-entertainment/nomercy-video-player';
Subpath imports for core-level features
If you only need the core (no video or music library), import from nomercy-player-core directly:
// Saves ~10 kB vs importing from nomercy-video-player when you don't need video:
import { LocalStorageBackend } from '@nomercy-entertainment/nomercy-player-core';
import { describePlugin } from '@nomercy-entertainment/nomercy-player-core/testing';
Avoid barrel re-exports in your app
// Bad: re-exporting everything pulls the full package into every module that uses yours:
export * from '@nomercy-entertainment/nomercy-video-player';
// Good: only export what you actually provide:
export { MyPlugin } from './my-plugin';
hls.js split chunk
hls.js is the largest dependency in the video player. Configure your bundler to split it into a separate chunk so the player core loads immediately and hls.js loads async:
// vite.config.ts
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
hls: ['hls.js'],
},
},
},
},
};
Tree-shake hit list
These patterns prevent effective tree-shaking:
| Pattern | Why it is a problem | Fix |
|---|---|---|
import * as X from 'package' | Marks every export as used | Use named imports |
Side-effect imports (import 'package') | Bundler cannot know what is safe to drop | Only use for polyfills |
Dynamic access (pkg[key]) | Bundler cannot statically analyze | Use a switch statement |
| Re-exporting everything | Pulls full package into dependents | Export only what consumers need |
Memory budget
Each player instance consumes memory for:
- The plugin registry and plugin instances
- The event bus (event listener maps)
- The queue (array of playlist items)
- The storage cache
- The
<video>or<audio>element (managed by the browser) - Web Audio nodes (if
AudioGraphPluginorEqualizerPluginis active)
Rough steady-state memory per player instance:
| Configuration | Approx. memory |
|---|---|
| Video player, no plugins | ~8 MB |
| Video player + DesktopUiPlugin | ~12 MB |
| Music player + AudioGraphPlugin + EqualizerPlugin | ~20 MB |
| Video + DesktopUiPlugin + SubtitleOverlayPlugin + OctopusPlugin | ~40 MB (+ WASM heap) |
For long sessions (6+ hours), watch for:
- Event listener accumulation (not calling
off()or not usingthis.oninside plugins) - Queue growth (unbounded
queueAppendcalls) - Subtitle cue accumulation (very long VTT files with thousands of cues)
Detecting listener leaks
Use the test harness’s assertNoListenerLeak in development:
import { assertNoListenerLeak } from '@nomercy-entertainment/nomercy-player-core/testing';
// In a test or in development tooling:
const before = player.listenerCount(); // total across all event names
// ... perform operations ...
player.dispose();
const after = player.listenerCount();
if (after > 0) {
console.warn(`Listener leak: ${after} listeners remain after dispose`);
}
For production, instrument with the metrics adapter:
player.on('dispose', () => {
const count = player.listenerCount();
if (count > 0) {
analytics.track('player-listener-leak', { count });
}
});
Long-session checklist
Before shipping a player that runs for hours (streaming platform, music app with autoplay):
- Every plugin uses
this.on(...)notplayer.on(...), so listeners auto-dispose - All
setTimeout/setIntervalinside plugins usethis.timeout/this.interval - Queue has a max length, so unbounded
queueAppenddoes not grow memory indefinitely - Subtitle cue array is cleared between items, because large VTT files leave stale cues
-
IndexedDBBackendhas a max entry count, so prune old entries on write -
OctopusPluginis disposed and re-created when the item changes (it holds a WASM heap) - Test with browser DevTools memory profiler: record a heap snapshot, play for 30 minutes, take another, then compare retained objects
Profiling in Chrome DevTools
- Open DevTools → Memory tab
- Take a heap snapshot before playback starts
- Play for several minutes (simulate a long session with fast-forward if needed)
- Take a second snapshot
- Select “Comparison” mode and look for growing
EventEmitter,Plugin, orArrayentries in the diff
A healthy player shows a flat memory profile after initial setup.
Growth in Array suggests an unbounded queue or cue accumulation.
Growth in EventListener suggests a listener leak.
What to read next
- Core: Testing,
assertNoListenerLeakand the full test harness - Advanced: Multi-Instance, memory budget multiplies per instance
- Advanced: Server-Side Rendering, SSR reduces initial JS parse time