Skip to content

feat: add layout-only Pretext native layout API#4

Merged
jingjing2222 merged 83 commits into
mainfrom
feat/text-relayout-benchmark-poc
Apr 24, 2026
Merged

feat: add layout-only Pretext native layout API#4
jingjing2222 merged 83 commits into
mainfrom
feat/text-relayout-benchmark-poc

Conversation

@jingjing2222

@jingjing2222 jingjing2222 commented Apr 12, 2026

Copy link
Copy Markdown
Owner

Summary

This PR turns react-native-nitro-pretext into a layout-only Pretext API for computing React Native text geometry before the visible UI renders.

Pretext does not render text, does not expose a hidden measurement view, and does not derive height from fontSize. Apps render with normal React Native UI after reading native text-engine metrics from the platform text engines.

import { Pretext, prepare, layout, usePretextLayout } from "react-native-nitro-pretext";

const prepared = prepare(text, style);
const metrics = layout(prepared, { width });
prepared.release();

Major Release

  • Changeset is marked major.
  • Removed measure(), measureBatch(), TextMeasure, public renderer components, native prepared renderer APIs, and raw prepared paragraph APIs from the package export surface.
  • Removed public native drawing/view-manager paths.
  • Manual lifecycle is now prepared.release() only; native prepared ids are hidden behind an opaque JS object.
  • Added ts-prune as a dev dead-export check and removed stale TypeScript/example exports found during the audit.

Current Public API

  • Pretext.prepare() / named prepare()
  • Pretext.layout() / named layout()
  • Pretext.usePretextLayout() / named usePretextLayout()
  • layout() returns metrics by default and can optionally return line geometry, diagnostics, or rich inline box frames.
  • usePretextLayout() owns prepare/layout/release from React lifecycle.

Compatibility And Native Contract

  • React peer range: *.
  • React Native support floor: >=0.81.0.
  • react-native-nitro-modules peer range: *.
  • Example app and latest local validation: React 19.2.3, React Native 0.85.0.
  • Android API 29+ canonical path: MeasuredText + LineBreaker.
  • Android API 24-28: StaticLayout/legacy fallback only, not canonical parity or performance target.
  • iOS canonical path: Core Text CTTypesetter + CTLine.
  • Android includeFontPadding defaults to true for RN <Text> compatibility.
  • Android accepts RN logical layout units from JS, converts them to native px for MeasuredText + LineBreaker, and returns RN layout units.

Examples And Docs

  • README presents Pretext as the pre-render text layout engine, not a prototype renderer.
  • docs/api.md documents only Pretext, prepare, layout, and usePretextLayout with props/types.
  • examples/use-case/* maps to the API docs.
  • examples/non-use-case/* shows the matching RN-only workaround with hidden measurement/callback state.
  • benchmark/measured-layout remains as the performance case study.
  • Static verification checks docs, navigation routes, example manifests, and automation report fields.
  • Maestro example/benchmark flows remain manual-only and are intentionally excluded from CI.

Performance Snapshot

The headline comparison is the path an app would otherwise build with hidden RN <Text> plus onLayout to get a stable height before rendering the visible UI.

Platform RN hidden measure Pretext layout Stable-height path improved by Time removed Relative speedup
iOS, iPhone 16 simulator debug 129.16 ms 1.55 ms 98.8% 127.61 ms 83.3x
Android API 36, Pixel_9_Pro AVD debug 174.95 ms 8.59 ms 95.1% 166.36 ms 20.4x

Hot relayout compute after prepare() is a separate layout-only benchmark:

Platform RN <Text> median Pretext hot layout median Compute improvement Relative compute speedup
iOS 247.11 ms 0.23 ms 99.9% 1074.4x
Android API 36 54.55 ms 0.06 ms 99.9% 909.2x

These are not claims that the final RN render surface is always faster. The Android debug AVD run reports Pretext visible surface median 85.01 ms versus RN <Text> median 54.55 ms; that surface timing is kept as context. The correctness/performance gate is the layout-only path plus required engine metadata.

CI-Safe Local Validation

  • yarn nitrogen
  • yarn fmt:check
  • yarn typecheck
  • yarn lint
  • yarn test
  • yarn verify:api-examples
  • yarn verify:package-exports
  • yarn build
  • npm pack --dry-run --ignore-scripts
  • yarn workspace react-native-nitro-pretext-example build:android
  • yarn workspace react-native-nitro-pretext-example build:ios

Additional latest checks run during review:

  • yarn ts-prune -p tsconfig.build.json -i 'src/index.ts|src/PublicTypes.ts|src/TextMeasure.native.ts' | grep -v '(used in module)' || true
  • yarn changeset status --since=main

Manual Device Validation

Maestro is not part of CI because it depends on installed apps, simulator/device state, Metro, and route-level API/example contracts that may intentionally change.

Latest manual iOS Maestro benchmark, iPhone 16 simulator, debug app with Metro:

  • Command: yarn benchmark:ios
  • RN baseline median: 247.11 ms
  • Pretext layout + RN surface median: 230.95 ms
  • Pretext layout-only median: 0.23 ms
  • Prepare once: 47.40 ms
  • Surface line-count parity: 0/240 mismatches
  • Surface sampled line-text parity: 35/240 mismatches
  • Core Text engine metadata and local benchmark gates passed

Latest manual Android Maestro benchmark, Pixel_9_Pro AVD API 36, debug app with Metro:

  • Command: MAESTRO_ANDROID_DEVICE_ID=emulator-5554 yarn benchmark:android
  • RN baseline median: 54.55 ms
  • Pretext layout + RN surface median: 85.01 ms
  • Pretext layout-only median: 0.06 ms
  • Prepare once: 140.01 ms
  • Canonical engine: android_measured_text_line_breaker
  • includeFontPadding: true
  • Local benchmark gates passed
  • RN <Text> parity drift remains diagnostic because RN <Text> is a compatibility oracle, not the correctness source

Review Focus

  • Public API and lifecycle in src/Pretext.ts and src/index.ts.
  • Nitro bridge shape in src/Pretext.nitro.ts and platform implementations.
  • Android/iOS text metrics paths and fallback diagnostics.
  • Removed renderer/view-manager surface.
  • API-matched examples and RN-only contrast routes.
  • CI-safe static/build gates versus manual-only Maestro scripts.

@jingjing2222 jingjing2222 self-assigned this Apr 12, 2026
@jingjing2222 jingjing2222 changed the title [codex] Split benchmark routes and fix iOS release build feat: Split benchmark routes and fix iOS release build Apr 12, 2026
@jingjing2222 jingjing2222 changed the title feat: Split benchmark routes and fix iOS release build feat: split benchmark routes and fix iOS release build Apr 12, 2026
@jingjing2222 jingjing2222 changed the title feat: add layout-only PreText native layout API feat: add layout-only Pretext native layout API Apr 24, 2026
@jingjing2222 jingjing2222 marked this pull request as ready for review April 24, 2026 14:43

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7e1723790f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread android/src/main/java/com/margelo/nitro/pretext/PretextShared.kt
Comment thread android/src/main/java/com/margelo/nitro/pretext/PretextShared.kt

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9cf7ba39ea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ios/PretextShared.swift Outdated
for segment in paragraph {
let baseOffset = (text as NSString).length
let breakBehavior = segment.breakBehavior.lowercased()
if segment.kind?.lowercased() == inlineSegmentKindBox || segment.boxId != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat empty boxId as absent before box classification

The box classification in prepareInlineParagraphSeed treats any non-nil boxId as an inline box, which includes empty strings coming from materializeInlineParagraphs (boxId: "" is preserved). In that case iOS converts the segment into an object-replacement box and drops its text, producing incorrect widths/line breaks; Android normalizes empty strings to null, so this also creates cross-platform drift. Please require a non-empty boxId (or normalize empty strings to nil) before taking the box path.

Useful? React with 👍 / 👎.

Comment thread ios/Pretext.swift
Comment on lines +83 to +85
paragraph.compactMap { segment in
guard let segmentObject = segment as? [String: Any] else {
return nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject invalid inline segment entries instead of dropping

materializeInlineParagraphs uses compactMap, so any non-object element in a paragraph (including null from sparse JS arrays after JSON serialization) is silently removed. That mutates segment count and UTF-16 offsets, yielding corrupted layout input without surfacing an error; Android currently throws for the same malformed payload, so behavior diverges by platform. This should fail fast rather than silently skipping entries.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75025bfee1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +584 to +587
val canonicalLayoutEngine = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
LAYOUT_ENGINE_ANDROID_MEASURED_TEXT_LINE_BREAKER
} else {
LAYOUT_ENGINE_ANDROID_LEGACY_FALLBACK

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat pre-Q static layout as canonical Android engine

For Android API 24–28, normal layout requests go through StaticLayoutLineLayout (which emits android_static_layout_compat), but this canonical-engine branch hardcodes all pre-Q devices to android_legacy_fallback. As a result, collectDriftKinds/line drift checks report engine_drift for the default pre-Q path, so diagnostics and benchmark parity signals are systematically wrong on supported older Android versions.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60a7e1662c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +96 to +100
val width = AndroidTextUnits.toPx(max(0.0, segment.width ?: 0.0))
val height = AndroidTextUnits.toPx(max(0.0, segment.height ?: 0.0))
val baseline = AndroidTextUnits.toPx(segment.baseline ?: AndroidTextUnits.fromPx(height))
.coerceIn(0.0, height)
return NativeInlineBox(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject inline box segments missing geometry

This path silently substitutes missing box metrics with zero values (width, height, baseline) instead of failing fast. Because InlineSegment does not require those fields when kind is "box", a caller can pass a box segment that type-checks but produces a 0x0 inline box and distorted line wrapping/box frames at runtime. Please validate box segments and throw when required geometry is absent.

Useful? React with 👍 / 👎.

Comment on lines +76 to +77
text = segmentJson.optString("text"),
breakBehavior = segmentJson.optString("breakBehavior"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate inline segment string fields before coercion

Using optString here coerces non-string JSON values into strings (for example, numeric text becomes "123"), while the iOS parser treats non-strings as empty. The same malformed payload can therefore produce different token boundaries and layout results by platform without any error signal. This should reject non-string text/breakBehavior values instead of coercing them.

Useful? React with 👍 / 👎.

@jingjing2222 jingjing2222 merged commit 400e33e into main Apr 24, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant