HomeArticles

Go Preact! ❤️

Stefan Baumgartner

Written by @ddprrt

More on Preact, React

You might have heard of Preact, the tiny 3KB alternative to React. It has been around for a while, and since its inception, it claims to be API and feature compatible compared to the more widely used Facebook library.

After using Preact for quite a while I come to the conclusion that Preact is much more. It offers a totally different development and design philosophy and has the potential to solve a ton of problems the current JavaScript ecosystem has to face. In this article, I try to compile a list of things that I find outstanding.

Progressive ecosystem #

Vue popularized the idea of a progressive JavaScript framework. The idea is that you can start small with just a few bits and pieces, but you can get very far with it. The more you progress with the framework, the more you are able to opt-in to more features. Evan You’s presentation on that topic shows an image that illustrates the idea very well (see slide 11):

  1. Declarative rendering through a templating system
  2. A component system to allow for composability and reusability
  3. Client-side routing
  4. State management (vuex)
  5. Build tools (vue-cli and single file components)
  6. Server-side rendering and hydration

With each step, you are able to create richer applications, but easing in is simple as you always build upon the previous steps, you never discard them.

Preact and its eco-system work also progressively. I tried to create a similar chart for Preact:

A progressive diagram for Preact, see below

Each step in detail:

  1. A light-weight component layer. Like in React, everything in Preact is a component. Components are the root of whatever you want to create. The component layer has around 3KB and allows you to compose and reuse pieces of markup, as well as introducing state through class components.
import { h, Component } from 'preact';

class Counter extends Component {
state = {
count: 0,
};

render() {
const { count } = this.state;
return h("div", null, [
count,
h(
"button",
{
onClick: () => this.setState({ count: count + 1 }),
},
"Increment"
),
]);
}
}
  1. JSX is optional as it requires a build step, but it makes your code a lot prettier.
import { h, Component } from 'preact';

class Counter extends Component {
state = {
count: 0,
};

render() {
const { count } = this.state;
return <div>
{count}
<button
onClick={() => this.setState({ count: count + 1 })}>
Increment
</button>
</div>
}
}
  1. Hooks are also optional. If you want to go full-on function components, you have to opt-in to hooks:
import { h } from 'preact'
import { useState } from 'preact/hooks'

function Counter() {
const [count, setCount] = useState(0)
return <div>
{count}
<button
onClick={() => setCount(prev => prev + 1)}>
Increment
</button>
</div>
}

Note that from step 1 on, you can choose to use htm, a small library that allows you to write JSX style Preact (and React) but in tagged template literals. The example from above, without a build step, without JSX, would look like this:

import {
html,
render,
useState
} from "https://unpkg.com/htm/preact/standalone.mjs?module";

function Counter() {
const [count, setCount] = useState(0);
return html`<div>
${count}
<button
onClick=
${() => setCount((prev) => prev + 1)}>
Increment
</button>
</div>
`
;
}

render(
html`<${Counter} />`,
document.querySelector("#app"))
  1. Preact ships its own client-side routing implementation with preact-router. This is also again an opt-in solution, and Preact is aware of alternatives, like wouter, which work as well.

  2. To tick off the tooling part, the Preact team ships its own CLI, which comes with a lot of templates for different use-cases, and wires up things like CSS processing, bundling, transpilation, code splitting, etc.

  3. If you need more, you can consume from the broader Preact ecosystem, where some of the best libraries are provided by core team members.

    • You have already seen htm JSX style components in template literals
    • preact-custom-element is a very tiny wrapper to make Preact work with web components. More on that later.
    • unistore, a small state container for Redux like state management
    • And there is more, the Awesome Preact repository should give you enough insights.

So it’s easy to start with small widgets on your website, especially when you have htm wired up. No builds, just a single library. And you can opt-in to use more and more features until you write full-fledged applications.

Tree-shaking optimized #

Everything about Preact is opt-in. In fact, Preact decides against having default exports where you can suck in the whole framework at once. It requires you to be intentional about everything you load (unless you use the compatibility layer, see below). That way, you only end up with what you actually need.

This is incredibly useful if your bundler works with tree shaking. Do you only need the useState and useEffect hooks? Then you won’t get any others in your production bundle.

I spun up a quick Vite project and compared a couple of simple components, pulling in progressively more features. A simple Greeting component:

function Greeting({name = 'Stefan'}) {
return <p>{name}</p>
}

As well as the counter example from above. Here are the results.

  1. Greeting function component: Minified 8.60kb, brotli: 3.20kb
  2. Greeting class component: 8.64kb, brotli: 3.22kb (most likely due my part being more verbose in a class)
  3. Counter class components, no hooks: Minified 8.73kb, brotli: 3.27kb
  4. Counter function component using useState: Minified 10.02kb, brotli: 3.73kb
  5. Counter function component using useState, logging useEffect: Minified 10.17kb, brotli: 3.79kb

It’s still incredibly small, but when you look at the minified site you see the size slowly increasing. Also, Preact stays small in the long run. Marvin from the Preact core team pointed me to a tweet by @halfnelson_au, where they compared the initial size of a Svelte app to a Preact app, bundle size evolves by increased source size. See the graph (nicked from the tweet) yourself:

Preact stays small. 80kb source become 20KB bundle in Preact, where as 80KB source become 40KB bundle in Svelte

Being optimized for tree-shaking is just another way to express the progressive nature of the framework. The good thing is that you don’t end up with any surprises once you ship. This makes tracking down things a lot easier in the long run.

First level TypeScript support #

If you have been following my blog you might think that TypeScript is a very relevant topic to me. Actually, my TypeScript + React is my most popular resource on this blog.

The @types/react package is excellently written and maintained, but it is a third-party add-on. There might be some differences, and also occasions where the framework and its types are out of sync. Not to mention that types need to be versioned differently as their bugfixes progress differently.

Preact ships types with each release. It is also written in TypeScript, but makes use of adding types via JSDoc comments and maintaining additional types in .d.ts files. Preact is actually an excellent case-study for this approach. If you’re interested, I really invite you to browse through the source. Start here, with the createElement function. You much likely see TypeScript usage as you’ve never seen it before.

Full React compatibility #

To stay as small as possible, Preact gets rid of lots of internal React functionality that you would rarely use in your day to day work. Things include support for React.children, PureComponent, forwardRef, etc. Legacy functionality that’s deprecated, but still supported. Framework internals. That kind of thing. This also means that things like hooks are not part of the main package, as everything in Preact is opt-in.

But Preact is designed to be a drop-in replacement for React, so this functionality is required by legacy applications, by frameworks like Next.js, or maybe by the component library that you include in your application. Preact patches this functionality via their preact/compat library.

This library includes all hooks, as well as patches for everything that was intentionally dropped to reach the 3KB goal. There are some guides on the Preact website on how to alias React to work with Preact, but my most favorite way is NPM aliasing.

NPM aliases install NPM or GitHub packages under a different name. That way you can point react to preact-compat, and the module resolution of Node figures out things on its own. I switched to Preact for all my Next.js projects, and all it took me was adding those lines:

{
//...
"dependencies": {
"preact": "^10.4.6",
"preact-render-to-string": "^5.1.10",
"react": "github:preact-compat/react#1.0.0",
"react-dom": "github:preact-compat/react-dom#1.0.0",
"react-ssr-prepass": "npm:[email protected]^1.0.1",
//...
}
}

And, of course, removing the old installations of react and react-dom. Switching to Preact shaved off 110 KB of minified production JavaScript, which resulted in 34 KB of minified + gzipped JavaScript.

Aliasing for other frameworks can work differently. Check out how to alias for popular bundlers and dev environments here.

Authoring library for web components #

Web components are always a good way to start a hot discussion on Twitter. And there seems to be this existential divide between people who prefer their shiny tech framework, and others who love “using the platform”. I love that Preact doesn’t give a damn about this discussion. It just loves and supports both sides.

There’s a little library called preact-custom-element that allows you to register you Preact components as web components.

import register from 'preact-custom-element';
import { h } from 'preact';
import { useState } from 'preact/hooks';

function Counter() {
const [count, setCount] = useState(0)
return <div>
{count}
<button
onClick={() => setState(prev => prev + 1)}>
Increment
</button>
</div>
}

register(Counter, 'my-counter')

Preact also allows you to think of web components not as an authoring layer for your apps, rather than a compile target or distribution possibility. You still write your apps and components with JSX in the virtual DOM, just as you are used to from React. But it’s easy to compile to a web component for distribution. Since Preact is so small, it also makes sense to use it as a web component run-time. Pick the level where you want to draw the line to enter web components land: Down at the presentational components, up at a widget level, or an entire application? All of it? It almost comes for free.

Independent #

I love React for its technical ingenuity and have the utmost respect for its core members and the innovation they bring along. Plus, members like Dan Abramov also put the human at the center and help foster a welcoming community.

Still, React is a framework by Facebook and for Facebook. It solves Facebook’s needs first, and it’s just coincidence that they have a lot in common with the needs of everybody else. Personally, I got weary of buying into a framework that is developed by a big tech corporation.

And you know… there’s an ethical dilemma.

Preact is independent. It is backed by Google projects, sure, but it’s not a Google framework. This also means that the team can work on things that would not end up on React’s roadmap if it’s not part of React’s overall goals. Preact ships an official router. Preact has an official web components layer. The Preact team ships a way to write without a build step.

I’m aware that those things exist for React as well. But it’s an explicit non-goal of the React team to provide everything.

Independence gives Preact the ability to innovate in certain areas, like performance and compatibility with the broader web ecosystem.

Gotchas #

There are a few gotchas that need to be pointed out.

  1. React compatibility works great until it doesn’t. Most of the time, React compatibility fails if people didn’t link to React as peer dependency but rather as a real dependency. This way you almost have no chance of aliasing to Preact through one of the means.
  2. The ecosystem is big, but maybe hard to navigate around. Especially if you want to progress continuously, it’s not always clear what your next steps are. But hey, maybe this can be solved through a “Learn Preact” guide here on the blog? What do you think?

Other than that, I haven’t found any caveats. It’s fun to use. As fun to use as React. Maybe even a little more…

What about Context? #

It has Context. Apparently the lack of Context is a myth that won’t fade!

Further information #

If you can spare an hour, I recommend watching those two talks.

  • Sara Vieira’s Preact talk at React Day Berlin. Sara gives an overview of Preact, highlighting some points that I make here, but others as well. The talk is already older, but the main philosophy around the framework and most points she makes still hold up.
  • Jason Miller’s talk at JSConf EU 2017. Jason gives insights on the inner workings of Preact and how concepts like DOM diffing and JSX are implemented in such a performant manner.
  • Marvin Hagemeister’s talk at Script’19. Marvin does an excellent talk on code golfing. He’s a member of the Preact core team and shows how they managed to include an entire virtual DOM implementation while staying small. Highly recommended talk!

And if using Facebook’s libraries keeps you up at night, take a look at this project by Andy Bell.

More articles on Preact

TypeScript and ECMAScript Modules

TypeScript + React: Why I don't use React.FC

TypeScript + React: Component patterns

Stay up to date!

3-4 updates per month, spam-free, hand-crafted. Our newsletter gives you links, updates on fettblog.eu, conference talks, coding soundtracks, and much more.