Vanilla-Extract & Stitches: A Comparison
Published: 2022-03-14 08:37:16

**How do you pick between these two great options?**

Both focus on providing the best-in-class developer experience with minimal runtime.

So, how do they compare? If you had to pick one which one would you pick **given your context**?

Let’s get the similarities out of the way first.

---

## Similarities

Both libraries support:

- **Type-safe API:** Both have a typed library API to assist the developers.
- **Token-based theming:** Tokens are the building block of a system, both libraries provide a first-class support for typed tokens.
- **Variants:** Both libraries provide an API to add variants to a component.
- **CSS custom properties:** Both libraries convert theme tokens to CSS custom properties. They also provide API for user-defined CSS custom properties.

In a gist, they provide a very comparable developer experience. So what’s different?

---

## Differences

### Library Runtime

This is the amount of JavaScript the libraries ship by default.

🧵 **Stitches** claims a “near-zero runtime.” Bundlephobia reports 6 kB gzipped which is quite slim.

🧁 **Vanilla-Extract** runs at build-time. So no runtime is added. If you use the `@vanilla-extract/dynamic` package, it’d add [718 bytes](https://bundlephobia.com/package/@vanilla-extract/dynamic@2.0.2) to your bundle.

### Userland Runtime

This is the amount of runtime added due to the user-land code.

🧵 **Stitches:** User-land runtime code scales linearly as CSS is bundled in JavaScript. Theoretically, this will be as big as the design system itself.

🧁 **Vanilla-Extract:** None if you use the [styling API](https://vanilla-extract.style/documentation/styling-api/) only. If you use the [Sprinkles API](https://vanilla-extract.style/documentation/sprinkles-api/) for generating Atoms, it generates a script to look-up the classnames which doesn’t impact rendering performance. This script grows along with the amount of atoms generated which usually stop growing over time.

### Rendering Performance

The way in which CSS is delivered to the users can have an impact on several metrics like **FCP** and **TTI** of the first page and subsequent pages.

🧵 **Stitches** 

Because the styles are rendered inline, **FCP** happens much earlier for the first page load.

New styles are injected dynamically which invalidates a lot of the work that has already been done by the browser, no matter how little CSS was injected.

**TTI** happens later because a significant time of main thread is spent on [“Style & Layout”](https://web.dev/mainthread-work-breakdown/#how-the-lighthouse-main-thread-work-audit-fails). The [“uncanny valley”](https://crystallize.com/blog/frontend-performance-react-ssr-and-the-uncanny-valley) is quite wide.

🧁 **Vanilla-Extract**

Because CSS files are generated statically and are `<link />`ed and requested separately, **[FCP](https://web.dev/fcp/)** happens later in comparison to Stitches for the first page load.

Subsequent renders are fast as the common CSS (atoms) are cached on the first request. Only new styles are fetched, if any.

**[TTI](https://web.dev/tti/)** isn’t delayed because the main thread isn’t busy with [“Style & Layout.”](https://web.dev/mainthread-work-breakdown/#how-the-lighthouse-main-thread-work-audit-fails) The “uncanny valley” is narrow.

### Token typing and mapping

This is whether the tokens themselves are strictly typed and any deviations from token usage are reported.

🧵 **Stitches** [sensibly and conveniently maps the tokens](https://stitches.dev/docs/tokens#property-mapping) to the CSS properties. But they are not typed to be limited to the tokens only. Though, there is a [provision](https://stitches.dev/docs/typescript#stricter-experience) to have a stricter experience.

![You may use any string and the type-checker won’t complain.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/thgyq4qra2d3ajj7chhg.png)

🧁 **Vanilla-Extract** doesn’t give you any mappings if you use the [styling API](https://vanilla-extract.style/documentation/styling-api/). Though, if you use  [Sprinkles](https://vanilla-extract.style/documentation/sprinkles-api/) (for atoms), which you should, you can do your own mapping. The CSS properties are strictly limited to the tokens.

![CSS Properties are strictly typed to the tokens when using Sprinkles](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u7sy205tjo7d7xd6jwhm.png)

### Component Debug-ability (React Developer Tools)

The library should not come in the way of debug-ability using Devtools.

🧵 **Stitches:** Using `styled` API causes component names to be mangled, also you cannot view component source code if you use `styled`.  However, using the [css](https://stitches.dev/docs/api#css) function doesn’t have any of these problems.

![When using the styled API, the components names are lost](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8zzujl3hawvzrysjhr3y.png)

🧁 **Vanilla-Extract:** No negative impact on React Developer Tools.

![Component names are preserved with Vanilla-Extract](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/whdmjg2011k1nx63atzb.png)


### Documentation

How thorough and thoughtful the library documentation is.

🧵 **Stitches** comes with an excellent documentation. Even the, otherwise usually neglected, [TypeScript experience](https://stitches.dev/docs/typescript) is given full attention.

🧁 **Vanilla-Extract** has terse and *API like* docs. They’re difficult to get started with and require a lot of jumping around.

### Custom Tokens and Names

Does the libary allow custom tokens and token names (e.g., `spacings` instead of `space`) as per the vocabulary that suits their team? E.g., `timingFunctions`.

🧵 **Stitches** only supports tokens from [system-ui specs](https://system-ui.com/theme). Also not possible to customise token names either.

🧁 **Vanilla-Extract** supports custom tokens with custom names.

### Atomic style generation

Does the library provide a way to generate a set of atomic classes to reuse and reduce the amount of CSS to be shipped?

![Atomic CSS stops growing after a while](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/blmvh9dq96qawc2f8wqd.png)

🧵 **Stitches** doesn’t support atomic generation at this time.

🧁 **Vanilla-Extract** supports atomic generation using [Sprinkles](https://vanilla-extract.style/documentation/sprinkles-api/). You can even use the atoms inside variants.

## Takeaway

For us, performance and user experience are important as a lot of our users come from a low powered mobile device. The choice was obvious, we decided to go with **Vanilla-Extract**:

- Zero runtime
- Ability to generate atoms that helps in limiting the amount of CSS we ship to our users
- Better rendering performance overall
- Ability to use expensive libraries like [Polished](https://polished.js.org) in `.css.ts` files without worrying about the impact on bundle-size as they’re evaluated at build time.

## A Note On React 18

If you care about performance and the nitty-gritty details, you should definitely read the library upgrade guides by the React Working Group:

- [Library Upgrade Guide: `<style>` (most CSS-in-JS libs)](https://github.com/reactwg/react-18/discussions/110)
- [Library Upgrade Guide: `<link rel="stylesheet">`](https://github.com/reactwg/react-18/discussions/108)

Here’s an interesting excerpt from the upgrade guide for `<style>`:

> While this technique for generating CSS is popular today, we’ve found that it has a number of problems that we’d like to avoid. Therefore we don’t have plans for adding any solutions upstream to handle this in React. For the time being, we expect this to have to be handled by third-party libraries such as in this guide.
>
> Our preferred solution is to use `<link rel="stylesheet">` for statically extracted styles and plain inline styles for dynamic values. E.g. `<div style={{...}}>`. You could however build a CSS-in-JS library that extracts static rules into external files using a compiler. That’s what we use at Facebook.
>
> It’s tempting to use dynamically generated style sheets since it contains only the rules that are active on the page right now. However, this comes at a cost. Usually at the expense of runtime cost. It also comes at a cost of inserting new rules frequently which invalidates a lot of the work that has already been done.
>
> Shared stylesheets are efficient to parse and are very cacheable. By extracting them into shared styles you are also effectively batching a lot of that work up front. This lets you avoid doing that work later on as the user starts interacting with the page.

## References

- [A thorough analysis of all the current CSS-in-JS solutions with SSR & TypeScript support for Next.js](https://github.com/andreipfeiffer/css-in-js)
- [How React-Native-Web uses atomic styles and high-level DOM-agnostic abstractions](https://youtu.be/tFFn39lLO-U)
- [How Facebook uses atomic styles](https://youtu.be/9JZHodNR184)
- [RF21 – Naman Goel – Rethinking CSS - Introducing Stylex](https://youtu.be/ur-sGzUWId4)
- [RF21 – Mark Dalgleish – Zero-runtime CSS-in-TypeScript with vanilla-extract](https://youtu.be/23VqED_kO2Q)