A browser-based LaTeX compiler. TeX Live 2025 running in WebAssembly, with lazy bundle loading and on-demand package fetching.
On initialization, the engine downloads:
- WASM binary (~29MB): the TeX engine
- Core bundles (~16MB for pdfLaTeX): format files, fonts, base packages
Common packages (amsmath, tikz, biblatex, etc.) are pre-bundled for fast loading. Less common packages are fetched from TexLive/CTAN automatically during compilation. Everything is cached in the browser for offline use.
The full bundle is ~195MB to deploy, but clients only download what their documents need.
Guides:
- Examples & Playground Tutorial — Build a LaTeX editor UI
- CTAN Proxy Guide — Self-host the package proxy for production
- Building from Source — Build WASM engine and bundles
Clone the repo, download the WASM engine and pre-built bundles from cdn.siglum.org (or GitHub Releases), then start the dev server.
We use Bun for development, it's faster than Node.js and runs TypeScript directly. Install it with:
curl -fsSL https://bun.sh/install | bashThen:
git clone [email protected]:SiglumProject/siglum-engine.git
cd siglum-engine
bun install
# Download WASM and bundles
mkdir -p busytex/build/wasm
curl -Lo busytex/build/wasm/busytex.wasm https://cdn.siglum.org/tl2025/busytex.wasm
curl -Lo busytex/build/wasm/busytex.js https://cdn.siglum.org/tl2025/busytex.js
curl -LO https://cdn.siglum.org/tl2025/siglum-bundles-v0.1.0.tar.gz
tar -xzf siglum-bundles-v0.1.0.tar.gz -C packages/
# Start dev server
bun serve-local.tsOpen http://localhost:8787 to try the playground.
For disk-persistent package caching, run the CTAN proxy in a separate terminal:
bun packages/ctan-proxy.tsnpm install @siglum/engineDownload WASM and bundles from cdn.siglum.org or GitHub Releases:
curl -LO https://cdn.siglum.org/tl2025/busytex.wasm
curl -LO https://cdn.siglum.org/tl2025/busytex.js
curl -LO https://cdn.siglum.org/tl2025/siglum-bundles-v0.1.0.tar.gz
# Extract to your public directory
tar -xzf siglum-bundles-v0.1.0.tar.gz -C public/
mv busytex.wasm busytex.js public/Then configure:
import { SiglumCompiler } from '@siglum/engine';
const compiler = new SiglumCompiler({
bundlesUrl: '/bundles',
wasmUrl: '/busytex.wasm',
});Or use the CDN directly (no self-hosting required):
const compiler = new SiglumCompiler({
bundlesUrl: 'https://cdn.siglum.org/tl2025/bundles',
wasmUrl: 'https://cdn.siglum.org/tl2025/busytex.wasm',
});import { SiglumCompiler } from '@siglum/engine';
const compiler = new SiglumCompiler({
bundlesUrl: '/bundles',
wasmUrl: '/wasm/busytex.wasm',
});
await compiler.init();
const result = await compiler.compile(`
\\documentclass{article}
\\begin{document}
Hello, World!
\\end{document}
`);
if (result.success) {
const blob = new Blob([result.pdf], { type: 'application/pdf' });
window.open(URL.createObjectURL(blob));
}const compiler = new SiglumCompiler({
// URLs
bundlesUrl: '/bundles', // URL to bundle files
wasmUrl: '/wasm/busytex.wasm', // URL to WASM binary
jsUrl: null, // URL to busytex.js (derived from wasmUrl if null)
ctanProxyUrl: null, // CTAN proxy URL (enables CTAN fetching when set)
workerUrl: null, // Custom worker URL (uses embedded worker if null)
// Feature flags
enableCtan: false, // Auto-enabled when ctanProxyUrl is set
enableLazyFS: true, // Load files on-demand (faster startup)
enableDocCache: true, // Cache compiled documents by preamble hash
// Performance tuning
maxRetries: 15, // Max retries for CTAN/bundle fetches per compile
eagerBundles: {}, // Bundles to load immediately (see below)
verbose: false, // Log TeX stdout (disable for performance)
// Callbacks
onLog: (msg) => {}, // Log callback
onProgress: (stage, detail) => {}, // Progress callback
});By default, large bundles like cm-super (fonts) are loaded on-demand when TeX requests them. This saves bandwidth but adds latency on first use. To pre-load specific bundles:
// Load cm-super fonts eagerly for all engines
const compiler = new SiglumCompiler({
eagerBundles: ['cm-super'],
});
// Or per-engine configuration
const compiler = new SiglumCompiler({
eagerBundles: {
pdflatex: ['cm-super'],
xelatex: [], // XeLaTeX uses system fonts
},
});Controls how many times the compiler retries when a package or bundle fetch fails. With pre-scanning (which batch-fetches most packages before compilation), retries are rare. Lower values fail faster if something is broken:
const compiler = new SiglumCompiler({
maxRetries: 5, // Fail fast (default: 15)
});Controls whether TeX stdout is sent to the onLog callback. Disabled by default for performance—a typical compilation generates ~4,000 log lines, each requiring a postMessage call from the worker.
const compiler = new SiglumCompiler({
verbose: true, // Enable TeX stdout logging (default: false)
onLog: (msg) => console.log(msg),
});
// Can also be changed after instantiation
compiler.verbose = true; // Enable for next compile
await compiler.compile(source);
compiler.verbose = false; // Disable againWhen verbose: false:
- TeX stdout (
[TeX] ...) is suppressed - TeX errors (
[TeX ERR] ...) are always logged - Worker status messages are always logged
- Error detection still works (stdout is captured internally)
const result = await compiler.compile(source, {
engine: 'pdflatex', // 'pdflatex' | 'xelatex' | 'auto'
additionalFiles: { // Include custom files
'mypackage.sty': '\\ProvidesPackage{mypackage}...',
'image.png': uint8Array,
},
});
// result.success — boolean
// result.pdf — Uint8Array (if successful)
// result.log — TeX log output
// result.error — error message (if failed)Clear all cached packages and compiled PDFs.
Free memory by unloading the WASM module. Call init() again to reload.
Helper to batch log messages and avoid DOM thrashing. The TeX compiler emits hundreds of log lines during compilation and updating the DOM on each message can cause significant slowdowns.
import { SiglumCompiler, createBatchedLogger } from '@siglum/engine';
const compiler = new SiglumCompiler({
bundlesUrl: '/bundles',
wasmUrl: '/wasm/busytex.wasm',
onLog: createBatchedLogger((messages) => {
// Called once per animation frame with all buffered messages
logDiv.textContent += messages.join('\n') + '\n';
logDiv.scrollTop = logDiv.scrollHeight;
}),
});If you're displaying compiler logs in the UI, always use createBatchedLogger or implement your own batching. Unbatched DOM updates can add 2-3 seconds to compilation time.
Call compiler.init() early (e.g., on page load) so bundles are ready when the user compiles:
// On page load
const compiler = new SiglumCompiler(options);
compiler.init(); // Fire and forget — bundles download in background
// Later, when user clicks compile
await compiler.compile(source); // Already warmed up| Engine | Status |
|---|---|
| pdfLaTeX | Full support, format caching |
| XeLaTeX | Full support, custom fonts via fontspec |
Use engine: 'auto' to auto-detect based on document content.
Download assets from cdn.siglum.org or GitHub Releases:
| Asset | Size | Description |
|---|---|---|
busytex.wasm |
29 MB | WebAssembly TeX engine |
busytex.js |
292 KB | Emscripten glue code |
siglum-bundles-v0.1.0.tar.gz |
~195 MB | LaTeX packages & fonts |
Serve with these headers (required for SharedArrayBuffer):
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Access-Control-Allow-Origin: *
Cross-Origin-Resource-Policy: cross-origin
Expected structure after extracting:
your-server.com/
├── busytex.wasm
├── busytex.js
└── bundles/
├── bundles.json
├── file-manifest.json
├── file-to-package.json
├── package-deps.json
└── *.data.gz (54 bundle files)
The CTAN proxy fetches missing LaTeX packages on-demand:
bun packages/ctan-proxy.tsPackages are cached permanently. The proxy tries TexLive 2025 archives first, then falls back to CTAN mirrors.
For configuration and deployment options, see docs/ctan-proxy.md.
- Modern browser with WebAssembly support
- SharedArrayBuffer (requires COOP/COEP headers)
- ~512MB RAM for compilation (max heap size)
Built on BusyTeX.
MIT