Roadmap
The bulk of Embedded React is built and verified — see the status table in the introduction. This file is the single home for what's left: known issues, planned work toward 1.0, the performance backlog, and the longer-term vision.
It is intentionally honest. If something here looks unfinished, it is — the engine, both flows, the backends that run on hardware, and the simulators all work today, but this is a beta. Flow B is being rebuilt around loadable packages (see Flow B); everything else is fixes and feature additions.
Known issues & caveats
- AOT animation
toValueuses a snapshot, not live state. A Flow B (Animated) animation whosetoValuedepends on current state may animate toward the value captured at compile/setup time rather than the live one. Tracked for the imperative / animation hardening pass. - Simulator asset-pack reload leaks buffers. The engine has no asset unregister, so each hot-reload of an image/font keeps the previous pack's buffers alive (a few KB per changed asset). Harmless for a dev session; never happens on-device.
- Browser host re-creates its
HEAPU8view every frame. WebAssembly memory growth detaches typed-array views, so the WASM simulator's present layer re-derives the view on each present. This is a workaround, in place and working — noted so it isn't "simplified" away. - Text is re-measured whenever a commit runs layout. The layout-dirty flag gates the
layout pass, but any commit that does run layout re-measures every
<Text>node, even if the change was unrelated. This is the top remaining per-frame layout cost (see the performance backlog). - Static RAM footprint is desktop-sized by default. The opacity/transform/shadow
scratch buffers default to generous dims. Opacity now composites through band strips
(
ERUI_SCRATCH_BAND_HEIGHT) — shrink the strip height for a large RAM saving with no size cap on fades — and the transform source has its own dims (ERUI_TRANSFORM_WIDTHandERUI_TRANSFORM_HEIGHT). Tune these plusERUI_MAX_OPACITY_DEPTHand shadows/transforms per board — the defaults are not board defaults. - Performance ceiling on JS-driven drag (Flow A). A drag whose handler runs in JS
still costs a React render per frame — around ~24 fps on PSRAM-QuickJS hardware,
bounded by per-event dispatch rather than rendering. The common cases are now covered:
touch-move coalescing collapses a frame's samples into one dispatch, and
<Dial>drags natively without entering JS at all (the thermostat dial measures ~47 fps on an ESP32-S3). Anything still driven fromonTouchMovekeeps the old ceiling; the AOT flow avoids it entirely. Native-driver animations were never affected.
Near-term (beta → 1.0)
Backends
The backends that run on hardware (sdl, esp32-lcd, esp32-spi-lcd, pico-spi-lcd,
dma2d) and power the simulators (software, web) are done. The remaining backends are
stubs / README-only:
backends/framebuffer/— Linux/dev/fb0direct framebuffer (embedded Linux SBCs).backends/opengl/— OpenGL ES 2.0 (Raspberry Pi, Android).
Examples / board bring-up
Five examples run end-to-end (see the board guides). These are README-only and need wiring:
examples/stm32h7/— first STM32 bring-up. The backend (backends/dma2d/) is done; what is missing is a board project that wires it to an LTDC panel end to end.examples/raspberry-pi/— RPi reference app (withbackends/framebuffer/oropengl).examples/dashboard-demo/,examples/marine-display/— larger reference apps.
Flow A — React on QuickJS (runtime)
- Wireless hot reload. On-device hot reload ships over USB serial; a Wi-Fi transport would give the same edit-save-see loop without a cable.
- Runtime asset registration. A
NativeUI.loadImage/loadFontpath for dynamically generated or OTA-delivered assets.er_font_load(blob → pool) already exists for this future case; the JS-facing API does not. - SDL keyboard → input. Mouse is forwarded as touch; hardware keyboard feed into the
engine is still TODO (needed for
TextInputon the desktop host).
Flow B — compiled, loadable packages
The compiler produces a package in the same .erpkg container Flow A uses, and a fixed
Flow B runtime in the firmware checks and runs it. There is no JS engine and no heap, and changing the
UI needs no firmware rebuild. Tracked under the
flow-b label.
- One firmware for every panel. A package holds a layout for each of the app's breakpoints, and declares no sizes. The firmware passes the panel size at boot and the package picks its layout. A layout that doesn't compile fails the build.
- Fixed memory. The firmware reserves one block of RAM, and the package states exactly how much it needs. The program, text, vector data, and assets are read in place from flash. A package that does not fit shows the error screen instead of loading.
- Nothing in a package can crash the device. The runtime checks the whole package before it runs: every offset and index in range, every jump valid, every native import matched by signature. While running, node references are checked handles, handlers have an instruction budget, and every fixed limit reports overflow. Any failure stops the app and shows a red error screen with the reason and the source location.
- On-device hot reload over the Flow A reload transport. The new package is checked in a staging slot before the swap, and the last good package is kept for rollback.
- Parity with Flow A through bounded, native versions of what Flow B lacks:
full list and string operations, structs,
useReducer,useContext, custom hooks, andButton/ImageBackground/SectionList. - Native modules for work outside the UI (network, sensors, storage). A typed TypeScript interface is implemented in C by the firmware, and the same interface works in Flow A.
Order: restructure the compiler (split aot/compile.mts, then add a typed intermediate
representation), then build the runtime and the package output, then multi-panel packages
and hot reload, then parity. Boards with no spare flash partition link the package into the
firmware instead.
Still to do:
- Resolve the live-vs-snapshot
toValuegap (see Known issues). - Imperative refs /
Animatedsequence / parallel / loop / interpolate hardening on the no-PSRAM hardware path. - Thermostat capstone under a frame-time gate; ping-pong band buffers for banded RGB565.
Engine features
- Canvas API (
canvas_bindings.c) — deliberately a stub; land only if the bundled React surface needs it. - Ellipsize modes —
tailandclipship;headandmiddleare deferred. textAlign: justify— left/center/right ship; justified text deferred.- Per-container layout-dirty. The dirty flag is whole-tree and conservative
(
er_node_set_propsraises it even for a pure visual change). Next: compare layout-relevant fields and re-solve only the changed subtrees.
Open API decisions
- Gradient style key. Gradients are an engine primitive with no React Native prop;
the key is
backgroundGradient(#108). Still open: its shape,{colors, angle?, positions?}or the CSS-modelled{type, angle?, to?, stops}recorded on #108. - Non-native-driver animation. Decide whether to support
AnimatedwithoutuseNativeDriver(needs a JS-side rAF loop callingsetValueeach frame) or requireuseNativeDriver: true. - Controlled
TextInputvalue.placeholder/placeholderTextColor/editable/cursorColorare done; a controlledvaluestill routes througher_text_input_set_text, notERProps. Modaltransparentprop. NoERPropsfield yet — handled via backdrop alpha at the JS layer; formalize or record as a gap.
Tooling / simulators
- Prebuilt / auto-built SDL simulator binary. Today
npm run simneeds a one-time CMake build of the sim binary. A scaffold + prebuilt/auto-built binary would remove the step. (The WASM simulator already ships prebuilt.) - Fast Refresh. The SDL/WASM simulators preserve
useStateacross reloads via a Babel transform; full per-component Fast Refresh (no keying fragility) is a later upgrade. - Workspace packages. The JS layer incubates in-repo at
packages/embedded-react. Once the public surface stabilizes, extract it into published workspace packages.
Done:
embedded-react buildnow produces the device artifact from a consumer project —app.erpkg(Flow A, bytecode compiled through the prebuilt.wasm, no native toolchain) and--aot→ a Flow Bapp.erpkg.
Performance backlog (engine)
Catalogued optimizations — correctness is fine, these are speed/RAM wins. Roughly highest-impact first.
- Cache text measurement on the node; invalidate only when text/font/size/spacing props change (top remaining layout cost).
- Skip clean subtrees in the render walk. Track a
subtree_dirtybit and cache sorted child order instead of descending the whole tree every frame. - Cache blurred shadow buffers keyed on geometry (shadows are fully re-blurred every repaint).
- Gradient color LUT. Precompute a 256-entry premultiplied LUT from the stops instead
of evaluating per pixel, and rasterize only the rows and columns inside the clip
(
render_linear()evaluates the whole box even for a small damage rect). - Cache / DDA-step scaled images instead of re-sampling (per-pixel division) every repaint.
- Resolve
BitmapFont*once when text props are set and cache on the node (currently astrncmpregistry scan per text op). - Batch anti-aliased corner and 1-bit glyph blits into a per-row buffer, one
copy/blendper row, instead of per-pixel / per-span. - Draw only the border ring of bordered rounded rects with an opaque background too; today only a transparent or translucent background gets the ring, and an opaque one is overdrawn.
- Clamp software-composite loops once per row instead of a per-pixel bounds check.
- Step transform source coordinates incrementally (and consider fixed-point) instead of a float matrix-multiply + bilinear sample per output pixel.
memsetonly the livew×hscratch region, not the whole slot.- Right-size recursive walker stack arrays (today each recursion level allocates
uint16_t[ERUI_MAX_NODES]); walk the sibling list in place or share scratch by depth. er_tree_append_childis O(n) per append → O(n²) to build a sibling list; keep alast_childpointer andprev_sibling/parentback-pointers.- Skip the zIndex insertion-sort when all children have
zIndex == 0, or cache the sorted order. - Bound the input-tick scan (currently scans all
ERUI_MAX_NODESslots twice for momentum) with a small active-scroller list. - Reduce static scratch RAM: size slots to the largest composited node, tile large transformed/opacity subtrees, or share one arena across transform/opacity/shadow.
- Scroll by moving painted pixels (deferred, #292). Every scroll step repaints the whole viewport. Moving the painted viewport by the delta and repainting only the exposed strip needs a backend "move rect" operation and a full framebuffer, so it would be opt-in for backends that have both (ESP32-S3 RGB, STM32 LTDC/DMA2D).
Vision / longer-term
The engine is runtime-agnostic by construction — er_scene.h is a pure C ABI, and
nothing in engine/ knows about React. That layering keeps Flow A and Flow B honest
against one C surface and leaves the door open for other frontends without forking the
engine. None of these are committed work; they're why the engine stays neutral:
- Lua UI — drive the scene graph from Lua tables / a Lua DSL.
- JSON UI — a serialized React-tree format loaded at runtime.
- Visual editor runtime — a desktop GUI designer that emits that JSON format.
- Other scripting APIs — language-agnostic C bindings other runtimes can wrap.
This does not change the project's identity: Embedded React is React Native for embedded MCUs. React is the supported developer path; the engine's neutrality is an implementation choice.
Non-goals
Deliberately out of scope:
- MCU SDK headers (
stm32h7xx_hal.h,esp_lcd.h, …) anywhere inengine/. Hardware specifics live inbackends/. - Flash tooling, OpenOCD scripts, deploy automation — firmware tooling, not engine concerns.
- RTOS setup / task creation — the caller's responsibility (bare-metal, FreeRTOS, Zephyr all work).
- Display initialization (SPI, MIPI DSI, LTDC) — the backend's responsibility.
- Network / fetch — provided by the firmware if needed.
- Web-only React APIs — DOM refs, portals, Suspense, Server Components.
- Host-only optimizations — SIMD paths (AVX2, NEON on desktop) in the
softwarebackend. On x86 and desktop it is the simulator, not a target; optimized loops belong in the backend of the board that runs them. - List virtualization —
FlatListwindowing, or a native recycling list node.FlatListis a documentedScrollViewalias in both flows: every row mounts and stays mounted (see FlatList). Windowing in JS would cost a React commit every time the window shifts — during a flick, every frame — the same per-event cost that caps Flow A drag, and Flow B has no runtime reconciler to recompute a window at all. A native recycling list node is the only version worth building, and it is not worth it here: these panels show a few dozen rows, not thousands. Paginate, or window by hand against a scroll offset you hold in state. - Pitching as a general-purpose multi-runtime UI engine. Non-React frontends could
live in
bridges/later, but React is the supported path.