Early access
Native apps with Lynx
Write the same components you write for the web, and render them as real native UI on iOS and Android. The two examples below are running right here, in your browser, from the same source.
What Lynx is
Lynx is an open-source rendering engine from ByteDance, used in production in TikTok. It runs your UI code on a multithreaded engine and draws it with platform-native views on iOS, Android and HarmonyOS. The same bundle also runs on the web, through Lynx for Web.
Lynx is the renderer, not the programming model. @octanejs/lynx is Octane’s renderer
for it: the components, hooks, context, Suspense and transitions you already know,
compiled ahead of time and committed to Lynx’s element API instead of the DOM. There is
no VDOM on either side of the boundary.
A recycling list, a like button, and a main-thread scrollbar
This is Lynx’s Product Gallery tutorial,
ported to Octane. A two-column waterfall <list> recycles its cells, each card owns
useState for its like button, and the scrollbar on the right is repositioned by a
'main thread' handler that never asks the background thread for anything.
It scrolls on its own because the example asks it to: ref.current.invoke('autoScroll', …)
drives the native list, and the scrollbar you see tracking it is the main-thread handler
doing its work every frame. Open the Device tab to run the same example on a phone.
The template reads the way it would on the web. @for keys the cells, @if swaps the
heart, and the scroll handler is an ordinary function marked to run on the other thread:
export function Gallery({ pictureData }: { pictureData: Picture[] }) @{
const scrollbarMTSRef = useMainThreadRef<object>();
const onScrollMTS = (event: { detail: GalleryScrollDetail }) => {
'main thread';
const scrollbar = scrollbarMTSRef.current;
if (scrollbar === null || scrollbar === undefined) return;
__AddInlineStyle(scrollbar, 'top', scrollbarTop + 'px');
__FlushElementTree();
};
<list list-type="waterfall" span-count={2} main-thread:bindscroll={onScrollMTS}>
@for (const picture of pictureData; key picture.id) {
<list-item item-key={picture.id}>
<LikeImageCard picture={picture} />
</list-item>
}
</list>
}A carousel that never wakes the background thread
Lynx’s Product Detail tutorial, also ported to Octane. Touch tracking, the release animation and its easing curve all run on the main thread, so a frame never waits on a round trip. The indicator underneath is background state, updated across the thread boundary.
Two threads, one component tree
Lynx runs two JavaScript environments. The main thread owns the element tree and the frames the user sees; the background thread owns state, effects, refs and everything that can afford a round trip. Octane compiles one authored entry into both graphs, so you write one component and mark the parts that must stay on the main thread:
const onTouchMove = (event: MainThread.TouchEvent) => {
'main thread';
// Runs on the frame that produced the touch.
};Everything else — useState, useEffect, context, Suspense, <Activity> — behaves the
way it does in the browser, on the background thread. The
differences from React that apply on the web apply here
too; Lynx adds no second dialect.
Run it yourself
@octanejs/lynx and @octanejs/rspeedy-plugin are not published yet. Until they are,
run the examples from a clone of the repository:
git clone https://github.com/octanejs/octane.git
cd octane && pnpm install
pnpm --filter @octanejs/rspeedy-plugin exec rspeedy dev --root examples/gallery --environment lynxInstall Lynx Explorer on a phone or a simulator, then scan the QR code the dev server prints. Explorer is Lynx’s sandbox app: it loads a bundle over the network and runs it against the real native engine, so you see the same rendering a shipped app would.
An Octane Lynx application is an ordinary Rspeedy project with one plugin:
// lynx.config.mjs
import { defineConfig } from '@lynx-js/rspeedy';
import { pluginOctane } from '@octanejs/rspeedy-plugin';
export default defineConfig({
environments: { lynx: {}, web: {} },
source: { entry: { main: './src/index.ts' } },
plugins: [pluginOctane()],
});environments.lynx produces the native bundle Explorer loads. environments.web
produces the Lynx-for-Web bundle the previews on this page mount, which is also how you
put a Lynx screen in front of someone who has no app installed.
Where this stands
The renderer’s exact claims, divergences and open gates are tracked in
packages/lynx/status.json,
and both examples document what was checked on a device in their own READMEs. If you
want to help close a gate, the
open pull requests are the place to start.