The Frontend Trilemma

If you're developing a cross-platform app, you've committed to a path along the frontend development trilemma, a choose two-of-three that is nicely shown as a diagram:

React Native recommends writing things twice generally for the best UX as is made clear in the <title> of the homepage: "learn once, write anywhere ". This is opposed to the sort of holy grail mantra of "write once, runs everywhere". Doing this results in native-feeling and performing apps, while still saving quite a bit of development time thanks to sharing everything besides your views: utils, state and data management, hooks, etc.

The center of this above Venn diagram would be a sort of platonic ideal: "write once, runs well everywhere". We're pretty far from it as of now, but it's not technically impossible. We can imagine a few ways to get closer:

  • A sort of Rails-for-React - unified routing, patterns, code gen, "all the batteries".
  • A UI kit that adapts to each platform's primitives confidently with flexible APIs.
  • A way to author styles that output to platform primitives without overhead: eg, CSS with media queries, pseudos, and variables on the web.

This document goes over how we can achieve the last one. The first one is doable (and Expo is working on as much ), and the second one Tamagui UI is working towards slowly.

The idea is to make another "bump" towards properly native-experience apps with shared code, much like how React Native Web made one:

This can be done especially on the web by reducing JS bundle size by a large % and greatly increasing render performance with reduced tree depth, logic, objects, and hooks. The Tamagui Compiler does this using partial evaluation, static extraction and hoisting, code elimination, and tree-flattening.

You can skip to the technical details without the backstory from here if you'd like.

Choose your journey

1

2

3

Strategy

Universal

Lean

Big-Budget

Native + Web

Code Sharing

> 70%

-

< 30%

Feels native

Ship Fast

Universal apps (those you "write once" that "run everywhere") make sense today for many cases: side-projecting, SEO-insensitive or enterprise-only apps, people who want to ship fast, experiment more, are pre-product-market fit, or generally have apps with simpler UI. Twitter and Tinder are two larger examples of this.

But today, at best, we use hooks for media queries and themes, which basically touch every component. This causes whole-tree re-renders and more expensive main-thread time in critical areas on the web. Combine that with the CSS-in-JS approach of React Native Web greatly increasing bundle size, and even medium-complexity pages will drop from 100% Lighthouse to half or worse (our homepage, a good complex example due to showing off many features that are well-optimized, goes from 95 or so down to 80ish with the compiler off).

With all your media queries, interactive styles, themes, animations, and dynamic styles in JS, it's hard to make ambitious apps that don't feel janky.

How Tamagui Helps

@tamagui/static is an optimizing compiler for React Native with four main features:

  • Extracts all types of styling syntax into atomic CSS.

  • Removes a high % of inline styles with partial evaluation and hoisting.

  • Reduces tree depth, flattening expensive styled components into div or View.

  • Evaluates useMedia and useTheme hooks, turning logical expressions into media queries and CSS variables.

  • The output is smaller bundles, better runtime performance, and many more native primitives used on the web.

    Here's what it does, in code:

    A powerful `styled` constructor, inline props, shorthands and more.

    import { Stack } from '@tamagui/core'
    import { Heading } from './Heading'
    const App = (props) => (
    <Stack px="$2" w={550} $gtSm={{ px: '$6' }}>
    <Heading size={props.big ? 'large' : 'small'}>Lorem ipsum dolor.</Heading>
    </Stack>
    )

    Styles extracted, logic evaluated, and a flatter tree with atomic CSS styles per-file.

    import { Stack } from '@tamagui/core';
    import { Heading } from './Heading';
    export default (props) => (
    <div className={_cn}>
    <span className={_cn2 + (_cn3 + (props.big ? _cn4 : _cn4))}>Lorem ipsum dolor.</span>
    </div>
    );
    const _cn4 = " _col-b5vn3b _ff-4yewjq";
    const _cn3 = " _ml-0px _mb-0px _mr-0px _mt-0px _col-b5vn3b _tt-3tb9js _ff-4yewjq _fow-3uqci0 _ls-3w5fg8 _fos-3slq2o _lh-3or5x5 _cur-text _ussel-text _ww-break-word _bxs-border-box _dsp-inline ";
    const _cn2 = " is_Heading font_heading";
    const _cn = " is_Stack _fd-column _miw-0px _mih-0px _pos-relative _bxs-border-box _fb-auto _dsp-flex _fs-0 _ai-stretch _w-550px _pr-1aj148u _pl-1aj148u _pr-_gtSm_1aj14ca _pl-_gtSm_1aj14ca ";

    See more examples on the homepage.

    Notice that the compiler turned the Text into a p, and the YStack into a div (on native, this would be Text and View). This is known as tree-flattening, and for both web and native it yields very nice improvements to render performance.

    This is a typical performance improvement, where much of the gains come from flattening:

    Tamagui

    0.02ms

    react-native-web

    0.063ms

    Dripsy

    0.108ms

    NativeBase

    0.73ms

    Across a few apps, we've seen 30-50% of components typically flatten, with a higher percent achievable just by being aware of how the flattening optimizes (adding the // debug comment to the top of the file will show a fuller output).

    Meanwhile, on Native, because we can't optimize to anything beyond vanilla React Native code, the gains are less. Still, the results are impressive given you now have performance within 5% of hand-optimizing React Native code, except you get a whole suite of features for free.

    Tamagui

    108ms

    React Native

    106ms

    NativeBase

    247ms

    You can see more Benchmarks with explanations here.

    The compiler itself deserves more detail, which we'll expand on in the blog. For now, this serves as a decent introduction.

    Compilers can dramatically improve code sharing without the typical sacrifice of performance. They don't solve every problem of universal apps, but by making responsive styling, themes and interactive styles all perform at native levels, they unlock sharing a much larger percentage of the components located in the middle to bottom of the render tree in apps.

    It gives us a new choice, "Universal + Compiler" that lets us ship fast while still feeling native:

    4

    Strategy

    Universal + Compiler

    Native + Web

    Code Sharing

    ~60-90%

    Feels native

    Ship Fast

    Tamagui makes React faster, but mostly makes making React faster.

    Give Tamagui a try with npm create tamagui@latest.