OSMD 2.0.0 is a major release focused on speed and real-world usability: render() is roughly 1.8× faster on average (up to ~2.3× on large orchestral scores), as a brand-new geometric skyline calculation removes the expensive getImageData pipeline. The release also adds lazy/incremental rendering (render system-by-system), support for tremolos between notes, cross-staff slurs, crisper stafflines, and an array of bug fixes that improve alignment, tuplets, and cursor/cloning behavior.
Why this release matters
- Much faster first paint and full render times — especially important for large scores and mobile devices.
- New incremental rendering API lets apps show a usable first view quickly, then progressively draw more content (perfect for long scores and lazy-load flows).
- Better engraving fidelity for real music (tremolos, cross-staff slurs, nested tuplets)
- Smaller UI/UX surprises fixed (render drift, unison handling, rehearsal mark overlaps), making OSMD more reliable for production use.
Highlights — what’s new
- Performance: Geometric skyline/bottom-line calculation replaces expensive canvas readbacks (getImageData). OSMD now renders ~1.8× faster on median samples and up to ~2.3× on some large scores.
- Incremental / lazy rendering: renderNext({systems|measures}) and enableIncrementalRenderingOnScroll() let you render one system (line) at a time for fast first paint and on-scroll loading.
- Notation features: Tremolos between notes, slurs across staves, nested tuplets display correctly.

- Visual fidelity: Stafflines now snap to crisp sub-pixel positions for consistent 1px strokes.
- Bug fixes: Cursor/iterator cloning bug, render drift, ghost-note and tuplet alignment fixes, unison notehead merging in more cases, rehearsal mark overlap fixes, and more.
Deep dive: How the performance win was achieved
The biggest cost in previous OSMD rendering was computing per-staffline skylines by second-pass rasterization: draw a measure to a hidden canvas, call getImageData() and scan pixels. On GPU-accelerated canvases this stalls the pipeline and creates heavy GPU to CPU data transfers.
Instead of rasterizing, the new approach intercepts VexFlow draw calls with a virtual drawing context — GeometricSkyBottomLineContext. It implements the subset of the canvas API used by VexFlow and computes geometric extents of drawn elements directly (flattened bezier extents, stroked/dilated shapes, text/glyph extents cached per font/character, parsed glyph outlines cached per glyph+scale). The result: no pixel readbacks, no framebuffer transfers, and far fewer repeated outline computations.
Concrete effects:
- Average speedup: ~×1.8 (median range ×1.5–×2.3 depending on sample).
- Skyline phase speedup 2x to 4x, even more in some cases — the biggest single bottleneck is gone.
- Layouts remain visually identical to well within sub-pixel tolerances; numeric tests across hundreds of samples show a mean difference of ~0.05px.
Why this is elegant
- Intercepting draw calls keeps the geometric results perfectly aligned with what VexFlow would draw — analyzing every Vexflow draw call gives us automatic and complete coverage of everything Vexflow renders
- Heavy caching (glyph outlines, character ink extents) reduces repeated CPU work across scores.
- Compatibility is preserved: the new method is enabled by default but can be disabled through EngravingRules if you need to reproduce pixel‑by‑pixel historic baselines.
Incremental / lazy rendering — practical UX gains
Large scores can block the UI while rendering. OSMD 2.0.0 introduces incremental rendering APIs to let apps render progressively and show a useful first view quickly:
Key API
// Basic pattern
osmd.load(musicXml).then(() => {
// render the first two systems quickly
osmd.renderNext({ systems: 2 });
// later, render more (e.g. on scroll)
osmd.renderNext({ systems: 2 });
});
// finish rendering synchronously (e.g. before export)
osmd.renderRemaining();
// convenience: auto-render on scroll
osmd.enableIncrementalRenderingOnScroll({ /* options */ });
TypeScriptHow it works:
- Vertical (endless-scroll) mode appends new systems while reusing previously computed layout info where possible.
- Horizontal single-staff modes grow the visible window while keeping lower stafflines stable.
- APIs return progress objects so you can show progress UI (IncrementalRenderProgress / IncrementalRenderingComplete).
Why this is important for users:
- Improves perceived performance for readers on mobile or slow devices.
- Lets web apps stream or lazy-load large collections without blocking the main thread for a long render call.
Notation improvements & engraving fixes
- Tremolo between notes: support for tremolos that connect two notes (drawn by OSMD, with customizable engraving rules like stroke thickness (TremoloStrokeScale), gaps, padding, slant).

- Slurs crossing staves: cross-staff slurs (e.g., left-hand to right-hand) are implemented with an EngravingRule toggle (default true).

- Nested tuplets display correctly and cross-staff tuplet alignment is fixed by making ghost notes carry exact tick durations.

- Unison merging: unisons that differ only by dotted vs plain are merged where engraving permits, preventing incorrect staggering.

- Staffline rendering: new SnapStafflinesToCrispPixels rule ensures consistent crisp 1px staff lines across systems.
Notable bug fixes (short list)
- Cursor/iterator clone shallow-copy bug fixed (cloning no longer corrupts original iterator state).
- Render drift between subsequent render() calls fixed by resetting per-render VexFlow state.
- Rehearsal marks no longer overlap notes/chord symbols — they lift above when needed.
- Layout edge cases (extra instruction measures) no longer produce negative widths or cut staff lines short.
Compatibility & migration notes
- Default: EngravingRules.UseGeometricSkyBottomLineCalculation = true. You can revert to the old pixel-based calculation via EngravingRules or IOSMDOptions if you need exact byte-for-byte legacy layouts.
- Layouts may shift by sub-pixel amounts compared to older releases. Visual-regression baselines should be re-blessed once for CI (the new method is more exact than the pixel-quantized approach).
- Incremental rendering is opt-in via the renderNext API; a normal render() call still does a single synchronous layout/draw as before.
How to upgrade (practical steps)
- Update your package dependency to 2.0.0 or load the new bundle from the release.
- Run a quick smoke test with your most-used scores. If you rely on exact historical pixel baselines, run visual regression tests and re-bless baselines as needed.
- If you use custom EngravingRules or low-level layout assumptions, check these keys:
UseGeometricSkyBottomLineCalculation,SnapStafflinesToCrispPixels(enabled by default), and new tremolo rules. Set the toggles to preserve behavior if needed. - To adopt lazy rendering for large scores, switch to
renderNext()and optionally enableenableIncrementalRenderingOnScroll()so users see a first useful view quickly.
Expert takeaways / opinion
This release is a textbook example of a high-impact optimization that trades raster work for exact geometric computation and smart caching. The old brute-force pixel readback pattern was too slow even on modern accelerated canvases; replacing it with an interception layer that computes extents mathematically removes the pathological GPU->CPU data transfer stall and makes performance more predictable across platforms, especially considering WebGL shaders were previously the fastest on some platforms and browsers, but even slower than default calculations on others. Geometric skyline calculation is now universally the fastest on all platforms.
From a product perspective, the incremental rendering API is equally important: real-world scores and mobile audiences benefit more from a fast first paint than from slightly faster eventual full render times. Combined, these changes make OSMD much more attractive for interactive web music apps (score viewers, practice/teaching apps, DAW integrations, music-learning platforms).
How to try it right now
Try the demo and sample scores included in the project. Example (basic):
const osmd = new OpenSheetMusicDisplay("#osmd", { drawTitle: true });
await osmd.load(xmlString);
// fast progressive render (first 2 systems)
osmd.renderNext({ systems: 2 });
// later, append more systems
osmd.renderNext({ systems: 2 });
TypeScriptTo force the legacy skyline calculation for parity testing:
osmd.EngravingRules.UseGeometricSkyBottomLineCalculation = false;
osmd.render();
TypeScriptLinks & resources
- Release/tag: OSMD v2.0.0 (GitHub)
- Full changelog: CHANGELOG.md (2.0.0)
- Major PRs: performance (PR #1681), incremental rendering (PR #1690), tremolos (PR #1680), cross-staff slurs (PR #1006)
- Performance tests & tooling are added under test/performance/ in the repo for reproducible benchmarks.
Final thoughts
OSMD 2.0.0 is more than a performance bump — it’s a maturation of the rendering pipeline that reduces platform noise (GPU readbacks), adds real-world UX features (incremental rendering), and fills long-standing engraving gaps. If you build web score viewers, practice or teaching tools, or embed sheet-music in web pages, this release should be on your upgrade path.
Try it, run your visual regression tests, and consider enabling incremental rendering for long scores — your users will notice the faster first paint.
Release details: https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/releases/tag/2.0.0




