How It Actually Runs

Your code runs in Hermes, Fabric builds the native views and TurboModules call native code over JSI, the default since 0.76.

SyntaxuseColorScheme(): "light" | "dark" | null

Four pieces run your app. Hermes is the JavaScript engine compiled into the binary. It has been the default since 0.70 and loads bytecode produced at build time, which is why startup is quick. Fabric is the renderer: it turns the tree your components describe into real native views, and keeps a C++ copy of that tree so it can work out the smallest set of changes to commit. TurboModules are native functions exposed to JavaScript, such as the file system or the clipboard, loaded the first time you use one. JSI is the C++ layer underneath: it lets JavaScript hold a reference to a native object and call it directly.

What changed in 0.76

Together these are the New Architecture, and in 0.76 it became the default in every project. Before that, each call between JavaScript and native was serialised and sent across an asynchronous bridge, so a result came back in a later tick. A 0.87 app has no such bridge. Articles about bridge batching, or about debugging the app in Chrome, describe the old system.

What trips people up

Your JavaScript still runs on one thread. A long synchronous loop blocks every state update and every handler you wrote. The native UI thread keeps drawing while that happens, so a native scroll view still scrolls. An app that scrolls but will not respond is a JavaScript problem, not a rendering one.

Example

Example · tsx
import {
  Dimensions,
  StyleSheet,
  Text,
  View,
  useColorScheme,
} from "react-native";

// Both values come from the native side and arrive immediately, with no
// promise and no await. JSI is what makes a direct call like this possible.
const { width, height } = Dimensions.get("window");

export default function Runtime() {
  // A hook from react-native. It returns "light", "dark" or null, and
  // re-renders this component when the system setting changes.
  const scheme = useColorScheme();
  const dark = scheme === "dark";

  return (
    <View
      style={[styles.screen, { backgroundColor: dark ? "#111418" : "#ffffff" }]}
    >
      <Text style={[styles.line, { color: dark ? "#f2f4f6" : "#111418" }]}>
        window: {Math.round(width)} x {Math.round(height)}
      </Text>
      <Text style={[styles.line, { color: dark ? "#f2f4f6" : "#111418" }]}>
        scheme: {scheme ?? "not reported"}
      </Text>
    </View>
  );
}

// Hermes runs the function above. Fabric then commits its result to the
// real UIView or android.view.View that you see.

const styles = StyleSheet.create({
  screen: { flex: 1, alignItems: "center", justifyContent: "center", gap: 6 },
  line: { fontSize: 16 },
});

When to use it

  • Explain to a team why a 0.87 app does not have the bridge their older notes and blog posts describe.
  • Tell a stalled JavaScript thread apart from a slow render before you start optimising the wrong half.
  • Read a library's README and understand what it means when it claims New Architecture support.

More examples

The module behind the hook

On screen: one line of text naming the colour scheme the app started in. This value never changes afterwards, because reading a module once is not the same as subscribing to it. Appearance also offers addChangeListener, and useColorScheme is the component-friendly wrapper around exactly that pair: read now, re-render later. Reach for the module directly only outside components, where hooks cannot go.

Example · tsx
import { Appearance, StyleSheet, Text, View } from "react-native";

// Appearance is the native module. getColorScheme() reads the current
// value once, at the moment it is called, and returns it directly.
const schemeAtStartup = Appearance.getColorScheme();

export default function Startup() {
  return (
    <View style={styles.screen}>
      <Text>scheme when the module loaded: {schemeAtStartup ?? "unknown"}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24 },
});

One thread, held

While this loop runs, Hermes is busy and nothing driven by your JavaScript can update: no state change, no press handler, no navigation. Native scrolling continues, which is why a frozen app can still look half-alive. Work shaped like this belongs in smaller chunks, off the first render, or on the native side; a later stage measures it.

Example · tsx
// No UI here. This is the shape of work that freezes an app.
export function sumTo(limit: number): number {
  let total = 0;
  for (let i = 1; i <= limit; i += 1) {
    total += i;
  }
  return total;
}

console.log(sumTo(1000000));

// Output:
// 500000500000

Discussion

  • Be the first to comment on this lesson.