The Frontend Trilemma
If you're developing a cross-platform app, you've committed to the frontend development trilemma, a choose-two-of-three best 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.
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.</Heading>
</Stack>
)
Styles extracted, logic evaluated, and a flatter tree with atomic CSS styles per-file.
export const App = props => <div className={_cn}>
<h1 className={_cn2 + (_cn3 + (props.big ? _cn4 : _cn5))}>
Lorem ipsum.
</h1>
</div>
const _cn5 = " _fos-16px"
const _cn4 = " _fos-22px"
const _cn3 = " _bg-180kg62 _col-b5vn3b _mt-0px _mr-0px _mb-0px _ml-0px _ww-break-word _bxs-border-box _ff-System _dsp-inline "
const _cn2 = " font_System"
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-1aj14ca _pl-1aj14ca _pr-_gtSm_lrpixp _pl-_gtSm_lrpixp"
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:
Tamagui makes React faster, but mostly makes making React faster.
Give Tamagui a try with npm create tamagui@latest
.
Previous
Extras
Next
Installation