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.
| Runtime | cache | load |
|---|---|---|
| Elixir | Lumis.Languages.cache/2 | Lumis.Languages.async_load/1, load/1 |
| Server-side JavaScript | cacheLanguages() | loadLanguages() |
| Browsers / CDN | automatic, verified parser bytes only | loadLanguages(), or createHighlighter({languages}) |
| CLI | lumis 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 lumis4j | construct 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
- JavaScript
- Elixir
- CLI
- Browsers / CDN
- Rust
- Java
Start serving, then load in the background:
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.
Call Lumis.Languages.async_load/1 from your application's start/2:
def start(_type, _args) do
Lumis.Languages.async_load(["markdown", "elixir", "javascript"])
Supervisor.start_link(children(),
strategy: :one_for_one,
name: MyApp.Supervisor
)
end
It returns before the download starts, so the boot never waits on the network.
The return value is deliberately not matched: the work runs under a
:temporary child of Lumis's own supervisor, so a failure is logged and never
retried, never escalates, and cannot stop the application from starting.
The API accepts language names and :bundle_* names such as :bundle_web.
Use Lumis.Languages.load/1 when the caller does want the result and can wait
— a release task, or a smoke test that should fail if a parser is unreachable.
For release and container details, see the standalone
Elixir deployment guide.
Use the CLI when cache preparation belongs outside the application lifecycle: an image build, a release task, or an operations step. It fills the store for a process that has not started yet, which is the case a startup warm-up cannot cover.
lumis languages cache rust javascript elixir
lumis languages cache bundle-web
lumis languages cache --all
Refresh compatible versions already in the store with --force:
lumis languages cache rust javascript --force
For an image build, name the destination explicitly and copy that same directory into the runtime image:
FROM node:22-bookworm-slim AS parsers
RUN npx --yes @lumis-sh/cli --data-dir /app/lumis languages cache markdown elixir javascript
FROM debian:bookworm-slim
COPY --from=parsers --chown=nobody:root /app/lumis /app/lumis
ENV LUMIS_DATA_DIR=/app/lumis
The first stage only needs a Node image because npx fetches the CLI; nothing
from it reaches the runtime image except the directory. Install the binary
instead — see Installation — if you would rather not reach the
network during a build.
Lumis.Languages.cache/2 and cacheLanguages() do the same preparation from
Elixir and JavaScript, for a release task that runs before the process that
serves. Keep the startup warm-up as well: a prepared directory removes the
download and the compile, and the warm-up still has to move the parsers into
the runtime, which needs no network once that directory is there.
loadLanguages() works here too, warming the runtime the module-level
highlight() uses. When you keep a highlighter instead, load into it and retain
that instance:
import { createHighlighter } from "@lumis-sh/lumis";
import css from "@lumis-sh/lumis/langs/css";
import html from "@lumis-sh/lumis/langs/html";
import javascript from "@lumis-sh/lumis/langs/javascript";
const highlighter = await createHighlighter({
languages: [html, css, javascript],
});
await renderApplication({ highlighter });
Pass eager language imports when they must be ready immediately. A bundle
registers lazy entries; call highlighter.loadLanguage() for the bundle members
the application must warm.
Verified parser bytes persist in CacheStorage, with an IndexedDB fallback for
WebKit. WebAssembly compilation belongs to the browser process and cannot be
persisted in Lumis's server-side compiled-module cache. Browsers do not use
LUMIS_DATA_DIR.
The Rust crate has no parser download or runtime compilation step. Select the languages at build time with Cargo features; their parser code is compiled into the application:
[dependencies]
lumis = { version = "0.1", default-features = false, features = ["bundle-web", "lang-rust"] }
There is no warm-up command, parser data-directory environment variable, or runtime parser-cache configuration for the Rust crate.
lumis4j ships its Lumis WebAssembly module and languages with the Java
artifact. Construct the Lumis instance during application startup and retain
it for the application lifecycle so module initialization happens before the
first request:
import io.roastedroot.lumis4j.core.Lumis;
var lumis = Lumis.builder().build();
startServer(lumis);
// Call lumis.close() from the application's shutdown lifecycle.
There is no parser cache command, LUMIS_DATA_DIR, or parser-store
configuration in lumis4j.
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/
| Host | Explicit setting | Environment fallback | Default |
|---|---|---|---|
| CLI | --data-dir /app/lumis | LUMIS_DATA_DIR | platform user data directory |
| JavaScript cache API | {directory: "/app/lumis"} | LUMIS_DATA_DIR | platform user data directory |
| Elixir | config :lumis, data_dir: "/app/lumis" | LUMIS_DATA_DIR | the 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 :lumis, data_dir: "/app/lumis"
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.