Help
Support Us
, written by
Jovi De Croock

Preact X, a story of stability

A lot of you have been waiting for Preact 11, announced in an issue opened way back in July 2020, and to be clear I was one of the most excited people for v11. When we started thinking about Preact 11 we believed that there was no way to introduce the changes we had in mind in Preact X without breaking changes, some of the things we had in mind:

  • Using a backing VNode structure to reduce GC, by doing this we'd only use the result of h() to update our backing-node.
  • Reconciler performance, to allow optimized paths for mounting/unmounting/etc
  • Some changes like removing px suffixing, forwardRef and breaking IE11 support.
  • Keeping ref in props.
  • Addressing event/child diffing bugs.

These were our initial goals for v11, but upon going down this path, we realised that many of those changes weren't actually breaking changes and could be released directly in v10 in a non-breaking way. Only the third point, removing the px suffix and passing ref directly in props as well as dropping IE11, fall into the breaking changes category. We went with releasing the other features in the stable v10 release line, which allows any Preact user to benefit from them immediately without having to change their code.

Preact has a much bigger userbase today compared to when we made the original plans for v11. It enjoys wide usage in many small to big companies for mission critical software. We really want to be sure that any breaking changes we may introduce are absolutely worth the cost of moving the whole ecosystem over to it.

As we were experimenting we went a new type of diffing, named skew based diffing, we saw real performance improvements as well as it fixing a bunch of long-running bugs. As time went on and we invested more time in these experiments for Preact 11, we started noticing that the improvements we were landing didn't need to be exclusive to Preact 11.

Releases

Since the aforementioned Preact 11 issue there have been 18 (!!) minor versions of Preact X. Many of them have been directly inspired by work done on Preact 11. Let's go over a few and look at the impact.

10.5.0

The introduction of resumed hydration -- this functionality basically allows suspending during the re-hydration of your component tree. This means that for instance in the following component tree we'll re-hydrate and make the Header interactive while the LazyArticleHeader suspends, in the meanwhile the server-rendered DOM will stay on screeen. When the lazy-load finishes we'll continue re-hydrating, our Header and LazyArticleHeader can be interacted with while our LazyContents resolve. This is a pretty powerful feature to make your most important stuff interactive while not overloading the bundle-size/download-size of your initial bundle.

const App = () => {
  return (
    <>
      <Header>
      <main>
        <Suspense>
          <LazyArticleHeader />
          <Suspense>
            <article>
              <LazyContents />
            </article>
          </Suspense>
        </Suspense>
      </main>
    </>
  )
}

10.8.0

In 10.8.0 we introduced state settling, this would ensure that if a component updates hook-state during render that we'd pick this up, cancel prior effects and render on. We'd of course have to ensure that this didn't loop but this feature reduces the amount of renders that are queued up because of in-render state invocations, this feature also increased our compatability with the React ecosystem as a lot of libraries relied on effects not being called multiple times due to in-render state updates.

10.11.0

After a lot of research we found a way to introduce useId into Preact, this required a ton of research of how we could go about adding unique values for a given tree-structure. One of our maintainers wrote about our research at the time and we've iterated on it ever since trying to make it as collission free as possible...

10.15.0

We found that a pass through re-render resulting in multiple new components re-rendering could result in our rerenderQueue being out of order, this could result in our (context) updates propagating to components that would afterwards render again with stale values, you can check out the commit message for a really detailed explanation! Doing so both batches these updates up as well as increased our alignment for React libraries.

10.16.0

In our research for v11 we went deep on child diffing as we were aware that there were a few cases where our current algorithm would fall short, just listing a few of these issues:

Not all of these resulted in a bad state, some just meant decreased performance... When we found out that we could port skew-based diffing to Preact X we were thrilled, not only would we fix a lot of cases we could see how this algorithm behaves in the wild! Which in retrospect, it did great, at times I would have wished we had good testbeds to run these on first rather than our community having to report issues. I do want to use this opportunity to thank you all for helping us out by always filing considerate issues with reproductions, you all are the absolute best!

10.19.0

In 10.19.0 Marvin applied his research from fresh to add pre-compiled JSX functions, this basically allows you to pre-compile your components during transpilation, when render-to-string is running we just have to concatenate the strings rather than allocating memory for the whole VNode tree. The transform for this is exclusive to Deno at the moment but the general concept is present in Preact!

10.20.2

We have faced a number of issues where an event could bubble up to a newly inserted VNode which would result in undesired behaviour, this was fixed by adding an event-clock. In the following scenario, you would click the button which sets state, the browser interleaves event bubbling with micro-ticks, which is also what Preact uses to schedule updates. This combination means that Preact will update the UI, meaning that the <div> will get that onClick handler which we'll bubble up to and invoke the click again toggling this state immediately off again.

const App = () => {
  const [toggled, setToggled] = useState(false);

  return toggled ? (
    <div onClick={() => setToggled(false)}>
      <span>clear</span>
    </div> 
  ) : (
    <div>
      <button
        onClick={() => setToggled(true)}
      >toggle on</button>
    </div>
  )
}

Stability

The above are some cherry-picked releases of things that our community received without breaking changes, but there is so much more... Adding a new major version always leaves a part of the community behind and we don't want to do that. If we look at the Preact 8 release line we can see that there's still 100.000 downloads in the past week, the last 8.x release was 5 years ago, just to show that a part of the community gets left behind.

Stability is great, we as the Preact team love stability. We actually released multiple major features on other ecosystem projects:

We value our ecosystem and we value the extensions being built through our `options API`, this is one of the main drivers behind not wanting to introduce these breaking changes but instead, allow you all to benefit from our research without a painful migration path.

This doesn't mean that Preact 11 won't happen but it might not be the thing that we initially thought it would be. Instead, we might just drop IE11 support and give you those performance improvements, all while giving you the stability of Preact X. There are many more ideas floating around and we're very interested in the wider Preact experience in the context of meta-frameworks that provide things like routing out of the box. We're exploring this angle in our vite preset as well as Fresh to get a good feel what a Preact first meta framework should look like.

Built by a bunch of lovely people like @NekR.