react-native-text-engine
v0.4.0
Published
Fast, worklet-compatible text primitives for React Native
Maintainers
Readme
React Native Text Engine
Native text measurement, layout, and rendering primitives for React Native.
react-native-text-engine exposes those primitives to JavaScript and Worklet threads through three APIs:
TextViewrenders text from direct props.- Prepared text stores text and typography in a native handle that can be measured, laid out at different widths, and rendered later.
- Glyph fields draw a fixed grid of character cells whose contents can be replaced without rebuilding the grid.
Operations are synchronous and handle based, so measurement, layout, and grid updates can run in animation, virtualization, and custom layout code.
TextView and PreparedTextView support native range selection and cap-height alignment on iOS and Android.
Installation
yarn add react-native-text-engineWorklet helpers require react-native-worklets:
yarn add react-native-workletsOn iOS:
cd ios
pod installTextView
TextView is the direct rendering API. Text and typography are passed as props, and the view can be wrapped with Animated.createAnimatedComponent.
import { TextView } from 'react-native-text-engine';
export function PriceLabel() {
return <TextView color="#111" fontFamily="Inter" fontSize={17} fontWeight="700" tabularNumbers text="$123.45" />;
}The primary text input is the text prop. TextView also accepts text props, including:
allowFontScalinganchorToCapHeightcolorellipsizeModefontFamilyfontSizefontWeightfontStyleletterSpacinglineHeightnumberOfLinesselectabletabularNumberstextAlign
selectable enables native range selection.
anchorToCapHeight measures text from the first visible line's cap-height top to the last visible line's baseline, for alignment with icons and controls.
Inline Runs
Inline runs apply style ranges inside one string.
import { TextView, createTextViewRunPayload } from 'react-native-text-engine';
const runs = createTextViewRunPayload([
{ start: 0, end: 4, style: { fontWeight: '700' } },
{ start: 5, end: 9, style: { fontStyle: 'italic' } },
]);
<TextView text="Bold text" fontSize={17} lineHeight={24} {...runs} />;Run ranges are UTF-16 offsets. They must be sorted, non-overlapping, and inside the string bounds.
JSX Children
Plain textual children can be compiled to the text prop with the optional Babel plugin:
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: ['react-native-text-engine/babel-plugin'],
};TextView does not flatten arbitrary children during render. The plugin only rewrites child text when the conversion can happen at build time.
Prepared Text
Prepared text is text plus typography stored in native memory. The JavaScript object contains a numeric handle. That handle is valid for measurement, layout, line geometry, rendering, and Worklet calls until it is released.
import { createPreparedText, type TextMeasureStyle } from 'react-native-text-engine';
const style: TextMeasureStyle = {
fontFamily: 'Inter',
fontSize: 17,
lineHeight: 24,
};
const message = createPreparedText('Hello world', style);
const layout = message.layout({
width: 320,
maxLines: 3,
ellipsizeMode: 'tail',
});
layout.width;
layout.height;
layout.lineCount;
layout.lastLineWidth;
message.release();createPreparedText() prepares the string and style. layout() supplies the width and line options for a particular layout result.
PreparedTextView
PreparedTextView renders an existing prepared handle.
import { PreparedTextView, createPreparedText } from 'react-native-text-engine';
const title = createPreparedText('Prepared once', {
fontFamily: 'Inter',
fontSize: 20,
lineHeight: 26,
});
<PreparedTextView handle={title.handle} style={{ width: 320 }} />;Rendering does not release the handle. Release it when it is no longer needed.
One-Shot Measurement
One-shot measurement returns metrics without keeping a prepared handle.
import { measureText, measureTextWidth } from 'react-native-text-engine';
const width = measureTextWidth('123.45', {
fontSize: 17,
fontWeight: '700',
tabularNumbers: true,
});
const block = measureText('Long paragraph...', { fontSize: 17, lineHeight: 24 }, { width: 320 });Batch APIs
Prepared text functions accept either a single item or an array.
import { createPreparedText, layoutPreparedText, releasePreparedText } from 'react-native-text-engine';
const prepared = createPreparedText(messages, style);
const layouts = layoutPreparedText(prepared, { width: contentWidth });
releasePreparedText(prepared);The array form prepares, lays out, or releases many handles in one native call.
Line Geometry
lines() returns every visible line for a layout request.
nextLine() returns the next visible line beginning at a UTF-16 offset. Its width argument applies to that line, so callers can build variable-width text flows.
const lines = message.lines({ width: 320 });
const next = message.nextLine(0, 220);Line ranges use UTF-16 offsets. A line's end and width describe visible text. Trailing whitespace is not included.
Glyph Fields
A glyph field is a fixed columns by rows grid. Creation sets the grid geometry, typography, alignment, and style variants. Updates replace the glyph and variant index for each cell.
import { GlyphFieldView, createGlyphField, type GlyphFieldVariant } from 'react-native-text-engine';
const variants: readonly GlyphFieldVariant[] = [
{ color: 'rgba(196,163,90,0.18)', fontWeight: '300' },
{ color: 'rgba(196,163,90,0.92)', fontWeight: '800', fontStyle: 'italic' },
];
const field = createGlyphField({
columns: 40,
rows: 24,
fontFamily: 'Georgia',
fontSize: 18,
lineHeight: 20,
textAlign: 'center',
variants,
});
const cellCount = 40 * 24;
const glyphs = ' '.repeat(cellCount);
const variantIndices = new Uint8Array(cellCount);
field.update(glyphs, variantIndices);
<GlyphFieldView handle={field.handle} style={{ width: 360, height: 480 }} />;field.update() accepts a string of glyphs or a Uint8Array of indices into glyphPalette. variantIndices selects the style variant for each cell.
Every update must match the grid:
glyphs.length === columns * rows, orglyphIndices.length === columns * rowsvariantIndices.length === columns * rows- every variant index points to an entry in
variants
field.release() frees the native grid.
Worklets
Importing the Worklets entrypoint installs Text Engine functions into the Worklets UI runtime.
import { measureTextsInRuntime } from 'react-native-text-engine/worklets';The Worklets entrypoint exports:
createPreparedTextsInRuntime()measureTextsInRuntime()layoutPreparedTextsInRuntime()layoutNextLineInRuntime()updateGlyphFieldInRuntime()createGlyphFieldBuffersInRuntime()commitGlyphFieldBuffersInRuntime()
import { layoutNextLineInRuntime, updateGlyphFieldInRuntime } from 'react-native-text-engine/worklets';
const next = layoutNextLineInRuntime(prepared.handle, 0, 220);
updateGlyphFieldInRuntime(field.handle, glyphs, variantIndices);createTextEngineRuntime() creates a dedicated Worklets runtime with Text Engine already installed. If an initializer is provided, it runs after installation.
import { createTextEngineRuntime } from 'react-native-text-engine/worklets';
const runtime = createTextEngineRuntime({ name: 'text-engine-layout' });App Defaults
The package ships with no app-wide defaults.
An app can define build-time defaults in react-native-text-engine.config.ts or react-native-text-engine.config.js at the app root:
import { defineTextEngineDefaults } from 'react-native-text-engine/config';
export default defineTextEngineDefaults({
anchorToCapHeight: true,
fontFamily: 'Inter',
fontSize: 17,
lineHeight: 24,
});Defaults fill undefined fields only. Explicit props and options take precedence.
Defaults are applied to:
- prepared text creation
- one-shot measurement
- Worklet measurement and layout helpers
- glyph-field typography fields omitted from
createGlyphField() - selected direct view props such as
anchorToCapHeight,allowFontScaling, andtabularNumbers
TextView does not read typography defaults from its style prop. Put typography on direct props when app defaults should fill missing values.
The config is read during the native build. Rebuild the app after changing it.
Native Resources
Prepared text and glyph fields allocate native resources. Release handles when they are no longer needed:
prepared.release();
releasePreparedText(preparedArray);
field.release();Text Style
TextMeasureStyle contains the text fields used by measurement and Text Engine's native views:
allowFontScalingcolorfontFamilyfontSizefontStylefontWeightincludeFontPaddingletterSpacinglineHeighttabularNumberstextBreakStrategy
Layout options are separate: anchorToCapHeight, ellipsizeMode, maxLines, and width.
Example App
The example app in examples/ shows:
TextViewand prepared text in the type demo- prepared text measurement in the chat demo
- glyph fields in the field and fire demos
Benchmarks
Native benchmarks against default React Native Text, using RN 0.87.1 in Release builds. Ratios are TextView / RN Text; lower is better.
Time
| Workload | iOS | Android | | -------------------------------- | -----------: | -----------: | | Chat list layout | 0.454× | 0.718× | | Layout, mount, and first draw | 0.841× | 0.468× | | Short label measurement | 0.427× | 0.717× | | Plain text creation and layout | 0.452× | 0.725× | | Styled text creation and layout | 0.432× | 0.611× | | Repeated layout queries | 0.172–0.457× | 0.379–0.409× | | Remeasuring unchanged paragraphs | 0.020× | 0.043× |
Memory
Memory above baseline for 128 laid-out paragraphs, with and without drawn views (KiB).
iOS
| Workload | Memory | RN Text | TextView | Ratio |
| ------------------------ | ----------------- | --------: | ---------: | -----: |
| Laid-out paragraphs | Native heap | 2,163.0 | 1,794.3 | 0.830× |
| Drawn views + paragraphs | Native heap | 2,672.1 | 4,279.3 | 1.601× |
| Drawn views + paragraphs | Process footprint | 67,152.0 | 59,808.0 | 0.891× |
Android
Native and managed heap combined.
| Workload | RN Text | TextView | Ratio |
| ------------------------ | --------: | ---------: | -----: |
| Laid-out paragraphs | 1,582.2 | 1,250.5 | 0.790× |
| Drawn views + paragraphs | 2,598.2 | 2,026.5 | 0.780× |
Full results · RN's opt-in prepared layout · Methodology · Library benchmarks
