Skip to content

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

PackageCore 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:

TypeScript
// 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:

TypeScript
// 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

TypeScript
// 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:

TypeScript
// vite.config.ts
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
hls: ['hls.js'],
},
},
},
},
};

Tree-shake hit list

These patterns prevent effective tree-shaking:

PatternWhy it is a problemFix
import * as X from 'package'Marks every export as usedUse named imports
Side-effect imports (import 'package')Bundler cannot know what is safe to dropOnly use for polyfills
Dynamic access (pkg[key])Bundler cannot statically analyzeUse a switch statement
Re-exporting everythingPulls full package into dependentsExport 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 AudioGraphPlugin or EqualizerPlugin is active)

Rough steady-state memory per player instance:

ConfigurationApprox. 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 using this.on inside plugins)
  • Queue growth (unbounded queueAppend calls)
  • Subtitle cue accumulation (very long VTT files with thousands of cues)

Detecting listener leaks

Use the test harness’s assertNoListenerLeak in development:

TypeScript
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:

TypeScript
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(...) not player.on(...), so listeners auto-dispose
  • All setTimeout/setInterval inside plugins use this.timeout/this.interval
  • Queue has a max length, so unbounded queueAppend does not grow memory indefinitely
  • Subtitle cue array is cleared between items, because large VTT files leave stale cues
  • IndexedDBBackend has a max entry count, so prune old entries on write
  • OctopusPlugin is 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

  1. Open DevTools → Memory tab
  2. Take a heap snapshot before playback starts
  3. Play for several minutes (simulate a long session with fast-forward if needed)
  4. Take a second snapshot
  5. Select “Comparison” mode and look for growing EventEmitter, Plugin, or Array entries 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.