Preact Signals
github.com
github.com
If you build an application that's genuinely slow because of the library you're using to pass data around, rather than the way you're using that library, I will eat my hat, your hat, and many other hats. In fact, I will adopt an entirely hat-based diet.
I think it's easy to do things like this in the name of "better" because they're measurable.
I'd advise that if you've not accumulated Gandalf levels of experience yet, or you genuinely don't really need to reduce bundle sizes, or eek out an extra 10ms of execution time, your energies are best focussed on creating better software for users (here's the important bit) _that doesn't hurt the developers who'll need to modify it in future._
Why? Because bad code compounds in cost. When you make a decision and write code, it always results in other tangential decisions to need to be made in future. If your decision makes future decisions harder, they're more likely to be wrong. And then it makes all the tangential decisions after that likely to be even harder. Compounding, like interest, but the bad kind: debt.
So it's far more valuable to invest your time in learning to make inoffensive decisions than to go swapping libraries or trimming bundle sizes in the name of kilobytes.
On that front, it's more noble to choose a framework because it makes the pit of success as wide as possible, helping even the least skilled engineers make changes that are simple to change, fix or undo when needed, than to save 30ms per user, only to lose it because someone screwed up because of some bad code you wrote in one of the features.
However, bundle size has a direct impact in loading speed and there are multiple thresholds where even kilobytes can matter.
The only time you can ignore it is if your users will exclusively access your website from a direct broadband connection.
I work on web apps and I've learnt from experience that optimising software for simplicity and ease of change over a few milliseconds or kb is far more valuable than a lot of people would have you believe.
Your users will barely notice a few kb or ms. But your colleagues do notice software complexity. Your stakeholders do notice delivery times. Your directors do notice costs. And ultimately you'll notice your share value.
That said, you might benefit greatly from fine tuning your bundle sizes and library choices. But what I'm saying, is that maybe you'd make a different choice if you take the time to consider the broader activity of software development.
I worked for an analytics product that never really bothered about performance on load or any of the web vital metrics. When they wanted to strike a deal with a fortune 20 customer, they were questioned on the performance of the app. It was not good. Took around 5 to 10 seconds to load everything on a macbook pro.
Now all of a sudden we needed to fix the performance for all the pages. I helped set up the processes we needed to follow to measure and focus. This was one of the most complex scenarios you can face in an Enterprise product. Touching old code for the sake of speed.
btw, the biggest perf improvement we realized was when we switched from React to Preact. There were some issues to iron out but it was okay considering the perf benefit.
All the API calls you make in a SPA are bound to be much much slower than your asset load time.
A little discipline goes a long way. You probably don’t need to fight against every few kb, but having a bot post the bundle size in every commit can help a lot. If you think you can ignore it, you will, until you can’t ignore it, and then it’s a very annoying problem to solve.
However...
window.requestAnimationFrame in browsers (which is what games will be using to manage their main loop in the background) matches the refresh rate of the display. If you're looking at a game on a a laptop that's 60Hz, but if you're looking at it in a VR display it's probably 90Hz, or on a top end flagship phone it might be 120Hz. Games, even games written in React, manage to do this quite easily so long as the dev doesn't screw it up. You definitely don't need a 'faster state library' to do it well.
Yes, of course you're using RAF for the game loop. Not sure of the relevance of that comment. I just said 60 to simplify things.
> Games, even games written in React, manage to do this quite easily
That's a really broad and generalising comment. All games are different. My way works but only because I'm using a library that is fast enough. Sure, maybe I should batch update calls and do all sorts of optimization techniques on my end, but why would I when I can just do it like this and focus on the core gameplay instead? Are you saying that libraries should not be performant in order to punish lazy developers like me?
It is though. We've all used web apps where something doesn't nail 60fps and it's so jarring. I used one yesterday where typing in a textbox was laggy. WTF is the dev who made it doing when a textbox is slow?!
This is the sort of problem that developers face and, if they're not great at their job, they reach for a library that claims to be faster rather than looking at their code to work out why it's slow. The impact of marketing something as "magically better perf by using this super special library!" is that developers stop trying to make things fast enough themselves.
Worse, some people start believing all web apps are slow because the web must be slow by default if you need these clever tricks that only libraries can provide to be fast. It's nonsense. Browsers are pretty damn fast and if you encounter a webapp that's slow it's almost guaranteed to be the fault of the developers who made it and not the libraries it's built on.
There is no library that a bad dev can't make slow software with.
Each framework has its quirks. Do you optimize for correctness or speed? Does every update need to be 100% correct/consistent or can you allow it to drop updates? These are performance details that the framework should provide tooling to deal with, or it may not, and it may have abstracted away all these decisions and made it impossible to fix the textbox lag unless you hack into the DOM manually. Which framework you use matters a lot.
What sort of Game UI has to be updated 60 times per second and would be implemented with something like React?
In-game UI like healthbars would be implemented in canvas/webgl or whatever you use for the game itself. The UI around the game can be implemented as DOM and overlayed, but rarely have to be updated 60 times/second
Don’t bother trying to render the DOM 60 fps, it’s unnecessary. Render your actual game with canvas and use regular react for your UI. If for some reason your UI needs to render that often, move it into canvas too.
In my experience anything that needs to be animated should be done with canvas, otherwise at some point you're either going to hit performance walls or end up fighting the DOM.
And even if it's just text that's explicitly something you shouldn't do in canvas if you care about performance: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/...
> Avoid text rendering whenever possible.
So then I'm forced to create rendered font glyphs and use tooling for the texture atlas when drawing characters. Again, much easier just overlaying an element and do it in the DOM.
1. For React, a hook is injected into every single component (!) regardless of whether it uses signals or not via __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.
2. For React, React.createElement is patched so that it can render signal values as Text nodes.
3. For Preact, they claim it's using a "pluggable renderer", yet they monkey-patch the global shouldComponentUpdate (and they don't call the old one so they break other patchy libraries)
[1] - https://github.com/preactjs/signals/blob/d25e8bac09c94ed3bad...
[2] - https://github.com/preactjs/signals/blob/d25e8bac09c94ed3bad...
[3] - https://github.com/preactjs/signals/blob/d25e8bac09c94ed3bad...
I actually like how useEffect has an explicit dependency array because you know when it will be triggered. Signal effects are implicit, and if you don't want it to trigger you have to use .peek(). I think I prefer React's explicit accessing of previous state to peeking when needed.
Why can you destroy effects? That seems like a recipe for disaster. Again, I like how hooks are permanent top-level calls. You know they always run.
When are effects executed? React makes it explicit that they are executed after the render.
What is the purpose of batching other than performance? Are there any possible negative effects of making multiple signal updates without batching?
Writing this has made me a realize I really like the explicitness of React's model.
I guess performance in some cases, but mainly better developer experience.
> I actually like how useEffect has an explicit dependency array because you know when it will be triggered. Signal effects are implicit, and if you don't want it to trigger you have to use .peek(). I think I prefer React's explicit accessing of previous state to peeking when needed.
Fair, but in real code, or at least in the code that I write, peeking/untracking is not nearly as common as just reading a signal, on average this should be cleaner.
Also with this execution model you can support conditional dependencies, with the dependencies array model the array of dependencies is fixed, so your effect potentially will re-execute even for things that you don't really even read at some point.
> What is the purpose of batching other than performance?
There's no purpose to batching other than performance.
> Are there any possible negative effects of making multiple signal updates without batching?
A signal can cause other effects/memos to execute, and they can do arbitrary computations, so the cost of over-executing can be arbitrarily high.
I care more about explicitness than cleanliness though, especially if my effect touches 3+ state variables in a relatively large function.
I can definitely believe that the average case is better with signals, but is the average case even important? I don't really care about the average case with something like state management because all the libraries are pretty good for that. It's the edge cases and complex state logic I care more about.
Yeah useState, setState, prevState blah blah blah is a lot of overhead for the average case but I really like the explicitness it provides me in the not-so-average cases.
What do you mean by conditional dependencies?
> What do you mean by conditional dependencies?
Random example:
useEffect ( () => {
if ( !supportsThemes () ) return;
if ( darkMode () ) {
doSomething ( darkTheme () );
} else {
doSomething ( lightTheme () );
}
}
In a reactive system this is all you have to write, if the first check never passes you only read "supportsThemes" so you will only be listening to that. If that passes in the future then maybe you'll start listening to "supportsThemes", "darkMode" and "darkTheme", but not "lightTheme". If "supportsThemes" becomes false again you'll automatically stop listening for "darkMode" and "darkTheme".In React this works differently, all those signals now become dependencies in the dependency array, the entire array will be diffed each time, whenever any of them change your effect will be re-executed, even if something you don't care about in that moment changed.
Basically the same problem exists for hooks too, all hooks must be called all the time, which also as a side effect implies that some hooks calls need to become non-sensical, like if I don't have an onChange function what do I need to call useDebounce for it for? Should the hook support receiving a non-sensical null argument? Should I call it with a pointless noop function? This problem doesn't exist either in a reactive system, fundamentally because dependencies are dynamic, if you don't want to call a hook you can just not call it. If you want to nest effects you can do that.
Which is basically re-inventing a dependency array... except it's not explicit and now I have to look at another function as well.
I don't understand how you are supposed to reason about complex effects with the signals model.
> Random example
Why would I want to complicate things with conditional effects though? It makes it so much harder to debug and reason about.
> the entire array will be diffed each time, whenever any of them change your effect will be re-executed, even if something you don't care about in that moment changed
An array diff on the order of ~10 elements isn't an expensive operation and I can implement conditional logic in React just like you've demonstrated here. What is different about signals in regards to conditional flows?
> if you don't want to call a hook you can just not call it. If you want to nest effects you can do that
I'm not convinced this is desirable. Complex implicit reactive effects seem fragile and difficult to debug and reason about. This is somewhat mitigated with React because of the Rules of Hooks, which gives you expectations about how your code/system should behave but even then people have complained about them being difficult to understand.
This feels like a step backwards in terms of complexity. Without better tooling/observability (E.g. easily trackable effect history, value history, etc that is integrated into the dev env) implicit reactive dataflow based systems feel like they will be way harder to manage.
Incidentally the “rules of hooks” are frustrating precisely because the expectations they impose around hook use are confusing and unintuitive.
If you're building something that is heavily focused on computing values rather than more event driven effects I could see a reactive system being more useful and appropriate though.
I'd love to hear from developers/teams who have built complex systems with a reactive framework (Solid, Signals, etc) to hear about the upsides and downsides of doing so.
It is definitely easier to reason about dataflow in a good incremental library with dependency autotracking ("Self-Adjusting Computation"[1]) than to reason about nondeterministic concurrent rendering in React :)
I believe it does. Or there won't be a huge discrimination between theoretical react performance and real world web sites. React developer expect user to do all required optimization themselves. But web developer don't even care. (as long as they can ship it without serious complain)
Developers always tend to do it the easy way instead of the proper way.
If the easiest way is messed up. Then it is.
This is an anti-feature.
To elaborate, a “conditional dependency” is still a dependency as much as any regular dependency would be.
The only difference is that the conditional check is now taken outside the dependent code, (where the condition is both explicit and colocated with the dependent code) and put somewhere else, becoming less explicit and harder to find.
Also maybe there are good reasons for disliking it, but this seems harder to mess up, by default all dependencies are accounted for, automatically, that's not the case with dependencies arrays.
It has better developer experience when you apply it to optimize performance. It is impossible to beat from-scratch recomputation in terms of DX.
It is also an optimization and I agree that it is worse in terms of DX than autotracking dependencies.
In contrast, with React I don't really have to worry about that because it explicitly mandates one way data binding with only a few places that data can be changed, such as in click handlers. I can basically treat the state as a render loop that runs from top to bottom every time. If I want to preserve state, I use useState, and if I want to cause an effect, I use useEffect.
I think the appeal of a reactivity system is different, for some people is better performance, for others it may be better memory usage, for others it may be better DX as a bunch of "workarounds" stop being needed (like dependencies arrays, rules of hooks, useRef, useEvent...), for others it's just simpler to understand and reason about.
These days with Vue 3, it’s even more explicitly one way.
Both React & Solid are consistent here, but I find Solid to be more intuitive.
If you use vue like this. I think you will never use vue properly. In vue, state IS the ui. UI is always direct mapping of state. You setup state, you describe mapping between state and dom. And the rest of things will be maintained by vue. Data is the trust anchor of everything. UI is derived from it and you shouldn't even think about update it yourself.
And vue also makes lots of utils specified to map or handling data change. (Personally I think the most important one is computed, as it is the bridge between the the different form of same state)
Some abuse of the internals may be "safe" because React Devtools relies on this stuff, but I would be very nervous betting production correctness on this strategy long-term. Maybe it's worth it? Really hard call.
Here's the details: https://github.com/preactjs/signals/blob/main/packages/react...
I don't believe this is worth it. The implementation is fairly dangerous and fragile as you point out, all in order to simplify something fairly clean/reacty such as `const value = useSignalValue(signal)` into `signal.value`.
I kind of like keeping things simple by using just Preact and esbuild.
But it actually uses Rollup and esbuild by default (via Vite): Rollup for the HTML/CSS and esbuild for the JS/JSX. It works great and it’s plenty fast.
Solid and Rollup are by the same author.
Agree, it explains much better the motivation behind signals. I wish HN would've linked to that instead of the README of the git repository.
To me the brilliance of React is the VDOM, which allows you to treat your components like a render loop in a game.
What’s the piece that I’m missing that separates Signals from RxJS observables?
At a very high level, the thing that this offers versus (anything) is a very minimal reactivity solution that’s small and designed specifically to be integrated into Preact’s VDOM. Other than that, it’s a whole lot of details at a level that most people won’t care about beyond the minutiae of implementation and the nuance people actively implementing these libraries can provide.
The computed function seems almost identical.
There's a lot of reason we let fear govern language design & direction, but I really believe we also need to give a lot of credit to what we make possible (or not) in these decisions too. For a long time, keeping async & sync separate has been the law of the land. I hope someday we can see values & their changes with less of a everything-neatly-in-one-box-or-another view, & more integratively.
In general signals looks like a synchronous but lazy system. I don't see anything that's actually async in there?
effectCount = effectCount.peek() + 1;
That is definitely absolutely clearly 100% for sure sync code. Adding a numerical value +1 to the result of a function (with no wait on it) makes it clear that .peek() here is synchronously returning a value.But the value getter and the compute tracking looks nicer. Anybody tried it with React pros/cons?
Is a signal just a useState hook?
I’m sure the answers are no. But I’m struggling to see it in the provided examples.
A signal is basically a function that you have to go through to read and write a value. In the case of Preact the function is split into getter and setter assigned to the "value" property. The interesting thing about signals is that they can tell their parent computation to re-execute, automatically, without any manual dependency array.
A computed is a signal generated from a function rather than a primitive. So like the function that generates the value is re-executed automatically whenever any of the signals read inside it change.
* Hooks - `setState` reruns the full code for your component.
* Signals - only the dependent effects/computed/JSX is run.
What I'm complaining about is that the signals seem to have the exact same semantics and use cases with different implementations and performance characteristics.
SolidJs shows you can get most of the functionality of hooks using signals. Who knows, if someday most Preact users converge to using signals for state management, maybe they will throw out hooks altogether. But in their current context, supporting both hooks and signals feels a reasonable choice to me.
EDIT: scroll up to "The global state struggle" first to get more background/comparison to useEffect if it isn't clear.
I have been working on a similar programming model for a while, where this kind of state management is the only approach:
https://github.com/sunesimonsen/dependable-view https://github.com/sunesimonsen/dependable-state
The library has other kinds of agendas like being able to run without a build step, being really small and allow multiple versions in the page.
Examples: https://github.com/sunesimonsen/dependable-example-hackernew... https://github.com/sunesimonsen/dependable-example-todomvc
Stay far far away if you want your app to be maintainable beyond 6 months.
Yes, signals can be used independently of any framework. It runs everywhere JavaScript runs. To use it in node without any framework adapters, import the `@preact/signals-core` package, which just exports the reactivity API.
I've talked to many React engineers and none of them have a good reason to use a library like Preact.
Once again, the author is content to reinvent the wheel unnecessarily and publish it as something that can/should be used in production. It's a useful toy, a nice exercise in "what can be done" but not much else. Yes, I'm aware of the countless "useless" things that get posted to HN all the time - none of them purport themselves to being anything more than what they are.
I have never met anyone with any sense using Preact, and I see no compelling reason to use Signals either when there are countless other libraries doing similar things, with developers that actually use them.
If startup and runtime performance have 0 value there's no reason to use Preact. If startup and runtime performance have greater than 0 value there's some reason to consider using Preact.
- https://github.com/mq2thez/blog/blob/main/upgrade-react-etsy...
- https://www.etsy.com/codeascraft/mobius-adopting-jsx-while-p...
Another example, you know those UIs that might drop down when you click on a chrome extension icon? No need to use react for a simple extension drop down.
If you’re building a 1st party platform or webapp then react makes more sense. But there’s clearly usecases for React-lite.
Regardless, preact core is boring and stable (like 5 years old). I built a multi-million dollar company on it previously for what it’s worth.
Not a particularly useful anecdote, unless you're telling me your choice of Preact was some kind of linchpin. And I'd bet it was most certainly not.
Oh it definitely wasn’t. The other reasons are valid though.