Compiler-Informed Rendering
ReactLynx analyzes JSX at build time and extracts stable host element structures ahead of time, allowing the runtime to focus on values and subtrees that may change. We call this division of work between the compiler and runtime compiler-informed rendering.
This optimization is applied automatically. Most applications can continue writing clear, idiomatic JSX without changing their component design to accommodate the compiler.
Here, host elements are Lynx elements such as <view> and <text> that ReactLynx renders through the Lynx Engine. User-defined components such as <ProductCard> are not host elements.
What the Compiler Can Do Ahead of Time
Consider a regular component:
If JSX is transformed only as in a traditional, purely runtime Virtual DOM, without static extraction, the compiled result of Greeting is semantically similar to the following code:
On every render, jsx() creates virtual nodes (VNodes) that describe the UI structure. ReactLynx then compares these nodes level by level to decide which host elements to reuse, create, or update. This process is known as reconciliation.
But in this example, the view, text, fixed text Hello, , and their parent-child relationships are static; only className and name can change. The compiler can generate the Element PAPI operations for creating the static structure ahead of time. The resulting static template definition can be reused across the initial renders of different instances and their subsequent re-renders; the runtime only needs to supply the dynamic data for each render. Conceptually, it looks like this:
These names and function shapes only explain the mechanism; they are not actual compiler output that applications can rely on. On the initial render, the compiled create() directly builds the known host structure. On subsequent renders, update() directly modifies a known element. The runtime does not need to repeatedly create and compare VNodes for this part of the structure, while children that cannot be determined ahead of time, such as name, are still handled through dynamic slots.
Prior Art and the Current Implementation: Snapshot
This idea of using information known at build time to reduce runtime work is not unique to ReactLynx. Vue's compiler-informed virtual DOM, Owl's BlockDOM, and Million.js all take a similar approach. From a broader computational perspective, it can also be understood as a form of partial evaluation: using host structures known from JSX to specialize the generic rendering process, leaving only the parts that cannot be determined ahead of time for runtime and producing an effect similar to “partial prerendering.”
In ReactLynx, this approach is currently implemented through an Intermediate Representation (IR) called a Snapshot. We are also exploring other representations with better performance. Snapshot follows the conceptual model above and can be summarized in four core parts:
Note: The IR is an internal implementation detail. Do not depend on or copy these outputs in application code. Specific names, parameters, and data structures may change with the source code, compiler configuration, or ReactLynx version.
Writing Compiler-Friendly JSX
Dynamic properties, text, and children do not prevent compiler optimization by themselves. For most components, these three principles are enough:
- Keep component semantics clear and the code readable.
- Let runtime data flow through properties and children as usual.
- Let host types, hierarchy, and order that are already known during development appear naturally in JSX.
There is no need to split a component just to make a property or text value static, nor does the entire tree need to be static. The compiler already separates stable structures from dynamic regions.
The following patterns are optional, not hard rules. Use them only when they also fit the component's intended semantics.
Keep Shared Structure Visible When It Is Natural
The following code duplicates the entire card structure in both branches:
When both branches naturally represent the same component structure, you can keep one stable shell and make the price region a dynamic child:
The second form makes the shared shell more directly visible to the compiler and leaves only the price region in a dynamic slot. However, do not restructure conditional rendering solely for compiler optimization. Different forms may affect component identity, state preservation, and readability.
Write Known Host Element Types Directly
The compiler can only analyze JSX structures visible in the source. If a host type is not determined until runtime, that part of the structure must also be handled at runtime:
If the candidate structures are already known during development, write them as explicit JSX branches:
Runtime data still selects the branch, but the host structure of each branch is visible to the compiler, so each can be extracted separately. Use dynamic creation only when the host type genuinely comes from runtime configuration.
Declare Known Properties Explicitly
Compiler-informed rendering supports spread properties. The compiler knows which element receives the spread object, but because the property set is not known until runtime, it must preserve the entire object as one dynamic position:
When the property set is known during development, declaring properties explicitly lets the compiler generate more targeted update logic:
The explicit form also avoids creating a cardProps object on every render.
Spread remains appropriate when a component needs to forward unknown properties from its caller; do not expand them one by one solely for compiler optimization.
In short, keep known host structures directly visible in the source and let actual runtime data enter through properties or dynamic children. For structures that genuinely need to be determined at runtime, keeping the code natural and dynamic is more important.
How the Compiler and Runtime Work Together
One characteristic of ReactLynx is that this compiled representation participates not only in update-time reconciliation, but also in Lynx's dual-thread Instant First-Frame Rendering (IFR) pipeline.
The same template can be reused by multiple mounted Greeting components. Each mounted Greeting has a corresponding pair of template instances: the main-thread template instance owns the Lynx elements it creates, while the background template instance stores the logical tree and dynamic data used for reconciliation. The runtime consumes the compiled output in two stages: initial rendering and subsequent updates.
Initial Render: Create a Template Instance
The UI elements for the initial render are created directly on the main thread. The runtime first finds the template from the VNode type, then creates a main-thread template instance for this Greeting. The template instance owns the Lynx elements returned by the static creator. Initial dynamic values are passed to their corresponding updaters, and dynamic children are inserted into the slots reserved by the compiler.
The following pseudocode illustrates this process:
Here, initialRender abstracts the main-thread template-instance information required for the initial synchronization; the Lynx elements themselves remain on the main thread. hydrateRootFromMainThread() abstracts the background render, instance association, and initial difference synchronization, returning the long-lived background root responsible for reconciliation. These names illustrate the mechanism and are not current public ReactLynx APIs.
Here, we borrow the concept of SSR hydration to describe this process: the main thread runs the application first and creates the Host Tree required for the first frame, allowing content to be displayed without waiting for the background thread to become ready. The background thread then runs the application again and attaches its own template-instance tree to the existing Host Tree instead of recreating the entire tree.
ReactLynx takes advantage of the main thread's ability to create the Host Tree ahead of time, blocking the UI thread until the initial render completes so that the first displayed frame already contains content. This is Instant First-frame Rendering (IFR). After the initial synchronization, the background thread takes over subsequent reconciliation, while the main thread continues to own the Lynx elements and apply updates.
Updates: Process Only Changes to Dynamic Positions
After the mapping is established, the background thread compares old and new dynamic data and generates updates. The main thread then finds the corresponding template instance and applies the changes.
The following pseudocode illustrates the same mechanism for any template. Changes to dynamic properties can be handled by targeted updaters generated by the compiler, while dynamic children remain reconciled by React:
For dynamic properties, the main thread directly calls the compiler-generated Template.update() without traversing the static host structure again. For dynamic children, the background thread still performs reconciliation and applies the resulting child changes to the reserved slots.
Back to Application Code
Compiler-informed rendering is not an API that developers need to invoke manually. It is a division of work that ReactLynx performs automatically between the compiler and runtime. Regular JSX continues to work even when a component contains dynamic properties, conditional branches, lists, and user-defined components; the compiler extracts stable structures wherever it can determine them.
In day-to-day development, prioritize components with clear semantics: let known host structures appear naturally in JSX, and let dynamic data flow through properties and children as usual. Consider additional adjustments only when performance analysis identifies the code as a bottleneck, and only when those adjustments preserve the component's semantics.
In other words, let the compiler find optimization opportunities in natural JSX instead of organizing application code around a particular internal IR.