Back

Hyperapp – Is It the Lightweight 'React Killer'?

Hyperapp – Is It the Lightweight 'React Killer'?

For a while now, JavaScript and its ecosystem have been thriving. From different tooling categories, UI frameworks have enjoyed immense popularity. New libraries and frameworks seem to pop up almost every day, while Vue, React, and Angular persistently lead the way.

From these new options, there are some that deserve your attention. I’m thinking Svelte, Solid, and Hyperapp - an ultra-lightweight UI framework that we’ll be taking a further look at.

What is Hyperapp?

Hyperapp isn’t really that new. It’s been around since the end of 2016 and gained a lot of popularity over this period. It’s gained over 18K GitHub stars on its repo and over 2.5K weekly downloads on NPM.

It’s easy to see why so many developers got interested in the library given its feature set. Tiny footprint, high performance, simplistic architecture, and development experience designed around not using any bundler or compiler - these are all very useful features.

v2

Hyperapp reached its popularity peak middle of 2020 and dropped quite a bit since. That’s potentially due to the rise of v2 of the framework, which, although brought many improvements, also came with many breaking changes.

These breaking changes caused the already small ecosystem to crumble and the loss of some handy features like JSX, and TypeScript typings, with both being still worked on for v2.

Bright future

With that said, Hyperapp still has a bright future ahead of it. Given all of its current advantages and continuous improvements, the framework is on track to match or surpass its prior popularity peak.

Now, to give you a better sense of what Hyperapp is about, let’s compare it with the most well-known JS UI library out there - React.

Comparing performance

Let’s start with performance - one of the strongest advantages of Hyperapp.

Benchmark

For such comparisons, I often start with an awesome, open-source benchmark that covers many JS UI frameworks and libraries - including Hyperapp. You can see the most recent results here, and an example table below:

Hyperapp speed benchmark

Although synthetic benchmarks don’t always reflect real-world performance, they provide us with a rough, easy-to-compare metric.

Above, you can see Hyperapp trading blows with Solid and Svelte. It’s a very impressive result, especially considering that we’re comparing compiler-backed frameworks to a pure-runtime one.

As for React - it’s the last in the whole group. That’s nothing new, considering that React isn’t known for its high performance, rather than the ecosystem, innovation, and overall leading the trends.

Bundle size

Size is another metric determining the overall framework’s performance. Although it’s becoming increasingly less important with modern devices and high-speed connections, it’s still worth considering for low-end devices and other constrained environments.

Hyperapp’s really lightweight - about 1.7KB minified + gzipped (what’s downloaded), and 3.7KB minified (what’s parsed and executed). That’s for Hyperapp core library, which is perfectly usable on its own.

As for React, we’ve got 2.8KB min-gzip and 7KB min-only. That’s already almost ×2 the size of Hyperapp, and it goes much higher when we factor in react-dom (required for rendering to DOM) - 39.4KB min-gzip and 121.1KB min-only.

These results translate nicely into the startup timing, measured by the previously-mentioned benchmark:

Hyperapp startup benchmark

Obviously, these benefits come at the cost of the framework’s feature set. Its minimalism and simplicity might not be good for everyone, but it’s just enough to get the job done. Let’s see how it compares in this regard.

Comparing architecture and reactivity model

Framework’s architecture, design, and API are what determines the end development experience and workflow. In the case of Hyperapp, it’s clearly going the minimalistic route, packing only the features you need while focusing heavily on performance. How does it affect its usability?

Initial setup

Right from the start, you can see stark differences between React and Hyperapp’s philosophies. While React pushes you towards packages & bundlers or CDNs, Hyperapp focuses on native ESM modules. Take a look at a primary example from the README:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script type="module">
      import { h, text, app } from "https://unpkg.com/hyperapp"

      const AddTodo = (state) => ({
        ...state,
        value: "",
        todos: state.todos.concat(state.value),
      })

      const NewValue = (state, event) => ({
        ...state,
        value: event.target.value,
      })

      app({
        init: { todos: [], value: "" },
        view: ({ todos, value }) =>
          h("main", {}, [
            h("h1", {}, text("To do list")),
            h("input", { type: "text", oninput: NewValue, value }),
            h("ul", {},
              todos.map((todo) => h("li", {}, text(todo)))
            ),
            h("button", { onclick: AddTodo }, text("New!")),
          ]),
        node: document.getElementById("app"),
      })
    </script>
  </head>
  <body>
    <main id="app"></main>
  </body>
</html>

You can see how Hyperapp focuses on pure JS, runtime-based usage. That’s why things like TypeScript or JSX support aren’t the highest priority.

This focus makes such high levels of performance possible, and it’s why the framework is so simple and minimalist.

On the contrary, React focuses extensively on JSX and thus requires code pre-processing. It’s not to say that this approach is worse or better in any regard - it’s just different.

With that said, both frameworks can still be used in a variety of ways. It’s just that Hyperapp presents a pure, no-bundlers ESM module way as an official recommendation for production.

Templating syntax

As far as creating your views go, both React and Hyperapp work similarly under-the-hood. It’s just that React’s support and push towards JSX made it the go-to choice.

In the case of Hyperapp, again, the support for JSX is in the works, but it’s not the primary focus anyway. Instead, it’s the React.createElement() equivalent in the form of h() , and text() functions. If you’re wondering why text() is a thing - it’s for additional performance.

If you don’t want to repeatedly type h(“tag”, …), then you could look into the official @hyperapp/html package. It’s a collection of shortcut functions that simplify h() calls to tag() calls, while still being usable without bundlers. Here’s an example:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script type="module">
      import { app } from "https://unpkg.com/hyperapp"
      import {
        main,
        h1,
        button,
        text,
      } from "https://unpkg.com/@hyperapp/html?module"

      const Subtract = (state) => ({ ...state, count: state.count - 1 })
      const Add = (state) => ({ ...state, count: state.count + 1 })

      app({
        init: (count = 0) => ({ count }),
        view: (state) =>
          main([
            h1(text(state.count)),
            button({ onclick: Subtract }, text("-")),
            button({ onclick: Add }, text("+")),
          ]),
        node: document.getElementById("app"),
      })
    </script>
  </head>
  <body>
    <main id="app"></main>
  </body>
</html>

JSX support With that said, it’s easy to see how JSX might still be more appealing than both of the provided syntaxes.

To be fair, there are ways to use Hyperapp with JSX or even template literals already. It’s just that as long as the official support is in development, it’s hard to pick the “go-to” implementation.

Hyperapp’s flexibility allows you to use many view-constructing syntaxes, given some time and effort. Though, most of them will come with additional complexity and potential performance decrease.

Components

Both Hyperapp and React allow you to create components for better reusability of UI logic. In React, components can be created as plain functions but with predictable structure, and they can have a local state.

const Example = (props) => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>{props.children}</button>
    </div>
  );
}

As for Hyperapp, its components look similar. However, unless you want to use a specific templating syntax, you don’t have to stick to any structure. Just make it so that your function results in the creation of some virtual nodes ( h() and text() returns), and that’s it!

const container = (content) => h("div", { class: "container" }, text(content));

Now, arguably you could do the same in React without JSX, but it wouldn’t seem equally natural. And aside from that, passing accepting props object as the only parameter is a good base practice.

No local state With that said, there’s one big difference between Hyperapp and React components, and it’s the absence of a local state.

In Hyperapp, the entire state is defined at the very start of an app and has to be passed down the node tree. It can then be interacted with using actions, effects and subscriptions.

Reactivity

Implementation of reactivity (state management, view updates, etc.) is where the two frameworks differ immensely.

React hooks On the side of React, hooks have been the primary way of adding reactivity to your components for quite a while now.

import React, { useState, useEffect } from "react";

const Example = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
};

They’ve been well received, and the core ideas behind them have inspired the likes of Vue’s Composition API and a big part of Solid’s API.

Hyperapp API Hyperapp takes a different approach. Instead of just hooks, you’ve got actions, effects, and subscriptions.

It might seem a bit complex at first, but it really isn’t, and additional separation allows for an easier understanding of all the concepts, which are just that - concepts! They’re either simple functions with expected input and output or data structures fitting given requirements.

There are no additional APIs. In fact, the entire API of the framework is just the h() and text() templating functions, as well as app() serving as an entry point for any Hyperapp app and memo() for easy view memoization. That’s fewer functions in the entire framework than core hooks in React!

Now, we won’t be diving deep into these Hyperapp concepts and APIs; however few there might be. Instead, let’s do a quick rundown. App We start from an app() call, which initializes and mounts Hyperapp to DOM.

import { h, text, app } from "https://unpkg.com/hyperapp";

app({
  init: { todos: [], value: "" },
  view: ({ todos, value }) => h("main", {}, []),
  node: document.getElementById("app"),
});

init is where the state gets initialized, view serves as the main rendering function, and node as the mounting point.

Actions To change the state, you have to use actions - functions which, given the current state and additional payload, output a new state.

const AddTodo = (state) => ({
  ...state,
  value: "",
  todos: state.todos.concat(state.value),
});

const NewValue = (state, event) => ({
  ...state,
  value: event.target.value,
});

app({
  init: { todos: [], value: "" },
  view: ({ todos, value }) =>
    h("main", {}, [
      h("h1", {}, text("To do list")),
      h("input", { type: "text", oninput: NewValue, value }),
      h(
        "ul",
        {},
        todos.map((todo) => h("li", {}, text(todo)))
      ),
      h("button", { onclick: AddTodo }, text("New!")),
    ]),
  node: document.getElementById("app"),
});

You can use actions in DOM event listeners, subscriptions, and even the init property for complex state initializations.

Subscriptions Subscriptions provide a way for dispatching actions, and related side-effects, based on events outside of Hyperapp’s control.

const keySub = (dispatch, props) => {
  const handler = (ev) => {
    if (props.keys.includes(ev.key)) {
      // Dispatch action
      dispatch(/*...*/);
    }
  };
  window.addEventListener("keydown", handler);

  // Cleanup function
  return () => window.removeEventListener("keydown", handler);
};
// Helper
const key = (props) => [keySub, props];

// Usage
app({
  // ...
  subscriptions: (state) => [
    key({
      keys: ["w", "a", "s", "d"],
      action: ChangeDirection,
    }),
  ],
});

Subscriptions are tuples, i.e., 2-element arrays, where the first value is the subscription’s function, and the second one is props to be passed to it.

You can register subscriptions from the app() call under the subscriptions property. There, you can add or remove your subscriptions based on the current state.

Effects As for the mentioned effects, they can be used for handling side-effects, like data fetching.

const httpFx = (dispatch, props) => {
  // Side-effect
  fetch(props.url, props.options)
    .then((res) => res.json())
    .then((data) => dispatch(/*...*/)); // Dispatch action
};
// Helper
const http = (props) => [httpFx, props];
// Usage - action
const GetPizzas = (state) => [
  state,
  http({
    url: "/pizzas",
    action: SetPizzas,
  }),
];

// Usage - view
h("button", { onclick: GetPizzas }, text("Get pizzas"));

Like subscriptions, effects are tuples consisting of a function and props and can be executed by actions when they return an array of the new state and all the effects instead of the new state directly.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

Comparing ecosystems

With performance and API behind us, all that’s left to investigate is the ecosystem, community size, and documentation.

Ecosystem and community

In terms of ecosystem and community, it’s become clear that unless you’re React, Vue, or Angular, you are going to struggle. Although some recent trends push for framework-independence, these 3 UI frameworks, and especially React, still have strong backing in this regard.

Hyperapp isn’t an exception. It doesn’t have nearly as large a community and an ecosystem of a tiny fraction of React’s size. Again, this doesn’t at all mean that it’s useless.

The community, however small, is very committed and actively works on improving the framework. Hyperapp works beautifully with framework-independent tools, CSS frameworks, and a handful of official and 3rd-party dedicated libraries available.

Documentation

Now, documentation is the go-to resource for starting with any framework or library.

React docs are really good - not perfect, but close. There’s a dedicated landing page, detailed API docs and concept overviews, a full-blown introductory tutorial, and a community directory with useful links, of which there are tons and more unlisted.

As for Hyperapp, docs are surely not its strength. There’s no dedicated landing page (though there was one for a brief period of time), and limited documentation consisted of 2 Markdown files and a README file.

On the contrary, given Hyperapp’s simplicity, it could be argued it doesn’t need as much documentation as, e.g., React. The current one-page API docs and tutorial should be enough to give the developer a good understanding of the tool. There’s also some good community-written content.

At the time of writing, there’s an open PR with a massive amount of new docs, so it’s worth keeping an eye on it.

Summary

So, to summarize this comparison, is Hyperapp a “React killer”? No, but it is a good alternative for those striving for simplicity, performance, or both. Those two are the primary focus of Hyperapp’s development.

The future looks bright for Hyperapp. Its most notable pitfalls, like JSX support, TypeScript typings, or good documentation, have opened PRs. The framework is constantly being improved upon and has a clear goal.

So, if this comparison made you interested in Hyperapp, go check it out!