Lumis Docs

Languages

Let Lumis load parsers on demand, or preload the languages you know you need.

A language in Lumis is a Tree-sitter parser: a WebAssembly module that Lumis downloads, verifies, compiles, and loads. You have two ways to get one, and you can use both.

On demandPreloaded
What you writenothinga list of languages, once
First document in a languagepays the download and the compilealready loaded
Every document afterfull speedfull speed
A language you did not expectworksworks, on demand

On demand is the default and needs no setup. Highlight a document and Lumis loads whatever that document turns out to name. Preloading is the same work, moved off your users' first request — it is the "pick a language" step done ahead of time instead of during a request.

Both end in the same place. Preloading only decides when you pay.

On demand

Nothing to declare. Lumis resolves, downloads, verifies, and loads a language the first time a document names it, then reuses it for every later document.

That includes languages injected inside another one, in the same pass: a Markdown file with a fenced Rust block highlights that block, and an HTML page highlights its <style> and <script> contents, without any of them appearing in your code.

A language that cannot be fetched costs its own block, not the document. A thousand-line file still highlights when one fenced block names something unpublished; that block stays plain.

The cost is one download and then a Wasmtime compile, and the compile is the larger half: seven parsers already on disk take about 8 s to compile, against 1.3 s once their compiled forms are stored. Registering an already-compiled parser is 3-15 ms, and about 0.3 ms after that. So the penalty is real but it is paid once per language, by whichever request arrives first.

That is fine for a CLI, a script, a build step, or anything where one slow first render does not matter. Preload when it does.

:::note Browsers are the exception. web-tree-sitter loads asynchronously, so a parser cannot be fetched inside a synchronous walk: in a browser, preload an injected language before highlighting the document that mentions it, or use a bundle. Node does not have this limit — it highlights through a native addon. :::

Preloaded

Name the languages you want. Which call depends on whether the process doing the work is the one that will serve:

RuntimeIn the process that servesIn a process that exits
JavaScript / TypeScriptloadLanguages([...]), not awaitedcacheLanguages([...])
ElixirLumis.Languages.async_load([...]) in start/2Lumis.Languages.cache/2
Browsers / CDNcreateHighlighter({languages}), or loadLanguages()— nothing to prepare ahead
CLI— the process exits, so there is no later to hold forlumis languages cache <names>
Rust— parsers are Cargo features compiled into the binary
Javaconstruct Lumis at startup and retain it— parsers ship inside lumis4j

The columns are the two verbs, spelled the same way everywhere. load caches the language and keeps it in this runtime, so nothing loads it again. cache only puts it on disk, where it survives restarts and is shared by every native runtime pointed at the same directory.

Load is cache plus keeping it, so a process that will serve wants a load. Cache is for a process that is not serving — an image build, a release task, a CLI run — which has nothing to hold on to. Caching on a native runtime compiles as well, so a directory prepared that way saves the serving process both halves.

:::note Preloading is an optimization, never a prerequisite. An application that starts cold serves correctly the whole time it is loading, so preloading must never delay a boot or fail one — every example below keeps both true. :::

Which languages to name

Name your root languages and anything they can inject. Markdown may need the languages used in fenced code blocks, and HTML may inject CSS and JavaScript. Prefer a focused list or a bundle over every parser unless your application genuinely accepts every supported language. Anything you miss still loads on demand.

Language catalog has every name, and the package each one resolves to.

All of these operations are safe to repeat. A normal run reuses valid files; force: true or --force is an explicit update that resolves the compatible package range again.

Preloading, per runtime

Start serving, then load in the background:

bootstrap.ts
import { loadLanguages } from "@lumis-sh/lumis";
await startServer();
// Not awaited, so a slow CDN delays no request. The
// `.catch()` is required: an unhandled rejection
// terminates the process, which is exactly the failure
// loading in the background is meant to avoid.
loadLanguages(["javascript", "html", "css"]).catch(
(error) => {
logger.warn(
{ error },
"Lumis preload failed; languages load on demand",
);
},
);

loadLanguages() warms the same default runtime the module-level highlight() uses, so nothing has to be threaded through your application. It accepts catalog names, aliases, and bundle names such as bundle-web. Every name is attempted; if any fail it rejects with an AggregateError naming each one, after the rest have loaded.

createHighlighter({languages}) is the same load into an instance you keep yourself, and is what browsers use.

Await cacheLanguages() instead when a build or prestart step is preparing a directory the serving process will read — failing is the point of a preparation step:

await cacheLanguages(["bundle-web"], { force: true });

{force: true} resolves the compatible package range again and replaces valid cached files. On the native Node addon, caching also validates the parser and queries and persists the compiled Wasmtime module. Bun and Deno, or a Node installation without the addon, can call the same API, but the portable web-tree-sitter fallback cannot persist compiled modules across processes.

Point every process at one directory

The CLI, Elixir NIF, and native Node addon share one store, so a parser one of them downloads is already there for the others. Name it explicitly when one process prepares files for another:

HostExplicit settingEnvironment fallbackDefault
CLI--data-dir /app/lumisLUMIS_DATA_DIRplatform user data directory
JavaScript / TypeScript cache API{directory: "/app/lumis"}LUMIS_DATA_DIRplatform user data directory
Elixirconfig :lumis, data_dir: "/app/lumis"LUMIS_DATA_DIRthe Lumis application priv/lumis directory

Elixir configuration takes precedence over the environment. The npm directory option controls where cacheLanguages() writes; also set LUMIS_DATA_DIR for the deployed highlighter when it must read that explicit directory.

WASM and CDN has the directory layout, integrity checks, custom resolvers, and offline images.

On this page