44.4% smaller than original
Terser · 118,031 Breact-markdown@10.1.0 · lil2 family · @itslil/lil2-react-markdown
react-markdown,
flat.
react-markdown on the whole flat stack: one LilScript program from characters to React elements. Components get node ids into the hast columns; tags, properties and plugins are int ids.
npm install @itslil/lil2-react-markdown katex31,169 B upstream (Terser) → 19,341 B lil2
Raw, gzip and Brotli each use a separate LilScript compilation targeting that objective; upstream is the smallest of Terser, esbuild and Oxc on the same API bundled for the browser.
A chat app · Playwright · phone CPUs
Streaming chats,
2.5× less CPU.
| short chat (5 replies) | average chat (20 replies) | long chat (60 replies) | |
|---|---|---|---|
| CPU while the replies stream, mid-tier phone (4×) | 7.1 s → 3.2 s 2.2× | 29.1 s → 11.5 s 2.5× | 1.3 min → 31.0 s 2.5× |
| CPU while the replies stream, low-end phone (6×) | 11.2 s → 4.7 s 2.4× | 45.9 s → 17.7 s 2.6× | 2.0 min → 47.4 s 2.5× |
| CPU while the replies stream, this machine | 1.6 s → 0.8 s 2.1× | 6.8 s → 2.7 s 2.5× | 17.6 s → 7.0 s 2.5× |
| updates slower than a frame (16.7 ms), low-end phone (6×) | 125 → 6 of 1,053 | 671 → 6 of 4,615 | 1,318 → 67 of 12,897 |
| opening the saved chat, low-end phone (6×) | 417 ms → 317 ms 1.3× | 843 ms → 519 ms 1.6× | 1.77 s → 960 ms 1.8× |
LLM-style replies (lists, code, tables, math; about 2.5 KB each) streamed a few tokens at a time and rendered by React, finished messages memoized. Chrome's CPU throttling stands in for phones (4×: Lighthouse's mid-tier mobile; 6×: DevTools' low-end mobile); median of 2 runs, libraries alternating, fresh tab each. Every streamed update renders the same DOM as react-markdown (test/chat.test.mjs, Chromium and Firefox). Reproduce: npm run bench:chat.
The lil2 family
This package,
then the stack.
Every layer of the react-markdown stack, rewritten bottom to top with ints for every id and enum and flat arrays for every tree. Each tab is one package: its size against minified upstream for each objective, its speed in real browsers, and the checks that its output equals upstream’s.
lil2-react-markdown
react-markdown on the whole flat stack: one LilScript program from characters to React elements. Components get node ids into the hast columns; tags, properties and plugins are int ids.
react-markdown@10.1.0
API<Markdown plugins={[GFM, MATH, KATEX]} components={[TAG_H1, H1]}>{markdown}</Markdown>
BehaviourSame rendered HTML as react-markdown on 736 documents under nine option sets; the gfm flavor equals react-markdown + remark-gfm on 1,453 documents under seven; the full flavor equals react-markdown + remark-gfm + remark-math + rehype-katex + remark-breaks under eight.
Cells3 of 3 smaller
Both builds, in this page
Live
lab.
Left is markdown. Right is lil2-react-markdown/full rendering it as you type. Race times
react-markdown with remark-gfm, remark-math and rehype-katex against lil2 on the current text, both loaded here.
The output is rendered by the shipped browser build; the race runs each side for 400 ms after a warm-up.
Browser build · minified upstream
Three objectives.
Measured separately.
36.5% smaller than original
Terser · 35,017 B37.9% smaller than original
Terser · 31,169 B| Compiler objective | LilScript | Original minified | Original minifier | Difference |
|---|---|---|---|---|
| Raw | 65,669 B | 118,031 B | Terser | 44.4% smaller |
| gzip-9 | 22,230 B | 35,017 B | Terser | 36.5% smaller |
| Brotli-11 | 19,341 B | 31,169 B | Terser | 37.9% smaller |
Each LilScript row comes from a separate compilation targeting that row’s objective. Raw is uncompressed JavaScript; gzip uses level 9; Brotli uses quality 11 and window 22. For each metric, the original is the smallest result among the minifiers below. No JavaScript minifier runs after LilScript.
Every artifact, measured with all three codecs
| Artifact | Raw | gzip-9 | Brotli-11 |
|---|---|---|---|
| LilScript · Raw objective | 65,669 B | 23,535 B | 20,380 B |
| LilScript · gzip-9 objective | 67,335 B | 22,230 B | 19,363 B |
| LilScript · Brotli-11 objective | 67,480 B | 22,255 B | 19,341 B |
| Original · Terser 5.51.2 | 118,031 B | 35,017 B | 31,169 B |
| Original · esbuild 0.28.1 | 120,485 B | 37,000 B | 33,090 B |
| Original · Oxc vite 8.2.1 | 118,061 B | 35,463 B | 31,702 B |
Browser builds on both sides: lil2’s `browser` condition and upstream bundled by esbuild 0.28.1 with the browser condition (`export {default as Markdown, defaultUrlTransform} from 'react-markdown'`). Upstream: react-markdown@10.1.0. External dependencies excluded from sizes: react, react/jsx-runtime. CSS, fonts, type declarations and source maps are outside these JavaScript sizes.
Measurements, hashes and settings ↗ · Commands and build receipts ↗
Real browsers · identical output first
Speed.
| Document | Upstream | lil2 | Time |
|---|---|---|---|
| chat | 0.817 ms | 0.313 ms | 0.38× |
| readme | 16.300 ms | 7.800 ms | 0.48× |
| large | 179.900 ms | 70.200 ms | 0.39× |
| gfm | 15.650 ms | 5.500 ms | 0.35× |
| math | 17.750 ms | 9.800 ms | 0.55× |
| load (import) | 9.8 ms | 5.4 ms | 0.55× |
| first call, cold | 19.9 ms | 15.8 ms | 0.79× |
| Document | Upstream | lil2 | Time |
|---|---|---|---|
| chat | 1.571 ms | 0.667 ms | 0.42× |
| readme | 34.000 ms | 14.000 ms | 0.41× |
| large | 410.000 ms | 136.000 ms | 0.33× |
| gfm | 26.000 ms | 11.000 ms | 0.42× |
| math | 21.000 ms | 15.000 ms | 0.71× |
| load (import) | 18.0 ms | 12.0 ms | 0.67× |
| first call, cold | 15.0 ms | 14.0 ms | 0.93× |
Playwright, one fresh browser context per lane; lanes interleave and alternate order every round. Upstream is the same API bundled from npm for the browser; lil2 is the shipped browser build. Load is the module import after the file is fetched; first call is the first document, cold.
Same machine · sequential builds
Build time.
| Output objective | LilScript compilation | Original bundling + minification | Original minifier |
|---|---|---|---|
| Raw | 33.75 s | 1.01 s | Terser |
| gzip-9 | 33.75 s | 1.01 s | Terser |
| Brotli-11 | 34.00 s | 1.01 s | Terser |
One fresh output build per objective and minifier, run sequentially. LilScript compilation includes optimization and objective scoring with persistent caches disabled and one worker. Original time includes Node startup, esbuild bundling of installed upstream ESM, and the named minifier. It excludes building the upstream package from its original TypeScript sources. Dependency installation, tests and final file compression are excluded on both sides. These are observations on a shared machine, not a build-speed guarantee.
AMD EPYC 7763 64-Core Processor · 8 logical CPUs · 15.6 GiB RAM · Linux 7.0.0-1014-azure · Node v20.19.0. Measured 2026-10-04. Exact commands and compiler identity ↗
Install
Typed, flat,
same output.
npm install @itslil/lil2-react-markdown katex