1. Introduction to Opt-in Design in Web Development
Hello everyone! I'm Ben Holmes, a core maintainer at Astro.build, and today I'll be talking about opt-in design in web development. Opt-in design is not about UI or systems design, but rather API design and understanding good defaults when building tools and frameworks. Let's dive into a classic example of building an app with create React app and how it evolves with added features.
All right, hello, everyone. Hope you all are enjoying the conference. Sad I'm joining you all virtually for this one, but I have a great talk lined up, so I'm hoping that we can make up for it.
My name is Ben Holmes, and I'm going to be talking about opt-in design, which I am boldly dubbing a new era in web development, new trends that I've noticed. So to clarify what is opt-in design, it's not a UI design talk, although graphic design is clearly my passion. It's also not a systems design talk. We're not going to be talking about Kubernetes clusters or anything like that. It's an API design talk, so learning what good defaults mean when you're building a tool or you're using a framework of choice and both finding and building those tools so that we can build the best apps for our users.
So a little extended intro, I'm a core maintainer at Astro.build, so I've been working in open source for a bit and getting paid to do it, which is very rare to do. I'm also the chief whiteboard officer at Astro as well. Unofficial title, of course, but if you've ever seen some videos of a vertical whiteboard online, I do a lot of educational content, might've been me and you can find an archive right over there. And I'm a champion of content formats. So Markdown, MDX, Markdoc, if you've ever used any of those, I love them and I work on maintaining all of those in the Astro framework. So you might see me around the support forums.
2. Opt-out and Opt-in Design in Web Development
And then you get your performance audit when you've reached a certain state, and you wonder why you have a 39 on your Lighthouse score. And you were following the blog post. You were doing what the community said was a good idea. But that is the consequence of using a SPA. You're assuming all this reactivity, and you might have to walk things backwards and dig through the React dev tools in order to figure out how you can scrape up those Lighthouse metrics again. And that is the pain of an opt-out system.
So talking about opt-out, what does that really mean? Well, first off, looking at the Create React app library, framework, whatever you want to call it, it has a happy path where they manage a big old web config for you. And if you ever need to modify that or bundle your app in a different way, you need to opt-out, which is using the eject seat, literally just hitting a button and managing it all yourself. Which of course, is a very scary thing to do, which is why many people just stick with Create React app and whatever it gives you, and they avoid opting out of any of the opinions.
3. Opt-in Design Principles for Web Development
Then you want to add your cart bubble again. How many items are in the cart? Maybe you want to do this in a client-driven way with client state. You can opt-in to using a client-side framework with Astro. It's a CLI option, Astro add React, that will install the dependencies and bundle React into whatever pages need it. It'll avoid React on pages that don't. Then you can use a lighter weight option, like nanostores, to store that global state. You don't have to deal with as many re-rendering concerns. That will bump up the client-side JS just to the level of React and React-DOM, but it's not adding anything unnecessary on top of it. You chose that path, and you're able to reach for a tool like the Astro CLI to get you there.
4. Principles of Opt-in Design in Web Development
These are three principles that guided how Astro was built: find the lowest common denominator, make complexity easy to add, and stay user-first. By understanding the simplest use case, adding complexity only when intended, and prioritizing the user experience, tools like Astro enable opt-in design. An example is Deno, which brings opt-in permissions, allowing users to control access to their system resources. This approach aligns with the principles of finding the lowest common denominator, adding complexity with simple flags, and prioritizing the user's needs.
These are three principles that guided how Astro was built, and I'm gonna dig into each of these in depth. So the first one, find the lowest common denominator. What's the simplest use case for the tool that you're using? For a Create React app, the simplest one is ship some HTML without any state, that's a hello world. You want to be smart and not have a car with opt-in wheels, for example. You don't want a framework that doesn't help you do anything at all, and you have to add a plugin to render HTML. But as long as you understand what the smallest use case is for your target audience, you can get there.
Also, you want to make complexity easy to add, only when intended. Don't start with a kitchen sink like a Create React app that you have to eject out of. Instead, you want to add really simple switches to opt into things that are more complex, like Astro's CLI, to add React when you want to add interactivity. Lastly, you want to stay user-first as you're building a tool, or you want to choose tools that are user-first. Developer experience does matter, but it needs to be guided by what the user experience is once it ships. So you want the low-complexity baseline for the base of the experience, the LCD, and an is opting into re-renders, which means fewer CPU cycles, better performance. And performance isn't the only user metric you can talk about. You can also measure the security of an application, for example. And I have a little Deno flag to show you there. So whole world of examples that I can reach for here to explain it. Deno, as I mentioned, it brings opt-in to permissions. Whenever you're running a CLI task, it may need to reach out into the world and access things on your system. And it shouldn't be able to do that willy-nilly. You should be able to say, I want to allow network access. I want to let you read from my file system. I want to let you access my environment variables. But without these flags, you're not allowed to do that. It's crazy when you log process dot env and node, and you realize just how much it has access to. It would be nice if we can make that opt-in instead. So how does this address our three principles? Well, first off, this is the lowest common denominator. The simplest thing you can build with DNO is just a CLI tool, maybe a text adventure game, without any IO. And then you can add complexity with simple flags that describe what's added. So no foot guns here. And it is user-first.
5. Opt-in Design in React and Astro
You know, we're caring about security vulnerabilities, opt-in, whenever security needs to be addressed. And the user understands what is being allowed. And also we can talk about server components.
You know, it's a hotness in React. Also in Astro. It brings opt-in to client-side JS, which of course is not a root evil or anything. It's just something that you want to be mindful about and only reach for when you need it.
6. Opt-in Design and Server Rendering with Spinners
This part discusses the concept of opt-in design in web development, specifically focusing on server rendering and the use of spinners. It highlights the default behavior of not streaming and waiting for server processes to complete, but also provides the option to opt in to spinners for a more interactive experience. The importance of considering layout shift and using suspense boundaries is emphasized to prioritize the user experience.
So yeah, this assumes the lowest denominator, which is, again, server rendering everything without spinners, so our heading is not gonna be inside of the spinner, we can just ship that straightaway. And it's additive complexity. Of course, streaming is more complex than not streaming. So the default is don't stream it, just block and wait for all the server stuff to be done. And if you want to opt in to spinners, which can have layout shift, understand the risk, you can add a suspense boundary to say what fallback you want to show while that server's resolving. And of course, that is user first, we care about the layout shift, we want to avoid that as much as we can. But when it's necessary, when we can't control it, let's reach for suspense. Let's try to give you some feedback straight away, it just feels a bit snappier.
7. Comparison of React and Solid Frameworks
So I obviously can't do this in person. So I can't see your hands. But I'm assuming this has happened to you at some point in your react journey, where you call on use effects, you forget your dependency array. And things happen. You don't need a performance audit to know that there's probably something wrong going on here. And that's because of reacts reactivity model.
So let me show you an example of how we can bring opt in to the component level, which you may not have seen before I have two components here, one built in, react, and one built in solid, you'll notice that at first glance. They look almost the same, but there is one subtle difference that I've highlighted right here. So we're building a shopping cart, right? We have our quantity, which is how many things are in the cart. We have our price, which is based on how many things are there. And then we have some unrelated server side fetches to resolve. Like getting the product images and getting the shipping text. So a few things going on that all display info about the product. And we can write this with JSX as you would expect. But the one interesting part is this dependency that we're drawing here. Price depends on quantity, so it needs to recompute anytime quantity changes. And if we pull up how these are written, React and Solid, I have working demos right here. You can see when you click on the React example it's going to update the price every time quantity updates. But it's also going to rerun product images and shipping text because you need to care about useEffect and useMemo and all these other things to prevent other stuff from recalculating behind the scenes which you might need to reach for developer tools to even figure out is going on. I made it really obvious here, but it might not be so obvious in your app. So React assumed you want everything to re-render. That's the simplest mental model, so they went with this approach here. Which it can be a little scary. It's a little foot gunny. Now Solid takes the opposite approach.
8. Opt-in Design and React Server Components
Instead of re-running the whole component any time state changes, it's just going to re-run the part of the page that cares about that state. So in Solid, for example, if you want to declare dependencies, like price depends on quantity, you can just create an execution function. And Svelte does the same thing. They all kind of work like this, and we're converging on a model of signals in order to manage this. Lastly, I'll leave you with a little demo on a repo called simple-rsc, which shows you how React Server Components work outside of Next.js.
Instead of re-running the whole component any time state changes, it's just going to re-run the part of the page that cares about that state. It's just going to re-render the quantity every time quantity updates. And it won't update product images and shipping text, yay, but it won't even update price. You have to be very specific about what parts of the page re-render.
So in Solid, for example, if you want to declare dependencies, like price depends on quantity, you can just create an execution function. And, the Solid Compiler is smart enough to say, alright, this is a function now, I'm going to trace the dependencies inside of this function, quantity, and I'm going to re-run that function any time one of those dependencies changes. So not even a dependency array, so it's just smart. Now that we've made this function, we can see that Solid will update price. But unlike the React problem, we're not going to reload anything else that doesn't depend on quantity, because again, you opt into reactivity with functions. So that's how the whole framework works, and it's a really nice primitive. And, we'll see this in modern examples like Svelte, and Vue, and Angular, they all are based on this model of where the initial state is going to be run once, and then we only rerun wherever state is used. So we rerun the JSX element right here instead of rerunning your function top to bottom as you would in React. Cool little trick, cool little mental model.
And Svelte does the same thing. Using the dollar sign operator, and this is changing in future versions, apparently to a state operator, that will rerun price whenever quantity changes. Same sort of flag to say, hey compiler, look at the dependencies, re-render this stuff whenever those dependencies change. They all kind of work like this, and we're converging on a model of signals in order to manage this.
So, lastly I'll leave you with a little demo, looks like I have about two minutes, but I'll you know, speed run this, see what we can get to. It's on a repo called simple-rsc. So if after the talk, you want to try out this tool, even contribute improvements to it, I definitely welcome it. But it's a basic benchmark that shows you how React Server Components work outside of Next.js. So you can understand the mental model of Server Components, what they really do. So, I have that code pulled up right here, we're going to zoom in for you all just a little bit. And we can see we have our server root, this is just a page that's rendering, don't worry too much about that. And we're just rendering the static text, abramix. I built this with Dan Abramov on Stream. It's a Spotify clone, so Abramov, Abramix. I thought it was funny, okay? I thought it was funny. But yeah. It's just rendering this static text right here.
9. Server Rendering and Streaming in React
In React, we can stream HTML as soon as it's available, without shipping a component to the client. Server rendering and server-side fetching can be used to display complex data. By streaming only necessary information, we can avoid sending unnecessary data. Adding suspense boundaries can improve the user experience by showing content while waiting for the rest to load. React's future looks promising with its server-driven approach. Check out the demo and join the Astro Discord for more support.
And if we pop over to our browser, which I'll go ahead and do right here, we can see our static text rendering to the page. And also a dev panel that shows you what information is being sent over to Wire to show this. So in React, these are very truncated server messages that tell React, construct an H1 with the child abramix. And we're not shipping a component to the client to do this, we're literally just streaming in HTML as soon as it's available.
So you can see it is blocking. We're now waiting a solid second for the database to load and for everything to be available because that's the default in React. Just block on everything so that there's no layout shift and we can show you that information. And it'll stream in just the info about the albums that we need on the page. This is kind of why it's like a modern GraphQL if you really think about it, where you don't have to be smart about what data you're codifying as JSON. It's just going to send down HTML. So as long as you're smart about fetching from a database, rendering what you need, you don't have to worry about sending stuff you don't. It's going to stream exactly what's necessary for the page. And if we decide, you know what, that one second wait is a bit long, I don't really want to wait for that, we can add a suspense boundary in here to unblock our heading. To show the heading straight away, and then show a little placeholder while the albums are loading into view. So we can see that, and I'll refresh a few times to show you what's going on. We can see Abramix streams from the server straight away. Then we show our loading spinner while the database loads, and then the server streams in the rest of the information a bit later. And React is smart enough to resolve that onto the page based on how you structure your component tree. Cool, little trick. Definitely excited about the future of React, and just making everything driven by the server, but keeping it in JSX. It's something Astro's been doing for a while, and I think React is codifying it in a really nice way. All right, so that's what I wanted to leave you with. Check out the demo if you want. And if you want to, you know, show your support to Astro and other opt-in frameworks, you can join the Astro Discord, astro.build slash chat, and you can find me everywhere at bhomestev if you enjoy my teaching style for some reason. All right.