Ghostty in the Browser: 404KB WASM Replaces xterm.js's Handwritten Parser

ghostty-web compiles Ghostty's Zig-based terminal parser into a 404KB WebAssembly module, offering an xterm.js-compatible API with superior compatibility for complex scripts and escape sequences by minimizing WASM boundary crossings through a RenderState snapshot approach.

Geek Labs
Geek Labs
Geek Labs
Ghostty in the Browser: 404KB WASM Replaces xterm.js's Handwritten Parser

Background: The Web Terminal Stack Has Not Changed in a Decade

For ten years, web terminals — VS Code's integrated terminal, GitHub Codespaces, online SSH tools, cloud IDEs — have relied almost exclusively on xterm.js. It is a solid project that powers the entire web terminal ecosystem. However, xterm.js re-implements terminal emulation entirely in JavaScript: VT100 spec, hundreds of ANSI escape sequences, Unicode edge cases, state machines, screen buffers, and scrollback. This handwritten parser creates a massive compatibility matrix where a fix in one area can break another.

Ghostty Takes a Different Path

Ghostty is a native terminal written in Zig by Mitchell Hashimoto, released in late 2024. Its VT parser has run millions of hours in real user sessions on macOS, where it is considered one of the fastest terminals. ghostty-web compiles that exact parser to WebAssembly and runs it in the browser.

Architecture: TypeScript UI + 404KB WASM Parser

The project delivers a JavaScript library plus a single .wasm file of 404 KB (smaller gzipped), with zero runtime dependencies. The division of labor is clean:

TypeScript side (browser) : DOM, canvas rendering, keyboard input, selection, scrollbar.

WASM side (Ghostty core) : Receives byte streams, parses them into a grid of cells.

Glue layer : Communicates between the two.

Because the browser runs the same parser as the desktop terminal, programs like vim, htop, tmux, and less — with their tricky escape sequences — behave identically. The project's own test suite covers 95 tests across terminal, renderer, input handling, and selection management.

ghostty-web online demo: 16-color, 256-color, true-color, and text styles all render correctly
ghostty-web online demo: 16-color, 256-color, true-color, and text styles all render correctly

Repository Structure

The lib/ directory contains about twenty files with clear responsibilities: terminal.ts — 2,000+ line main class implementing the ITerminalCore interface (same method names as xterm.js: open(), write(), onData(), resize(), buffer, dispose()). ghostty.ts — TypeScript wrapper for the WASM module: loads .wasm, creates terminal instances, translates JS calls to WASM. renderer.ts — Canvas renderer handling font metrics, DPI scaling, 256-color and true-color, bold/italic/underline/strikethrough, three cursor styles, dirty-line detection for stable 60 fps. input-handler.ts — Keyboard event to escape sequence translation, mouse reporting, Kitty keyboard protocol. selection-manager.ts — Selection and copy. link-detector.ts with providers/ — Detects OSC 8 explicit links and regex-matched URLs. addons/fit.ts — Equivalent of xterm.js's FitAddon: auto-calculates columns/rows on container resize. interfaces.ts — Replicates xterm.js type definitions ( ITerminalOptions, ITheme, IDisposable, ITerminalAddon) for drop-in compatibility.

Usage: Nearly Identical to xterm.js

Migration cost is minimal — change one import and add an await init() call to load the shared WASM instance once.

// Before
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';

// After
import { init, Terminal } from 'ghostty-web';
await init();
const term = new Terminal({ fontSize: 14, theme: { background: '#1a1b26', foreground: '#a9b1d6' } });
term.open(document.getElementById('terminal'));
term.onData(data => websocket.send(data));
websocket.onmessage = e => term.write(e.data);

All subsequent API calls, event subscriptions, and buffer reads work exactly like xterm.js. A quick demo is available via npx @ghostty-web/demo@next, which starts a local server on 127.0.0.1:8080 with a real shell connection. The demo binds only to loopback, protects the /ws endpoint with a per-run same-origin token, and rejects cross-origin WebSocket handshakes — explicit opt-in via HOST and GHOSTTY_ALLOWED_HOSTS is required for remote access.

Why It Stays Small: The RenderState Optimization

The main WASM overhead is not computation but boundary crossings . Each JS↔WASM call serializes parameters and results. A naïve "ask parser for each cell's color" approach would require ~2,000 calls per frame (80×24), killing performance. ghostty-web solves this by adding a dedicated RenderState interface to Ghostty (via patches/ghostty-wasm-api.patch, 64 KB). Each frame makes only two crossings:

Call WASM to compute all render data into a shared memory snapshot.

Read the entire snapshot back to JS.

The patch adds C interfaces for terminal creation, data write, render state update, and viewport readback. Its header comment states: "The key optimization is the RenderState API which provides a pre-computed snapshot of all render data in a single update call, avoiding multiple WASM boundary crossings." This reduces crossings from thousands per frame to two.

This patch is temporary. Ghostty's author is building libghostty, a stable library distribution. Once mature, ghostty-web will consume the official WASM release and the patch can be removed.

Concrete Advantages Over xterm.js

Two verifiable differences stand out:

Complex script rendering — Devanagari, Arabic, etc., require grapheme-cluster segmentation (not code-point segmentation). xterm.js has long-standing rendering bugs here; ghostty-web inherits Ghostty's correct grapheme handling.

XTPUSHSGR / XTPOPSGR support — Save/restore graphic attributes (finer-grained than tput smcup/rmcup). xterm.js had an open issue since 2024, still unsupported in 2026; ghostty-web supports them.

Additional capabilities listed in the changelog: true-color and 256-color, CJK IME input, iOS support, Kitty keyboard protocol, hyperlink parsing/click (OSC 8 + regex), smooth scrolling, alternate-screen scrollback, dynamic font scaling, DSR response handling (enables nushell). Benchmarks in bench/versus.ts compare throughput, cursor movement, and viewport reads; run with bun run to see numbers.

Where It Fits

Originally built for Mux (Coder's desktop app for running multiple parallel AI agents, each with its own high-frequency terminal). Under that load, xterm.js lagged far behind the native-parser WASM approach.

Broader use cases: cloud IDEs/web editors, web SSH clients/bastion frontends, online teaching/demo environments, CI log viewers with streaming color/cursor output, browser-based messenger terminals, container management panels, Kubernetes pod terminals. The rule of thumb: if you currently use xterm.js and users run real programs inside it, it's worth trying.

Should You Switch Now?

New projects: use it directly. Zero migration cost; init() + Terminal and you're running. You can even develop against xterm.js docs and swap the import at the end.

Existing projects: evaluate selectively. Two factors:

Addon ecosystem: beyond FitAddon, xterm.js addons (webgl, search, web-links, image, serialize) are not yet provided; you must implement or wait.

Maturity: version 0.4.0 (still 0.x) means API may change.

Known issues in 69 open issues include: dispose not cleaning document listeners, empty-cell buffer export returning empty string causing cursor collapse, incorrect Ctrl+Shift+C copy on Linux/Windows, missing ESC prefix for Alt+printable (breaks readline/tmux), IME target position offset after zoom. These are actively fixed in nightlies, but edge-case parity with xterm.js is not guaranteed yet.

Windows demo is unsupported (PTY server is Linux/macOS only), though the library itself runs on Windows.

The project's stated goal is xterm.js API compatibility; when libghostty matures, it will switch to the official WASM distribution, reducing maintenance burden over time.

Conclusion

xterm.js's decade of handwritten fixes is valuable and not easily displaced. But there are two fundamentally different roads: piling human effort onto a reimplementation, or reusing a battle-tested native parser. ghostty-web takes the second road. It's still 0.x with rough edges, but the direction is sound and the migration cost is low enough to try in ten minutes.

https://github.com/coder/ghostty-web
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Performance OptimizationFrontend DevelopmentZigWASMWebAssemblyxterm.jsGhosttyTerminal Emulation
Geek Labs
Written by

Geek Labs

Daily shares of interesting GitHub open-source projects. AI tools, automation gems, technical tutorials, open-source inspiration.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.