How to add music to a web game
There are four common ways to add music to a browser game: play audio files (usually through Howler), render a song with a tiny tracker like ZzFXM, synthesize it with Tone.js, or synthesize it with plonk, which we make. Files sound like anything you can record and cost the most bytes; the code-based options are small and can follow the game, but only make the sounds a synthesizer can.
Short answer
Use audio files when the music needs real instruments, vocals or a composer's finished mix. Use ZzFXM for size-limited jams like js13k. Use Tone.js when music is the product: a sequencer, a generative piece, anything that needs serious synthesis. Use plonk when you want small game music in code that reacts to the game: chiptune and synth styles, with tempo, layers and in-key sound effects built in.
At a glance
| Audio files + Howler | ZzFXM | Tone.js | plonk | |
|---|---|---|---|---|
| What you ship | A library plus the music files | A song array and a tiny renderer | A library, songs in code | A library, songs as text |
| Library, brotli* | 8.8 kB (howler 2.2.4) | Not on npm, so not measured here | 51.0 kB (tone 15.1.22) | 3.8 kB (plonkjs 0.2.0) |
| A 1-minute loop | About 720 kB at 96 kbps | A few hundred bytes of song data | Your code | A few hundred bytes of text |
| Sound | Anything you can record | ZzFX-style synth instruments | Full synthesis, samplers and effects | Synth voices, noise drums, echo, reverb |
| Reacting to the game | Crossfade separate files (stems) | Re-render or swap songs | Anything you program | tempo(), layer(), sfx() in the song's key |
| Timing | One buffer, no per-note timing | One rendered buffer | Ahead-of-time scheduling | Ahead-of-time scheduling (measured) |
| Authoring | A DAW, a composer, a stock library | Its tracker tool, or by hand | Code | Text patterns, the playground |
| Licence of the music | Whatever you bought or commissioned | Yours | Yours | Yours |
*A minimal app that plays one loop, as a Vite production build, brotli. From bench/music-size. The loop size is arithmetic: 96 kbps is 12 kB a second.
Audio files
The default for most games, and the right one whenever the music is a recording. Export a loop from your DAW (or buy one), and play it with Howler or plain Web Audio:
import { Howl } from "howler";
const theme = new Howl({ src: ["theme.ogg"], loop: true, volume: 0.6 });
theme.play();The cost is size. Compressed music runs at about 12 kB a second at 96 kbps, so a one-minute loop is about 720 kB, often more than the rest of a small web game put together. On portals and mobile, that's load time before the first note. Streaming helps the first play but not the total download.
Music that reacts to the game means stems: separate files for drums, bass and melody that play in sync, faded in and out. That works well, and it multiplies the download by the number of stems. Changing the tempo of a recording changes its pitch unless you time-stretch, which costs CPU and can sound smeared.
ZzFXM: a tiny tracker
ZzFXM by Keith Clark is a music generator for tiny JavaScript productions, built on Frank Force's ZzFX sound generator. A song is an array of instruments and patterns, loosely like a MOD file, and ZzFXM renders it into audio data that then plays as one buffer. It's made for size limits like js13k's 13 kB, and it has a tracker tool for writing songs.
Rendering happens before the music plays, so a long song costs some time at startup, and changing the music means rendering again. For a jam entry where every byte counts, it's hard to beat.
Tone.js: a full music framework
Tone.js is a framework for interactive music in the browser: synthesizers, samplers, effects, and a Transport that schedules notes ahead on the audio clock, so timing holds when frames drop.
import * as Tone from "tone";
const synth = new Tone.Synth().toDestination();
new Tone.Sequence((time, note) => synth.triggerAttackRelease(note, "16n", time), ["C4", "E4", "G4"], "16n").start(0);
Tone.getTransport().start();It can do far more than a game soundtrack needs: FM and granular synthesis, convolution, samplers that play recorded instruments. That range is why it's 51 kB for a minimal app. If you're building a music toy, a sequencer or a generative piece, it's the mature choice.
plonk: music made in code, for games
plonk is our music engine for web games. A song is data: instruments, and patterns of notes written as text, arranged in sections. It synthesizes them live with Web Audio, scheduling ahead on the audio clock like Tone.js, in 3.8 kB.
import { Player } from "plonkjs";
const player = new Player();
player.play({
bpm: 112,
instruments: {
lead: { wave: "square", decay: 0.14, sustain: 0.35, echo: 0.3 },
kick: { wave: "sine", drop: 24, decay: 0.12, sustain: 0 },
},
sections: { a: { lead: "C5 . E5 G5 . E5 C5 .", kick: "x . . . x . . ." } },
order: ["a"],
});
player.tempo(1.12); // speed up without a jump
player.layer("kick", 0); // fade the drums outIt's built around what game music does: tempo() without a jump when time runs out, layer() to bring tracks in and out with the game's mood, sfx() for sound effects in the song's key and on its beat, and (from plonkjs 0.3) player.step to animate in time. Songs are text, so a coding agent can write and edit them, and notes() checks them without speakers.
The limits are a synthesizer's: no recorded instruments or vocals, and a chiptune-and-synth palette. We think it's the best fit for small games with a code-drawn look; for an orchestral score, use files.
What every approach has to handle
Autoplay. Browsers don't play audio until the player interacts with the page (MDN). Start the music, or resume the AudioContext, from the first click, tap or key press, like a "press any key" title screen.
Hidden tabs. When the player switches tabs, pause the game and the music together. Listen for visibilitychange and stop or suspend on document.hidden. Browsers also slow down timers in hidden tabs, which starves any music scheduled from code.
A mute button and a music volume. Players expect both, separate from sound effects. Route music through its own gain node (or Howler's per-sound volume) and remember the setting.
Loops without a gap. MP3 encoders pad the start and end of a file with a little silence, so an MP3 loop can gap or click at the seam. Use Ogg Vorbis or Opus for loops, or let a synthesized song loop itself.
What this doesn't cover
Spatial audio, dialogue and voice, and sound-effect design are their own topics. The library sizes are for a minimal app on one day's versions; real games add their own code, and ZzFXM isn't measured because it isn't published as a package.
Make a loop in the plonk playground, copy it as code, and paste it into your game.