Instant websites using Fresh and Deno on the Edge

Bookmark
Slides

Any interaction faster than 100ms is imperceptible to users - what if all website interactions, including loading were 100ms or less? Let's explore strategies and how Fresh & Deno can help.

by



Transcription


Hey everyone, welcome to my talk about writing instant websites for Fresh and deno. My name is Luca. I am a software engineer at the deno company. I work on the Fresh project, the deno project, and deno Deploy, which is our serverless edge compute runtime. We'll talk more about that in a second. And in addition to that, I'm also a delegate at tc39 working on standards/talks">web standards. tc39 specifically is the standards committee that standardizes javascript, but I also work with folks at Whatwig and W3C to standardize things like the Fetch spec, web crypto, and other related specifications. At the W3C, I'm also a co-chair of the Winter CG community group, which is a community group which focuses on standardizing behavior across javascript server-side runtimes. So things like node.js, deno, or Cloudflow workers, you want to standardize the behavior between those so you can portably write code and have it work across a bunch of different platforms. So that's who I am. Let's get to the actual meat and potatoes of the talk. Before we can do that, we need to figure out what do I actually mean with my title of the talk, Instant Websites with Fresh and deno. So what are instant websites? How do I make a website feel instant? The core idea here is that we want to minimize the time it takes between a user interacting and the user being unblocked by that interaction. So what does that mean? If a user does some interaction, they expect something to happen. For example, when they navigate to your page, they expect some certain content to load because they're interested in that content. They want to read that content, review that content. So if we want to make a page feel instant, what we want to do is we want to minimize the time between the user interacting and them being unblocked. In fact, I can give you an example of this. So the user wants to visit a recipe page which shows the recipe for a certain dish. They look up this recipe on Google or they look up this dish on Google. They click on a link. That's the interaction. How long does it take for them to actually be able to look at the recipe, understand it and, well, maybe not even understand it, but look at the recipe and start to understand what's going on. So the point at which the user is unblocked is the point at which they can see the recipe and they can start to act on that. So what's the time here that we need to minimize? It's the time between them clicking the link and the primary content that they care to see about or that they care to see the recipe being loaded. So how do we actually achieve that? Well, to do that, we need to figure out how fast instant actually needs to be. How fast do these interactions need to happen for the user to think that they're instant? Well, really we want to make them happen as fast as possible because A, there's no reason to make them slower. The user is not going to think, hey, this page is too fast. That doesn't make any sense. The faster, the better, always. But there are practical limits to at what point the user thinks or does not realize the difference between too slow and or between fast and even faster. For example, you can, as a rule of thumb, interactions faster than 100 milliseconds are generally imperceptible. So that means if you have an interaction which is 60 milliseconds versus 100 milliseconds, they're going to feel the same to the user. They're going to feel really fast. So we're going to aim for interactions which are around that time, maximum about 100 milliseconds. That's really difficult, though. So there's a little bit of leeway we have. For example, users often are much more lenient with their understanding of instant or their perception of something if the visual change that they see is big. This comes from reality. If a user looks at something in real life and there's a big project going on, like a big thing is happening, there's a house being built from ground where there was previously nothing, that's a big change. Users expect this to take more time. They translate this into software as well. If there's big visual changes, there's usually more leeway for a user to still perceive something as being instant. You don't want to take this, you obviously want to always take this with a grain of salt and you still want to try to be as fast as possible, but the larger your visual change, the more leeway you have. What's also really important is that you give feedback to the user quickly that something is happening at all. If there is an interaction which is going to take longer and which you cannot make as fast as 100 milliseconds, tell the user about it. Show the user that something is happening. Give them something to look at while it's happening. They click on a button and you know that button is not going to complete within the next second or within the next 100 milliseconds. Show them. Put a loading indicator on it. Put a spinner on it. Put a timer on it. Something to indicate to the user that yes, progress is being made. Here's approximately how long it's going to take. This is how far we are. Give them something to look at. It's going to make your page feel much faster than if you just have a button which you click and then nothing happens and then, I don't know, still nothing happens, still nothing happens and at some point something pops in. It's not a great user experience. But how do we actually do this? This seems like a very difficult problem to make. For example, let's focus specifically on page loads for now. How do you make your page load happen so quickly that the user does not notice that they're happening? Well, we really have to take a lot of variables into consideration like how fast is the user's device? How fast is the network? What's the round trip time to the server? But there's some general rules that we can follow which will apply to whatever network, whatever device the user is on, which will always make the site feel faster, even if they're on a really high-end device or really low-end device. The main idea that we always want to have in the back of our mind is we want to prioritize the things that the user actually cares about. We want to prioritize the things that the user is actually waiting for. We want to prioritize the things that are blocking the user because the primary goal to making a page feel instant is to minimize the time between interaction and unblocking the user. So if we prioritize unblocking the user over other auxiliary functions of a page, we may be able to increase performance. Let me demonstrate what I mean by that. So first thing with figuring out how to make stuff faster in this case is to figure out what we even want to make faster. So figure out what is actually blocking the user. For most pages, this is relatively doable because you can split up most pages into different types of content, primary content, secondary, or even tertiary content. What are these different types of content? So primary content is content that the user is actually there for, that the user came to your page for to see. Things like on a news page, the article itself, on a cooking site, the recipe, or on an e-commerce store, the product listing. These are the things that the user needs to see to be able to be unblocked, right? The user does not come to an article page or a news page to view related articles, to view ads, to view navigation banner. No. They came here to view a sponsor post or something like that. No. They came there to read that actual article. So the important thing that's going to unblock the user is loading the things that the user actually cares about, the primary content. You can still load secondary and tertiary content, but don't make the loading of that content slow down the primary content. There's multiple ways you can do this. Some real world examples here from real websites. First one is the deno homepage itself. You can split this into two primary contents. This is the landing page for the deno project. The primary content, which is the Euro section with a title, a subtitle, an installation button, the current version. And then you have a bunch of talking points underneath, which explain what deno is and why you'd want to use it. And then there's secondary content, like the navigation bar, for example, contains a search button. The search button is still useful, but it's not the reason why most people go to the deno homepage. Go to the deno homepage to learn more about deno, not to immediately go search for something. So the search is a secondary content. The rest of the page, the things that the user actually wants to see, it's primary content. So what we can do is we can make sure the primary content loads first, and only after the primary content is loaded do we start loading the secondary content, like the search button. And you can actually see this in action. I have a little gif here showing the loading of the deno homepage. And if you look at by that red arrow there, you can see that the search button actually flashes in slightly after the entire page is loaded. Put some real numbers behind this. The actual page loads at 1.3 seconds, but the search button only loads at 1.6 seconds. There's a 0.3 seconds there, which the page is loaded. The search button is not yet. This is obviously greatly exaggerated in slowness here, because what I'm showing you is actually DSL connection at 1.5 megabits a second, and most desktop users in North America are not on connections that slow anymore. But this really illustrates the problem that you're trying to solve, because a lot of people actually still are on connections like this, not on their desktop, but on their mobile devices. It's very common for mobile devices to have a very high latency, even if the actual maximum throughput is relatively high. And a lot of times, latency can still be quite great. So what you want to do is you want to minimize, you want to optimize for cases, for the worst case, and that's automatically going to make the best case better as well. So through this optimization here, where we load the primary content first and then load the secondary content, the search button later, we actually save about 0.3 seconds on this DSL connection. That's a 15 to 20% improvement in page loading times, just by lazily loading this search button after the primary content. This unlocks the user faster. I have a different example of this, where it's less obvious what's happening, which can be good. You don't want to make it super obvious that your page is loading slowly in pieces. If you can hide that, that's cool. So this is the fresh home page for the fresh project. And this also has primary and secondary content. But if you look closely, there's nothing that flashes in on this page after the initial load. The initial load is design-wise complete. It loads all the components of the page. And if you didn't know any better, you wouldn't actually notice what the secondary content here is. So let me tell you, the secondary content here is actually an animation. It's not a specific component on the page. It's an animation, which is a nice thing to have, but it's not critical for the user to view this page. So it's not something we block the page rendering on, because it's not something that actually blocks the user. When the user goes to this page, they want to view information about the fresh project. The animations are nice to have, but it's not something that's actually blocking them. So if you actually want to see the animation here, it's right up here in the hero banner for the page. The fresh logo drops down this little drip that splashes on the rest of the page. It's a really nice animation, but it's not critical for the page to load. So we loaded lazily after the initial page has loaded. Again, this is very slow, because this is on a DSL connection. Just a little streaming point. Next thing you want to do is you probably want to be server-side rendering. I'm at a react conference here, and you all are probably very familiar with react, and react is very client-rendering heavy. But nonetheless, you want to be server-side rendering. Because server-side rendering can be and often is much faster than client-side rendering. Let me explain why. One of the biggest cost factors for your application loading speed is network round trips. The amount of times, every time you make a network request, there is a number, a certain fixed time that this just has to take due to the network switching equipment between the client and the server. So there's a maximum speed at which data can travel between your client and the server. This is related to your ISP, to your device, to your connection type, to a bunch of different things. But it's something that you don't have influence over. It's something which is fixed. You cannot change this very much. So what you want to do is you want to minimize your reliance on this fixed time that every request takes. One way to do this is to make sure that you don't have the scenario where you load one request, then that request, once that's downloading, you start two more requests or three more requests or whatever. Once those are downloading, they start even more requests. And only after all those requests are done do you actually load the page. What ideally you want to do is you want to do all the requests at once in parallel, because then the latency doesn't matter as much. And the thing that you're primarily waiting on is not the network round trips anymore, because you only have one of those if you're doing everything in parallel. The only thing that you care about now is how big the asset is. The problem with client-side rendering is that very often you're in a scenario where you have at least three network round trips before you render anything. That is because when you do client-side rendering, you often ship an empty html to the client in the initial render, which then has a few sub-resources, usually some css, and with client-side rendering, always some javascript. You have to download that javascript. That's one more network round trip. You have to then execute that javascript. And usually that executed javascript does another api call or some other call to fetch data from the server. That's another network round trip. And only after that third round trip has finished can you actually render the page. Whereas if you do server-side rendering, you can get away with a lot less sub-resources, sub-requests. Server-side rendering, you can do data fetching in the initial request for the html, which means you don't need to have this other network round trip from the client to an api, for example. You can make that request from the server, which is very beneficial because servers usually have much better latency to other servers than clients have to servers, because servers are connected on high-performance networks. They appear directly to other data centers. Maybe your database is even running in the same data center that your server-side rendering from. So you have much reduced latency here, which means that once the html is done downloading, you may only need a single other sub-request, which is, for example, something like fetching css. You've cut out an entire network round trip here, which if you have a 50-millisecond network round trip, which is actually pretty fast, then that means you've now cut your client-side or your rendering times by at least 50 milliseconds. The entire thing takes 150, cut it down by 50, that's 30% improvement. That's very good. And additionally, obviously, client-side rendering has the downside of using a lot of CPU cycles because a lot of, on your client's device, which especially in mobile devices, which are battery-powered, can result in additional battery drain. It's not something we want. If we do server-side rendering, you have to do less work on the client. It's good for the user's battery. They're going to be much happier. And you can actually take this even a step further. So this is a... diving a little bit deeper on these sub-requests that are required for rendering. This is a timeline, a network timeline, of the fresh homepage that I showed you earlier. And what you can actually see here is that the fresh homepage can render and can show meaningful content to the user after the first request is done. It requires no sub-requests to be able to show content to the user. And you might ask, how does this work? Don't you always need some css, for example? Well, you can do things like inlining your css into the html to not require an additional network roundtrip for your initial render. This can be very, very beneficial, even on very fast connections. For example, in this case, the html is done downloading at 0.45 seconds. But the next... which actually results in the yellow line, yellow-green line here at 0.52 seconds. That's when the page actually renders for the first time. Results in rendering within 55 milliseconds here, which is very fast. If you would have had to wait until an additional network sub-request with another network roundtrip would complete, the first one completes at 0.65 seconds. So we would have had to slow... the page rendering would have slowed down by at least 10 to 15 milliseconds, which in this example here is 30% slowdown. Very significant. So you really want to be minimizing your sub-requests. If you can at all, try to inline your css into your html to prevent an additional network roundtrip for downloading the css. It's going to be very beneficial. And finally, we sometimes need client-side rendering to do certain interactive things. If we do need to do client-side rendering and we need to ship some javascript to the client to do this, we want to make sure that this javascript only client-side renders a piece of the page. Only the piece of the page that actually requires being interactive. A lot of existing frameworks like next.js and remix, what they will do is they will client-side render not just the components that are actually interactive, but they will client-side render the entire page. So what they will do is they will server-side render the page on the server for the first time, and then they will bundle up the entire rendering code that was required to server-side render, ship all of that to the client, and do all of that server-side rendering on the client again. This is very painful because a lot of the content that you'll be rendering on the client really hasn't changed since the server-side render. As an example, you can think of a blog, for example, with an article. The article is written in Markdown. To be able to render that, you need to parse the Markdown. You need to turn that into html. You can do all that in the server, but if you also need to do that in this client, you need to ship this entire Markdown parser, the Markdown itself, the Markdown to html converter, you need to ship all of that to the client. This can be a significant weight on your application, and it can slow down the loading significantly for no real benefit because the Markdown hasn't changed since the server-side rendering. There's no benefit of doing that again on the client. So what you want to do is you want to make sure you client-side render only the components that are actually interactive on the client. You want to render very selectively. A lot of your page is static. You do not need to re-render this static content on the client. You only need to re-render things that are actually interactive. So make sure to only ship the javascript to the client for components that are actually interactive. I'm going to give you a quick example of this. This is the Merge Store, deno Merge Store. You can find it at merge.deno.com. And this is the landing page. The landing page has a bunch of links to different products that are available on the Merge Store. And those are just regular A tags. They don't need any javascript to function. When you click on them, it'll navigate to a different page. But there is actually one bit of javascript that is required on this page, which is to power this cart button at the top. This cart button, when you click it, it actually opens a dialog from the side, which you can use to look at your shopping cart, remove items from it, press the checkout. Stuff like that. This requires some javascript to function. In a lot of traditional frameworks, that would mean that now we need to ship the rendering for the entire page, including the product previews, the header, the little icon in the top left, the footer, all of that to the client and re-render it on the client. But what we do instead is we only ship javascript to the client for the things that are actually interactive. So we only ship the javascript to the client that is required to render that little button and to be able to perform the right actions when you click on that button, the event listener that gets invoked when you click on that button. I can give you some other examples from this store. So this is the product preview page where you can view information about a given product. This offers a cart again, which requires some javascript. But there's other pieces of this page which also require javascript. For example, the image viewer has a left and a right button that allows you to view different images of the product. This requires some javascript to be powered. And it also has a size selector and an add to cart button, which also requires some javascript. All of these things are shipped to the client individually. And the javascript that is shipped to the client is constrained to only the elements that actually need to be interactive. You could, for example, not do any of this and ship the renderer for the entire page to the client. But again, this is slow. You would now need something to be able to render the product description, which is probably written in markdown to your client and render that. Not something you want. You only want to ship the components and the javascript to the client for the components that are actually interactive. This whole idea of shipping only the javascript that you need for making some components interactive is called island architecture. The idea behind it is that you render your pages on the server and then you hydrate specific parts of that markdown with client-side javascript. And you don't touch static content of the page. So page content that is completely static stays completely static. It's never touched by client-side javascript. You only hydrate the components that actually need interactivity. If you want to learn more about island architecture, after this talk, you can check out the blog post island architecture by Jason Miller. Jason Miller is the person that originally invented Preact. It's a great blog post. It has some great diagrams. I really urge you all to read it. So that was the first part of the talk title, instant websites. But it was the talk title in its entirety was instant websites with Fresh Endino. So let's get to Fresh. So Fresh is a new web framework we built for Endino, which is built with the idea in mind that everything should be really fast and that you should be able to build instant websites really easily. So it takes a lot of those ideas that I previously talked about in this talk and builds them right into the framework, makes them really easy for you to use. For example, Fresh renders all of your pages just in time on the server. There's no client-side rendering by default, which also means there's no javascript to the client by default. Instead, if you want to do some client-side interactivity, you can have certain components, the islands, which are shipped to the client, but you have to be very selective about this. You don't send the entire page render to the client, for example. You only ever send specific components to the client to be rendered there. And Fresh is also very, we try to make it very easy for you to do the right thing. For example, I told you inlining a css can be a great performance win. We inline your css automatically if you use our Tailwind plugin, for example. So we try to make it really easy to build fast websites and make it really easy to stay on the fast path. So how is it actually building sites with Fresh? Well, if you've used next.js or remix before, it's going to feel very familiar because pages and islands, they're written in JSX. They're going to look a lot like the react components that you've built for your Next or remix sites. The difference is that they're actually not rendered by react. They're rendered by Preact. Preact is an alternative framework to react. It's much smaller, much faster, and much more customizable, really, than react. It's a really great alternative. You should all check it out, preactjs.org. But there's also other ways where it feels a lot familiar to next.js. For example, routing is done through file system routing, essentially identical to next.js, which is a very... A lot of people are familiar with it, and it's a good way to do routing nowadays. And Fresh uses deno, right? It's a deno framework, which means that if you've built anything for the browser in the last 10 years, it's going to feel very familiar. If you want to do an HTTP request, use the Fetch api. Requests and responses are the same requests and responses that you have in browsers through the Fetch api or Service Workers. It's going to feel very familiar. You have web crypto at your disposal if you need to do things like hashing or encryption or decryption. And some other really cool features of Fresh, which it sort of inherits from deno, are that there's no build step, which means you have really, really fast iteration times. You don't need any configuration. It all just works out of the box, no configuration necessary. And typescript also just works completely out of the box with no configuration necessary, just like it does in deno. Let's look at some code real quick, just to illustrate my point here. This is a very simple, basic route, just like you would find it in probably most projects. To create a route, you put a TSX or JSX file into the routes folder in your project. In this case, I have a route called routes slash about.tsx, which, due to the file system routing, would be available at slash about on my web server. This is just a regular JSX component. It returns some html markup, which is a service that rendered for each request. And that for each request is important because if you have dynamic routes which contain parameters, for example, this route, greet slash name, you want to be able to change the output of your route depending on the input parameters. In this case, you want to greet the specific user that requested that route. I think one of the most cool features of Fresh is how it deals with interactive islands and how easy it makes it to take a static component and turn it into an interactive island. To make a static component into an interactive island, the only thing you need to do in Fresh is put it into the islands folder. There's a folder called islands, put a JSX component in there, you import that from your route, use it in your route, and Fresh will do the rest. It'll make sure that it is set to the client and hydrated there. It'll also make sure that anything which is static, for example, this route has a static title and paragraph, are not sent to the client as javascript. They're only sent to the client as static markup. data fetching is also something that's very important nowadays, and if you're doing server-side rendering. This is actually inspired by remix a lot. So if you've used remix before, you've used the data handlers and the fetch functions there. It's very familiar. You have a way to have a function that runs for each request, which you can do data fetching in and then pass some data to the page component or the route component, rather, where that data is then rendered into some html. You can even pass this data into the property of an island and it'll be serialized and hydrated all on the client automatically. So this was really fast. I'm sorry, we don't have a super large amount of time. If you want to get started yourself on Fresh, download the deno CLI. You can do that from deno.and. Then you want to run this bootstrap command here, deno run dash a dash r HTTPS fresh dot deno dot dev, which also happens to be the fresh home page. So if you want to learn more about fresh, you can go there, specify a directory to generate a project and enter that directory and run deno task start. And then you can view your new website at localhost 8000. So that's fresh. I want to point out one other really cool thing here, which is we talked a lot about server-side rendering and network round trips today. And one way to drastically improve your page performance is to just reduce the network round trip time, which sounds really difficult. And I said it's nearly impossible earlier, but there's actually one way you can have a lot of effect on, or there's actually one way that you can affect this a lot, which is to just move your server closer to the user. If you have a server in the US East one, if you have a user in Tokyo, they need to do a network round trip from Tokyo to US East one. That's like 500 milliseconds. That is very slow. So you want to avoid this. You want to have a second server in Tokyo. Or what if you don't have just have a second server, but if you have 30 servers all across the world, really close to all of your users. This is something which you can do with DenoDeploy. DenoDeploys are a hosted deno offering. You can give us your deno code, we'll run it in 34 different regions across the world. It has a integrated GitHub integration where you can push something to your GitHub repository and then we'll instantly deploy to all of these 34 regions within two to five seconds. And we're within 100 milliseconds of essentially all internet users in the developed world. You can check this out at deno.com. And if you want to look at some real world products that are built with DenoDeploy, Netlify Edge Functions is powered by DenoDeploy. So if you've ever used Netlify Edge Functions, you've actually already used DenoDeploy. Cool. So that's my talk. Here's some final resources that I want to send you off with. If you want to learn more about Fresh, go to fresh.deno.dev. You can go to deno.land, you can find the deno manual, installation instructions, api references, guides, all of that on that page. If you have any questions about deno or Fresh, you can ask them on Discord, discord.gg.geno. And if you want to talk to me personally, my email is on my website at lcast.dev, or you can tweet at me or DM me on Twitter at lcast.dev on Twitter. And then finally, we're currently hiring. So if any of this seemed interesting to you and you're interested in working with us on deno or on Fresh or on DenoDeploy, we're currently hiring a bunch of different positions, including design, infrastructure engineering, operations, and performance engineering. So if any of that seems interesting, please apply. You can find all the job listings and all the details at deno.com. And if you have any questions, again, feel free to reach out to me on Twitter. That's my talk. Thanks, everyone, for watching. And I'll see you all next time. Bye bye.
33 min
24 Oct, 2022

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Workshops on related topic