About this talk
This talk focuses on the concept of signals in the context of front-end development, explaining their relevance as reactive variables in JavaScript. The speaker outlines how signals can automate the recalculation of dependent variables, thus preventing errors associated with manual recalculations. They explore the evolution of web reactivity, tracing its journey from a pull approach with server requests to a push approach where the UI is directly updated in the DOM. By outlining the differences between frameworks like React and others that use signals, the session emphasizes the importance of predictability and deterministic UI in application development. The talk also introduces a simplified signal implementation and discusses modern challenges and potential improvements related to state management. Finally, the speaker mentions the TC39 proposal aiming to integrate signals into the JavaScript language, which could optimize performance across frameworks.
Full transcript
All right. Let's get started and let's get started with like a maybe disappointing disclaimer. We've had a lot of good AI talks so far yesterday and today. There's going to be no AI whatsoever in my talk. So Instead, I want to talk about signals and I want to talk about if it lets me. Doesn't let me. It does. I want to talk the customization of the web
if we have time for that. But I think signals are very interesting because especially in the front end ecosystem signals have been kind of like a hot topic over the last couple of years. And it means that people most people have heard about signals by now, but I think a lot of people still don't really know what they are and what all the fuss is about. So
this talk is for those people. It's also for everyone else who just wants to understand more about how signals work underneath the hood and what problems they're actually trying to solve. And to understand that we need to ask a question, what are signals first? So the idea of signals is essentially the idea of reactive variables in JavaScript. So if we take this very basic example, we have
a counter variable and then we have two other variables that are derived values from that counter. So is even is either true or false based on the counter and parity is a string based on that Boolean. Now in plain JavaScript, if we change the counter which we can because it's just a mutable variable um we have to also recalculate all the derived values from that. So we
have to manually do all these calculations again, which is not only tedious, but it's also opens up for errors because if we forget to do it or if we do the calculations slightly differently, we end up with the wrong states. Now, the idea of reactive variables is that we don't have to do that. And instead, we just have some way to tell the compiler or the runtime,
"Hey, these variables that I'm declaring, please recalculate them automatically whenever any other variables in my calculation change." So, just for the thought experiment, this is pseudo code. This has nothing to do with signals just yet. But, let's imagine we had this fancy new assignment operator. Instead of just equal, we use dollar equal, and that tells the compiler, "Hey, anything in here, if any of my dependencies change,
please rerun this calculation." And again, the end goal is that if we change counter, all the recalculations happen for us underneath the hood automatically. We don't have to manually do it. So, why do we want that? Most of what we do as front-end developers, and that might sound a little bit silly, but most of what we do as front-end developers is managing state, turning that state into
UI, managing state changes, and then making sure that the UI reflects those changes correctly. When I started as a developer, the main principle that was flying around was model-view-controller. And the main idea behind that was that your view should always be a pure function of your model. So, in more modern terms, that means like your UI should always be the result of a pure function of your
state. Now, the pure function bit is really important, because basically what we want is predictability. We want if we have any given state, if we feed that into our application, we want the same UI out of it every time. So, again, it's predictability, it's deterministic UI that we get out of it, that makes it much easier to reason about, it makes it much easier to debug, and
it makes it really hard to mess it up and get into inconsistent states with your application. So, how did we get from that to signals, which sounds like really side effecty because like we're poking like something changes automatically underneath the hood? To understand that, we need to take a very quick look at the history of reactivity on the web. And we kind of have to start at
the very beginning with HTML. And I'm using PHP as a server like server side language here. It could be any other server side language. The idea is always the same. We have some form of state. So, in this case, we have the counter coming from the URL. And then we have our templating that describes the HTML based on that state. And then we have some ways to
update the state. In this case, we update the URL, and that means we go back to the server and we request the HTML for the new So, this is basically the perfect example of what we just described of the pure function approach. So, every time the state changes, we go back to the server, we pull the new HTML from the server uh in one go, and we
get the fresh UI from it. So, that's why we call that the pull approach. Every time the state changes, we pull the new UI from the server. And it has all the benefits that I just described. So, it's very predictable, it's very deterministic. It means that your state is always consistent, it's very hard to mess up and get inconsistent states within your application. It comes with the
obvious drawback of performance. So, like every time the state changes, re-rendering your whole page, requesting your whole page again from the server is obviously not really fast. And especially with modern applications where you have a lot of state flying around and a lot of things happening. to try to improve that, we started introducing JavaScript, especially in the era where we had jQuery and then Ajax allowed us
to make server requests from the client even after the initial page load was already done. This gave us the chance to still do the same thing. So, we we're still kind of going back to the server and requesting HTML from the server whenever the state changes, but we can be more um delivered about like what parts of the HTML we actually requesting. So, we don't have to
request the whole page. If we know that only sub parts of the page change, we can request smaller chunks of HTML. So, that speeds up the whole server request cycle. It still It doesn't eliminate it. So, like there's still the performance issue of you have to go back to the server. We're now start also starting to open ourselves up to issues if you like if you forget
the part of the side that changes, we might end up with inconsistent state. So, it's not really ideal. But, the performance penalty gets smaller. So, we start working on that. And then in good old JavaScript fashion, we just went nuts and went all in and said like okay, what what if we just don't go back to the server at all? And we just whenever the state changes,
we know what needs to change in the page. Let's just update the the DOM directly through JavaScript. >> [snorts] >> And this might still look very similar because it's essentially still jQuery. But, fundamentally it changes what we're doing. From the pull approach, we're not going back to the server and pulling the new UI anymore. Now, we're actually pushing the new UI into the DOM directly from JavaScript.
And that means that it's super fast because again, we're completely cutting out the server now. But, it eliminates all of those benefits that I was describing before of the pull approach, the deterministic, um predictable state change, and uh consistent UI. this was great to fix the performance, not so great for these kinds of things. The other thing with jQuery is that it was really messy. Like it's
famous as the spaghetti code era for a reason. We did a lot of things very manually, reinventing the wheel. Um to try to solve that, we started introducing what we would call application frameworks. So, I'm calling out Knockout here for two reasons. For one, uh it's it was one of the first application frameworks that I personally used. Um, but most application frameworks, not only at this time,
but like to this day, they all fundamentally work on the same three principles. We have three layers here that help us describe our The first one is, like I said before, state management. So, we have a way here, uh, in the view model, that helps us define our state, the counter parity, and our actions that can change the The second layer is the templating. So, we describe
our HTML quite deterministically, um, to describe what the output should be. And then, the important bit is the third layer, where we actually combine those two and have a data binding layer that allows us to access our state within our template. And fundamentally, this is still a push approach. So, Knockout, underneath the hood, whenever our state changes, it updates the DOM directly and is very perky about
it. >> But, it makes the developer experience feel a lot more pull-like by having this deterministic- looking templating format. So, it's trying to get the best of both worlds. You have still the performance benefits of doing the push but it from a developer experience, it feels like a pull approach. The other reason why uh, calling out Knockout is because it was one of the first frameworks that
introduced the idea of observables for your state management. And observables is what we would now call signals. So, it's the idea of I have a variable, but I want to react to whenever it's changing, I want to do something. And in Knockout's case, I want to change wherever that variable is actually used, I want to make sure that the DOM updates Um, and with observables, it also
has the concept of computeds. So, that's the idea of having derived values from your observables. So, for a computed, we just define like how it's being calculated, but whenever any of the dependencies change, we want to automatically recalculate this computed value. this idea of having these three layers and having the idea of like observables and computed values kind of stuck with it. So, we see more and
more frameworks after this coming out. View in the very early days had the idea of data and which is your state and again computed which is your derived Uh Svelte this might not look like it, but Svelte also in the very beginning already had the concept of signals. It just did it underneath the hood for you in a custom compiler. They then realized that that's a little
bit too much magic, so they introduced the idea of runes to just do the same thing but make it more explicit. So, again we have our state uh and we have our derived state through the runes. Uh and Solid is probably most famous for starting to coin the term signals. Um at least they get all the credit for it. Um but again the idea here is is
exactly the same. So, we have a create signal helper and we have a create computed helper and all of this just allows us to have reactive variables and derived values that the application automatically reacts to whenever they change. Now, let's talk about the elephant in the room. What about React? React famously does not use signals and this is by design and on purpose. React um the philoso-
philosophy in React is to embrace the pull approach. It's the idea of whenever state changes your app re-renders. Um and the reason for that is simple uh and this is a really good talk. I highly recommend watching it. It's from 2014 um when the Meta team actually introduced uh React and they talk a lot about the motivation which was fundamentally the Facebook application was really struggling with
inconsistent states where like your notification uh icon still had the bell thing on even though you already read the uh, read your messages etc. Um this was their solution of like moving away from this push approaches with like early frameworks and having a framework that's deterministic um, with And the way Facebook, uh, React tries to get away with that is by optimizing that re-render. So, again, the
pull approach, we have to re-render the whole thing, but let's make that as efficient as possible. And it's doing it in a few ways. The first one is, obviously, components components in themselves um, help a lot with that. So, if a state in a component changes, you don't have to re-render the whole application, you just need to re-render that component and everything underneath Um, React also introduced
the idea of virtual DOM. So, instead of immediately going in the DOM and updating all the things, it calculates first what actually needs to change to minimize the amount of DOM, uh, manipulations that need to be done, which is the bottleneck, um, performance-wise. And, lastly, there's a lot of memorization going on. So, if anyone here is writing React, you've probably seen all the memos, use memos, use
callbacks flying around in your code base. That is for that reason. It's, again, just trying to minimize the amount of re-computation and re-render that needs to happen and only do that re-computation if the relevant, um, state changes. And React is doubling down on that. So, last year, uh, the new compiler finally hit like the stable release. And if you're using the new compiler, you actually don't need
to use memo, use memo, or use callback anymore because the compiler does that for you underneath the hood. So, again, it's just trying to, out of the box, help you optimize these re-renders and embrace uh, the pull approach as the more deterministic, more stable way of reactive rendering. It's also worth noting, and I'm not going to go too deep into this, um, but it's worth noting that
they It's not like they haven't considered it. So, last year uh, at React Conf, again, another talk that I highly recommend watching if you haven't. Um the React team was talking about the experiments that they've done and benchmarks that they've done comparing signals in combination with React with the compiler, which just led to better results. With other experiments that are still ongoing that they call React Fear,
um which is just something to watch out for. They're still actively working on how can we make this whole thing faster and faster? Um and again, it just proved their point of like the way the framework works, the way the structure works, the pull approach for them is uh their fundamental principle. And all of that is to say like I think it's wrong to think about signals
as a right or wrong question of like, "Oh, you have to use signals, otherwise you're stupid." or vice versa. I think it's important to understand that there's just two fundamental principles um how you can do reactive Um and this is the difference of like React follows that pull approach still uh where we came from. Most other frameworks these days have started adopting the push approach. All right.
So, hopefully that gave a little bit of a view of like what problem are we even solving with this? How did we get to where we are today? Now to the question of like, how do they work underneath the hood? Like how does the magic work? How do signals know when they changed and what they need to recalculate? For that, let's implement a very simplified, and I
repeat, very simplified form of signals ourselves. Let's do a little bit of live coding because I like pain. Makes me feel alive. we have a demo application here. It's just a button and if if I click the button, I want to increase the counter and then I want to show the parity of the current counter. So, fairly simple. In plain JavaScript, what I would probably do is
just read out the current value from the Then increase the value, then calculate the new parity, and then update the DOM with the new values. So, that's fair enough, and that works. If I have a second counter now, let's imagine I have two buttons and a second value. And instead of just the parity, I actually want to show the sum and then the parity of the sum.
So, I can still fairly easily do that. So, read the second value as well now. and then I have calculate the new sum. And then I use that for my parity. And then I have another update that I need to do to the DOM. Still fairly straightforward, and it it allows me to still do the same thing. But now I have a second button, so I need
to have a second button handler, and it's like slightly different, so I can't just use the same function. So, for now I'm just going to copy-paste it and adjust it. So, we have button two on click. And instead of new value one, we have new value two. And this turns into value one plus new So, and hopefully this should do the trick. So, again, there's nothing fundamentally
wrong here. This still But it already starts growing, and let's keep going and say like, "Okay, we want a third button." Uh and this time we want to increase both counters. So, again, it's slightly different, so I'm just going to copy and adjust. Button both two values. Now we actually increase one and two. Use some. And we also need to update the DOM for the first counter
And if everything went right, again, this still works. Again, there's nothing fundamentally wrong with it, but I hope you can already see like this gets out of hand pretty quickly. It just keeps growing. It feels fairly redundant. Um but also, probably more importantly, it feels very messy because we're mixing several concerns within the same parts of the code base. So, let's imagine we had a signal implementation
and a uh computed helper and some form of doing side effects that we could use instead. And what we could do then is have a much cleaner way to define our state. So, for the values, we just use the signals cuz they are really. Uh and then we have the computed values. So, the sum is derived from value one plus value two. And parity Thank you. I'm
going to get I'm just going to steal that from here. So, fundamentally still the same, but we have a much cleaner way of now. Okay, our state is here. The next thing is we still have our actions, but in our actions we can be much more concise. Like, what do we actually want to happen when you click the button? Really, what we care about is we want
to change the state. Right now. That's the the the only thing we really care about. So, instead of all of this, what we do is just value one sets plus one. So, as an action, that's all I really For the next button, same thing for value two. And the last button does both. And then lastly, we still want to update the DOM, right? So, this is where
our side effects come in. So, with the effect helper, basically what we do is we're saying whenever any signal that we use within the effect function changes, we want to rerun the effect. And we're just using that to update the DOM essentially for all of the values that we care about. value one inner text already it's not just less code, which is great, but it's much cleaner
because we have a clear separation between those three concerns. We have our state management, we have our actions to change the state, and we have our definitions of what we want to do to the UI when the state changes. Now, this obviously doesn't do anything because we haven't implemented that yet. So, let's go into signals and actually do that. So, first we need the signal function, which
we saw has an initial value that we pass in. And the signal itself, it's really just keeping track of the And then returning a getter, which allows us to read the value. And a setter, which allows us to update the value. Then we have the computed helper, um which just takes a function that describes how we calculate that computed value. And it only has a getter because
by definition you would never set a Um you're only describing how it's calculated. This This no setting in that sense. and for the effect, for now let's just Again, it takes a function that describes the effect that you want to run. And for now we're just executing that function. Um So again, it still doesn't do anything. Like we we're starting to keep track of the value, but
like the real is still not there where we tell all the to rerun whenever any signals within them change. For that we need two things essentially. We need to keep track of what effect is currently running. And we just set that as we execute the effects. Then reset it afterwards. Um and then whenever we read, we do the get on the signal, we basically want to keep
track of that effect as like a subscriber. So if there's a current effect, just push that effect into that array. >> And then after we've updated the signal, we notify all the subscribers, all the um to rerun. So again, this is a very simplified form of it. But in a nutshell, that's really what signals are. Signals keep track of your state and then broadcast whenever the state
changed to whoever needs to know. And in these 30 lines of code, we basically implemented a overly simplified but functional form of signals. And again, it also helped us clean up the usage of it and the way we're describing our state and using uh the side effects to update our UI massively. hopefully that gives a pretty good idea there is magic going on, but it's not too
much magic. It's actually pretty simple underneath the hood. I keep saying it's over simplified and that's for a few reasons. Uh there are still challenges and this is where we essentially evolve from the early days of Knockout to um to the modern frameworks and the modern implementations of signals. I don't have time to go into all of these, but I want to mention a few of them
just to give you an idea of what I'm talking about when I say challenges. It's basically the first thing is just glitch states. So when we have signals and computed values and all of this broadcasting it's important to recalculate your your your computed values and run your effects in the right order. If you don't do that, if you take this example, so we have a counter and
we have an increase, which is always counter plus one, and then we have another derived value, which is is greater, which just compares increase and counter. By definition, is greater should always be true, right? Because increase is always counter plus one. But if we recalculate in random order and say like counter changes to one now, if we recalculate is greater first, we would compare the old increased
with the new counter value. One is not greater than one, so is greater suddenly is false. And then we would recalculate increased. Um that leads to a glitchy state. So, what all signal implementations do is they just keep track of the dependencies in a top topological tree and make sure that recalculations happen in the correct The second glitch that's very common um is intermittent states. So, if
you update multiple signals at the same time, so like we did in our example, we have a button that increases both counters. If we increase counter one first and counter two after that, that two operations. So, technically what happens is after the first signal changes, it will run all the computers, all the effects on that change. And then it will run after the second change again. So,
instead of when we run increase both, instead of just seeing sum being four, we actually see sum being two first and then sum being four, usually not a big deal uh because it happens fast enough so that your users wouldn't see it. But, if your computation is too slow or if you're actually relying on those values being accurate all the time, you get that intermittent glitch state
uh that you don't want. So, modern implementations just deal with it with batching. If you update multiple signals within the same kind of like with the within the same cycle, uh they batch that together and only trigger the broadcasting once. The last thing worth noting is again with the evolution where we came from, implementations have become more and more efficient. So, instead of just always recalculating, modern
implementations keep track of what they call the dirty state of the signals and its dependencies. So, in this case, when I change counter, counter is marked as dirty because it changed, then is is even checks it has counter as a dependency, so it's a dirty dependency, so it has to recalculate as well. But then is even still calculate like ends up being false, so parity checks is
even has not changed, so parity can skip its recalculation. Um and especially with large states, this helps uh skip a lot of the computation uh that you would otherwise do. All of that is to say we've come a long way, and this is kind of segueing into the main piece of my talk uh that I don't have a lot of time to talk about, the TC39 proposal.
So there's an official proposal uh in stage one to actually bring signals into the JavaScript language itself. Uh and I think this is important for two reasons. For one, it's kind of one minute manifestation of what we call the carcinization of the web, or what people call the carcinization of the web. Carcinization comes from biology where it's a thing where multiple crustaceans completely independently all kind of
evolved into crabs, and therefore we deem crabs being like the the best form of life, apparently. Um on the front end, we see that in in in various different ways as well. One of them is signals. Like we've seen a app application frameworks independently evolved into this signal uh like structure for their state state management, and have come up with their own implementations of these things. So
the idea of the TC39 proposal is for one to just combine all these. Instead of every framework having to re-implement the wheel, um do it slightly differently, doing weird quirks, have one implementation that every framework can use, uh and it's just shipping as like a new primitive in the language. The second idea, or the second motivation coming with that proposal, is once it's part of the actual
core language, once it's part of the spec, suddenly runtimes, browser vendors, etc. can actually start optimizing for that. We've seen that before with just-in-time compilation in Node.js and browsers. This is my hope with this proposal. It's like once it actually gets into the language, we will see massive improvements performance-wise, computationally, as if you memory-usage-wise, because browser vendors can actually specifically optimize for the implementation that is in
the spec. So, definitely check out the There's a GitHub repo with the proposal for the signals. Again, it's still in stage one, so still early stages, um but I think it's it's worth keeping an eye on. Also, if you are interested in that, get involved. They have a Discord channel. Um they they are very active in that discussing where this is going. if I triggered any interest
uh in you on any of this, this is a list of really good resources. I highly recommend to keep going deeper. Um this goes into reactivity as a concept in general, um but also deeper into the actual implementation details, some of the quirks, some of the things that you need to be aware of. and yeah, this is a link to the slides that also has all those
links before uh and some links to my socials if you want to reach out. If you have any questions, I will be running around here for the rest of the day, so definitely feel free to hit me up, but yeah, that's me. Thank you.