Skip to content
HN On Hacker News ↗

Real-Time LuaTeX: Recompiling Large Documents in 1ms [pdf]

▲ 66 points • 23 comments • by amichail • 4mo ago • HN discussion ↗

Pangram verdict · v3.3

We believe that this document is a mix of AI-generated, AI-assisted, and human-written content

75 %

AI likelihood · overall

Mixed
18% human-written 67% AI-generated
SEGMENTS · HUMAN 0 of 5
SEGMENTS · AI 4 of 5
WORD COUNT 1,576
PEAK AI % 99% · §5
Analyzed
Jul 18
backend: pangram/v3.3
Segments scanned
5 windows
avg 315 words each
Distribution
18 / 67%
human / AI fraction
Verdict
Mixed
Pangram v3.3

Article text · 1,576 words · 5 segments analyzed

Human AI-generated
§1 Mixed · 61%

TUGboat, Volume 0 (9999), No. 0 draft: June 13, 2026 03:22 ?1 Real-Time LuaTEX: Recompiling Large Documents in 1ms Clemens Lode Abstract This paper shows that pixel-perfect LATEX output and real-time editing are not mutually exclusive. Line breaking is paragraph-local; page breaking is global but not perceptually immediate and therefore need not block the editing loop. By extracting display lists directly from Lua- TEX’s node structures (bypassing PDF generation), a single paragraph can be recompiled and its dis- play list delivered in ∼1 ms, using vanilla TEX Live with no engine modifications and full microtype [6] support. We demonstrate that this approach is sufficient to build software that can recompile large documents in real time: texlode, a browser-based book author- ing tool with output identical to standard LuaLATEX. Benchmarks show that per-paragraph recompilation achieves O(1) latency, constant regardless of docu- ment size, whereas Typst’s [3] incremental compila- tion scales linearly (O(n)). The tradeoff is temporary inconsistency: pages the user is not viewing may lag until a background compile converges, the same pattern word processors have used for decades. 1 The speed problem LuaLATEX compilation scales poorly with document size. A 100-page book may take 10–30 seconds to compile, making interactive editing (even with caching) impossible. Cloud compilation (for example, Over- leaf) helps with toolchain complexity but not with the fundamental bottleneck: long documents with com- plex preambles, OpenType fonts, and full microtype settings are computationally costly. Word processors solved this decades ago. Word, InDesign, and Google Docs recompile only the edited paragraph on each keystroke; the rest of the docu- ment stays put until a background process recon- verges global layout. LATEX has never worked this way, not because TEX’s algorithms are too slow, but because its architecture treats compilation as a mono- lithic, file-in-to-file-out operation. This paper grew out of a decade of providing LATEX publishing services, over thirty client books, and the author’s own How to Publish Your Book with LATEX [5].

§2 AI · 84%

The book documents the complete self- publishing workflow using LuaLATEX, and in doing so, documents the pain: client books with complex fonts routinely hit Overleaf’s compile timeout. It recommends using draft mode during editing, then switching to a local installation for final output, forc- ing authors to choose between typographic quality and interactive feedback. The question this paper addresses is not how to make LuaTEX faster, but whether full compilation is even necessary during editing. 2 Prior approaches The LATEX community and its neighbors have tried several strategies to make typesetting feel interactive. TeXpresso [1] achieves near-real-time XE TEX ren- dering through process snapshotting. Every ∼500 ms of processing, the driver forks the en- gine, creating a copy-on-write checkpoint. When an edit arrives, it finds the most recent check- point before the change and re-runs from there. This incrementality with full reruns from opti- mal starting points requires a modified XE TEX engine and fork(), limiting it to Linux and macOS. SwiftLaTeX and BusyTeX [7, 8] compile TEX Live to WebAssembly, enabling client-side LATEX in the browser. But compilation still processes the full document each time. Typst rebuilt typesetting from scratch around in- cremental compilation. Every element depends only on explicit inputs, enabling automatic mem- oization at the engine level. The result is fast (single-digit milliseconds for small documents), and the engine is clean, modern, and well-engineered. But it is inherently incompatible with LATEX, and its incremental recompilation still scales linearly with document size. These approaches share an assumption: that the unit of compilation is the document. They differ in how to make that unit cheaper: better check- pointing, smarter memoization, faster engines. None asks whether full compilation is necessary in the first place. 3 The measurement How long does LuaTEX actually need to typeset a paragraph? We can find out by wrapping paragraph text between two \directlua timing calls, as in Figure 1; running the file with lualatex prints the time to the terminal. On a mid-range laptop (Intel Core i7-1355U, TEX Live 2025), this consistently reports under 1 ms.

§3 AI · 97%

Because a single compile is often faster than the res- olution of os.gettimeofday(), a more systematic benchmark amortizes the timer — each sample is the mean of 100 consecutive in-session compiles — and ?2 draft: June 13, 2026 03:22 TUGboat, Volume 0 (9999), No. 0 % paragraph-benchmark.tex \documentclass{book} \usepackage{microtype} \begin{document} \directlua{t0 = os.gettimeofday()}% The quick brown fox jumps over the lazy dog. \par \directlua{ local ms = (os.gettimeofday() - t0) * 1000 texio.write_nl("Paragraph: " .. math.floor(ms * 100 + 0.5) / 100 .. " ms") } \end{document} Figure 1: A self-contained single-paragraph timing example; run with lualatex and read the terminal output. Table 1: Per-paragraph line-breaking time by paragraph type in vanilla LuaLATEX (median, P5, and P95 over 30 amortized samples). Paragraph length Median P5 P95 Short (1 line) 0.17 ms 0.03 ms 0.96 ms Medium (4–5 lines) 0.70 ms 0.65 ms 0.89 ms Long (10+ lines) 1.99 ms 1.87 ms 2.41 ms Inline math 0.20 ms 0.17 ms 0.27 ms Display math 0.11 ms 0.10 ms 0.13 ms reports median, P5, and P95 over 30 such samples after a 5-sample warmup: The dominant cost is line breaking, not content complexity: a one-line paragraph with inline math (0.20 ms) is faster than a plain five-line paragraph (0.70 ms). Display math is particularly fast because it produces a single-line box with no break-point search.

§4 AI · 97%

4 Why one paragraph is enough A single paragraph is a meaningful unit of work because of a structural property of TEX, evident in the Knuth–Plass line-breaking algorithm [4]: Line breaking is local. Given the same \hsize and content, TEX produces identical line breaks regardless of what appears before or after the paragraph. Changing one paragraph does not affect how any other paragraph breaks into lines. Page breaking is global. A paragraph’s height change can cascade through the rest of the document: subsequent paragraphs may shift to different pages, floats may move, page numbers may change. This separation is exactly what word proces- sors exploit. During editing, the user is looking at one paragraph on one page. The only operation that changes what they see at that moment is line breaking, and line breaking is local. Page breaking matters, but it does not need to happen on the same keystroke. A background process of the word pro- Table 2: Median per-paragraph compile time in each window. No degradation has been observed; the variation between windows is noise, the variation between columns reflects paragraph length. Compile window Short Multi-line 1–50 0.12 ms 1.89 ms 226–275 0.09 ms 1.84 ms 451–500 0.09 ms 1.93 ms cessor can reconverge global layout while the user continues typing. The challenge for LATEX is that TEX was never designed to work this way. Word processors have built-in paragraph renderers that can be called as functions. TEX is a batch processor: it reads files, processes the entire document, and writes output. There is no API for “compile this one paragraph and return the result”. Our engineering contribution, texlode, adapts LuaTEX to this pattern: keeping the engine alive, feeding it individual paragraphs, and extracting re- sults from internal structures that were not meant to be an output format. To verify that compile time is truly independent of document state, we ran 500 consecutive paragraph compiles in a single LuaTEX engine session: The locality claim has boundaries. Footnotes couple a paragraph to page breaking; counter-dependent content (\ref, \thepage) requires global state; \parshape set by surrounding code is unavailable in isolation.

§5 AI · 99%

These cases fall to the background convergence path, the same full compile that resolves page numbers and float placement. The fast path handles body text, which is where the user spends most editing time and where latency matters most. 5 From measurement to real-time editor Compiling only the edited paragraph and converging global layout in the background is how every modern word processor works. The engineering challenge is adapting LuaTEX’s batch-mode architecture to support it. Keeping the engine alive. LuaTEX startup takes ∼1 second for a book-class preamble with Open- Type fonts. We maintain a persistent LuaTEX process that accepts paragraph content, com- piles it, and returns the result without restart- ing. Capturing output without PDF. Generating a PDF for a single paragraph would add signifi- cant overhead. Instead, we extract the display list directly from LuaTEX’s node structures after line breaking: a stream of positioned glyph com- mands with coordinates in scaled points. The TUGboat, Volume 0 (9999), No. 0 draft: June 13, 2026 03:22 ?3 Table 3: Time breakdown for a single per-paragraph round trip. Pipeline stage Short Medium LuaTEX (line break + traversal) 0.65 ms 4.99 ms IPC + serialization 0.12 ms 1.03 ms Deserialization + expansion 0.02 ms 0.09 ms Total round-trip 0.79 ms 6.11 ms traversal must correctly replicate TEX’s internal position calculations, including all microtype adjustments, to produce output identical to stan- dard LuaTEX compilation. TEX was never de- signed to expose its internal rendering state; extracting a pixel-identical display list from Lua- TEX’s post-line-break node structures required reverse-engineering internal conventions with no prior precedent. Rendering in the browser. The glyph stream is sent to the browser and drawn to HTML5 Canvas via opentype.js. Each glyph command maps to a Canvas path at the coordinates specified by the engine. Rules (filled rectangles) and color operations complete the primitives. Background convergence. A full document com- pile runs periodically, producing a complete page-position cache. We overlay fast-path para- graphs at cached positions, so the user sees live line breaking with correct global layout.