Open-source engineering
React Native markdown performance with Nitro
Inside react-native-nitro-markdown: C++ parsing, React Native rendering, and a practical plan for benchmarking markdown and streaming interfaces.
Separate parsing from rendering
A markdown interface performs several kinds of work: parsing the input, creating a document tree, rendering components, and updating those components as content arrives. A faster parser addresses one part of that path. It does not establish how quickly the complete screen becomes usable.
My react-native-nitro-markdown package uses a C++ parser built on md4c and exposes it through Nitro Modules. It supports CommonMark and GitHub Flavored Markdown, a headless document-tree API, and React Native components for rendering. The public repository, which has 300+ GitHub stars, contains the implementation and comparison harness.
Choose the boundary deliberately
The native parser boundary allows parsing and presentation to evolve independently. A headless consumer can work with the parsed tree, while an application can customize the React Native renderers and theme. Streaming interfaces also need to decide when partial content is parsed and how updates reach the visible list.
The headless parseMarkdown API returns its document tree synchronously. Native C++ execution and background-thread execution are different properties. Measure the full call, including the cost of returning data to JavaScript and rendering it.
A measured streaming optimization
The repository records an initial v0.12.2 experiment from September 8, 2026. Both versions of the streaming append helper ran in the same Hermes runtime on an Android 17 / API 37 arm64 emulator. The optimization avoided scanning the full document for fences on eligible plain-text appends.
For the 280,004 UTF-16-unit fixture, the table below reports quantiles of batch averages: 30 samples per version, 20 updates per sample, alternating execution order, with the first round discarded. The comparison also checked matching output and fallback decisions across 5,000 seeded cases.
This is a historical append-helper measurement. It excludes native parsing, rendering, frame rate, and physical-device performance. The linked comparison guide records the baseline commit, candidate hash, reproduction command, and subsequent performance reports.
| Batch-average quantile | Before | After |
|---|---|---|
| p50 | 3.283 ms | 0.033 ms |
| p95 | 3.982 ms | 0.078 ms |
A benchmark that helps a product decision
Use the repository’s comparison harness as a starting point, then reproduce the workload your application actually has.
- Record the device, OS, React Native version, package versions, build mode, input corpus, and enabled parser options.
- Measure parsing separately from the time to the first usable render. Report repeated samples and variation rather than selecting the fastest run.
- Include long documents, tables, code blocks, and incremental streaming updates when those are part of the product.
- Check memory use, interaction responsiveness, rendering correctness, and malformed-input handling alongside elapsed time.
- Repeat on iOS and Android release builds. Preserve the input files and commands with the results so another engineer can rerun them.
Integrating it into an Expo application
The parser requires native code, so an Expo application needs a development build with the Nitro dependency included. Expo Go does not contain that native module. Follow the package’s installation guide for current compatibility and rebuild after native dependency changes.
The repository includes rendering examples, streaming support, and a comparison guide. Use those entry points to evaluate the package against your product’s workload, device, and baseline.