Skip to main content

Warm up parsers

Warm-up moves parser setup ahead of the first request.

Two words, one meaning everywhere

Every runtime spells warm-up with the same two verbs:

  • cache puts a language on disk. It survives restarts and is shared by every native runtime pointed at the same directory.
  • load caches the language and keeps it in this runtime, so nothing loads it again.

Load is cache plus keeping it. Loading writes the same files caching does — verified parser bytes, and on the native runtimes their compiled Wasmtime module — and then holds the result in memory. So inside a process that will serve, loading is always the better of the two.

Caching exists for the process that is not the one serving: an image build, a release task, a CLI run that exits. There is nothing for those to hold on to.

Compiling belongs to caching, not loading. compiled/ is a directory, so a cache that compiles is still a cache; it is holding the language in memory that makes a load.

Runtimecacheload
ElixirLumis.Languages.cache/2Lumis.Languages.async_load/1, load/1
Server-side JavaScriptcacheLanguages()loadLanguages()
Browsers / CDNautomatic, verified parser bytes onlyloadLanguages(), or createHighlighter({languages})
CLIlumis languages cache— the process exits, so there is no later to hold for
Rust— parsers are selected with Cargo features and compiled into the binary
Java— the WASM module and languages ship with lumis4jconstruct Lumis at startup and retain it

Browsers cache verified parser bytes in CacheStorage, with an IndexedDB fallback for WebKit, but never a compiled module: WebAssembly compilation belongs to the browser and cannot be persisted by Lumis.

Warm-up must not own the boot

Warm-up is an optimization, never a prerequisite: highlighting loads what a document names on demand, so an application that starts cold serves correctly the whole time it is warming. It should therefore never be able to delay a boot or fail one, and every example below is written to keep both true.

Runtime APIs and commands

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
// warming in the background is meant to avoid.
loadLanguages(["javascript", "html", "css"]).catch((error) => {
logger.warn({ error }, "Lumis warm-up 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.

Caching without loading

cacheLanguages() writes the store and holds nothing, for a build or prestart step preparing a directory the serving process will read:

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

Await that one — failing is the point of a preparation step. {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 use the same API, but the portable web-tree-sitter fallback cannot persist compiled modules across processes.

Store directory and precedence

The CLI, Elixir NIF, and native Node addon use the same store layout. Point them at the same writable, persistent directory when one process prepares files for another:

$LUMIS_DATA_DIR/
parsers/ # exact package metadata and verified parser WASM
compiled/ # native Wasmtime compiled modules
themes/
HostExplicit settingEnvironment fallbackDefault
CLI--data-dir /app/lumisLUMIS_DATA_DIRplatform user data directory
JavaScript 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 JavaScript directory option controls where cacheLanguages() writes; also set LUMIS_DATA_DIR for the deployed highlighter when it must read that explicit directory.

config/runtime.exs
config :lumis, data_dir: "/app/lumis"
Environment alternative
export LUMIS_DATA_DIR=/app/lumis

Choose the warm set

Warm root languages and anything they can inject. For example, Markdown may need the languages used in fenced code blocks, and HTML may inject CSS and JavaScript. Prefer a focused list or bundle over every parser unless the application genuinely accepts every supported language.

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

For the store layout, integrity checks, custom resolvers, and offline images, continue with WASM and CDN.