 Hello, everybody. My name's Hussain. My name's Jason. We work on the Chrome team to improve speed and performance on the web. In our talk today, we'll cover rendering on the web and the performance implications of different application architectures. When we use the term application architecture, we tend to mean a lot of different things involved with building a website. This can include building out your component model, managing data on state, handling routes and transitions, and even rendering and loading your assets. For this talk specifically, we'll be focusing on different approaches behind rendering and loading, how they all work, as well as how they can impact your site's performance. Rendering or shipping HTML, CSS, JavaScript, or any other resource from a server to a browser will always have some sort of cost. This can be runtime or build time and figuring out the best approach to rendering your assets can and will provide a better experience to your users. Let's take this example of a website and see what we mean by rendering cost. This app ships a megabyte of JavaScript code and its entirely client-side render, meaning that it only ships a shell of HTML content at the first request. And then it uses a client library or framework that blocks rendering to entirely create a DOM at an attached route. The issue with this approach is that until the entire JavaScript bundle finishes executing, the user doesn't see any real content whatsoever. This is especially an issue if they're not relying on the best network or device at that given moment. Although that's a pretty contrived example of how a user might experience a site that ships a megabyte of render-blocking JavaScript on a slow 3G connection, its client-side rendering really a problem in the general sense. To try and answer that question, let's try again to visualize the process of loading for client-rendered sites to get a better idea. When the user navigates to a web page, that initial request to get the HTML document is fired. Once it completes, the user only sees a partial content provided by the HTML shell. This could be nothing but a blank page or it could be a loading indicator of some sort. Now, this essentially marks the first paint of your application. Only after the main JavaScript bundle is fetched and rendered does the user see real, meaningful content. And some time after that would mark time to interactive, or the time it takes for the user to interact with your site. The issue with relying on a main JavaScript bundle to populate content to users is that the larger the bundle, the longer the user will have to wait before they can see anything meaningful or even begin using the page. And that brings us to the biggest problem we have with heavy client-side rendered experiences. Bigger JavaScript bundles will always mean a slower performance. If we use HTTP Archive to try and convey why this is a problem, the amount of JavaScript code we're shipping to our users is growing year after year after year. Looking at these stats, we can see that in just a few years, the median amount of JavaScript from mobile web pages has grown considerably. The bigger problem is that user devices and conditions are generally not keeping up with this trend. Yes, newer, more powerful devices are being released every year. But the more dominant trend for devices around the world is that they're getting cheaper, not stronger. Right. So part of the reason why we see these larger application sizes is because there's been a shift towards client-side rendering. We began to treat JavaScript heavy single-page applications as being fundamentally different than a traditional website. More closely related, perhaps, to native apps in the way they're delivered and the features that they provide. This differentiation cuts a very firm line between server rendered and client rendered web apps. But in recent years, that's become the change again. So if large JavaScript bundle sizes are a problem, do we just go back to server-side rendering? Server-side rendering is generally going to give us a faster first contentful paint since the content that we need to display on the screen is available as soon as the HTML is parsed. On the left, you can see we have a server rendered list of tomatoes. And on the right, that same list rendered on the client. And notice how on the right, our images didn't start loading until the list had already been rendered. So we want the performance of server-side rendering where bundle sizes aren't a huge problem during application development. But we also want to deliver rich and interactive applications in a way that is only possible using client-side rendering. We need both. And many modern web applications are built using both server-side and client-side rendering. We call these applications universal or isomorphic. And that just refers to their ability to span multiple different rendering environments and modalities. Developers have been gravitating back towards server-side rendering. They're just doing it in different forms. We've identified three high-level reasons why server-side rendering is important that we're going to cover today. The first is performance. The second is search engine optimization. And the third is data fetching. Let's dig into performance. In a traditional purely server rendered architecture, incoming requests are handled entirely on the server. The requested content is rendered and returned as an HTML document. And that document includes everything necessary to show the content. This means that the browser can start rendering it immediately as soon as it's being streamed in. This is where hydration kicks in. In a hydration architecture, our HTML page contains a script that loads a full-fledged client-side application. This means we have to wait for it to download, then evaluate that code, let it boot up our whole app on the client all before the page can get interactive. So let's take a look at a visual representation of this so that we can get a feel for the effect on time to interactive. Just like in a server rendered site without hydration, once we get our markup back from the server, we can render the page. This is essentially an inert picture of the page that we asked for, though. Links might work, but dynamic functionality is only available once the JavaScript bundle has been downloaded and executed. Going back to our Tomatoes app, when we compare server-side rendering with hydration to client-side rendering, the content is shown sooner when server rendered, but the difference here is now a little bit less dramatic. Both architectures allow time to interactive to be governed by an application's code size. So how exactly do we render on the server and then hydrate that on the client to pick up where we left off? So there are a number of frameworks that are providing this functionality for us today. In a typical client-side rendered application with React, the render method is responsible for rendering the top most React element into the supplied DOM node. To enable rendering the server, however, you need to use the React DOM server module on a node server like Express and call the render to string method to render the top-level component as a static HTML string. Now, back on the client, we can use the hydrate method instead of render to use this already-generated HTML markup on the server instead of reconstructing it again, brand new on the client. Vue works in a very similar manner, where you can use the Vue server render library to render a Vue instance into HTML using render to string. With Angular, Universal makes it possible to turn client requests into fully server rendered HTML pages. The ngExpress engine method can be used to bridge Universal's renderer to the client-side Angular app, providing that same type of server rendered experience at boot up. But there are tools present that make this even easier, where you can get the server rendered and client-hydrated architecture right out of the box. These are sometimes referred to as meta frameworks. And Next.js and Nox.js are two examples that allow for a complete server rendered experience for React and V, respectively, with very minimal setup. The one important thing to note about typical server-side rendered architectures that get hydrated on the client is that although the time to first contentful paint can be reduced, it doesn't change anything about how fast the users can begin to interact with your application. This uncanny value experience can mean your users can think that your site is ready to be interacted with before it actually is. So to summarize the user experience of server rendering and hydrating to an existing HTML markup, yes, we can get a much faster first paint. But we have the issue of a longer time to first bite since we have to account for additional and unavoidable server think time. Our time to interact might remain the same or even get slightly worse, but this leads to a bigger problem. The first input delay or the time it takes for the page to react after the user has begun attempting to click on something. Now, if this gets significantly worse, we can get that rage click experience where the user clicks on things and doesn't see anything happen. Right. So server-side rendering gives us the best of both worlds. But it's important to remember that it can also give us the worst of both. Thanks. It's a great slide. He worked on it for quite a while. Thankfully, the architecture choices we make when building applications exist on a spectrum. And that means that we have options. At one end of the spectrum, we have server-side rendering. And at the other end, client-side rendering. We can think of the left side like thin client applications and the right side like thick client. Hydration lets us produce the same highly interactive experiences as purely client-side rendering while preserving some of the performance benefits of server-side rendering. Similarly, we can use a technique called pre-rendering to bring some of the benefits of server-side rendering to client-rendered applications. Pre-rendering is similar to server-side rendering. But here, we render the application to static HTML at build time. This gives us the improved first contentful paint advantage of server-side rendering, but without the maintenance and infrastructure overhead typically associated with those setups. To try and visualize what we mean by not having the same overhead as we would with a normal server-rendered approach, we could take a look at the differences between how the HTML is fetched. For a server-rendered site, once the initial request is made, the page needs to be generated on the server at runtime. And only when it gets completed, it gets shipped back to the browser. But with pre-rendering or static rendering, that HTML page has already been generated at build time. So once that initial request is made, it can be sent back almost immediately. One tool that takes advantage of pre-rendering is Gatsby. And it's an open-source static site generator that uses React. Instead of using React DOM's render-to-string method, it uses render-to-static markup during builds to render React elements to HTML without DOM attributes that aren't needed for simple static pages. They couple this with pre-loading the main JavaScript chunk to try and download it as soon as possible while prefetching any future routes. Although pre-rendering has helped sites and tools like Gatsby provide better static experiences, it's not always an ideal solution. It only works for static content and cannot really be used to generate pages on a server if content is expected to change. With pre-rendering, we also need to have a list of all the URLs ahead of time in order to create their pages. An example of a site that actually has to overcome some of these issues is the Google I.O. event site. It pre-renders HTML for most pages using pre-act CLI. But because there are a number of scheduled pages with content that changes frequently, these are specifically server rendered at runtime. Moreover, there are thousands of URLs on this site alone. And it easily passed the threshold where rendering many of these pages on the fly became more effective than pre-rendering all of them ahead of time. And lastly, it can sometimes be a Band-Aid solution, where instead of fixing the critical problems that can slow down a site, it just allows you to quickly ship some HTML, even if the rest of your experience remains slow. So we've talked about how we can think of these different rendering approaches as a spectrum, but the whole purpose of framing things in this perspective is because we want to minimize the time it takes for our site to become interactive, but to also deliver initial content as fast as possible. The sweet spot lives right in the middle, and there have been newer approaches coming out that can help us get there, one of them being streaming server-side rendering. The idea behind streaming server-side rendering is that we can render multiple requests at once and send down content in chunks as they get generated. This means we don't have to wait for the full string of HTML before sending content to the browser. And doing this can improve our time to first byte. Frameworks like React already provide an API to make streaming possible. Instead of using the render the string method, you can switch to using render to node stream, which lets you pipe the response and send the HTML down in chunks. We can do the same with view server renderer, where switching to render the stream provides a streaming response that can also be piped and chunked. One site that takes advantage of streaming server-side rendering is Spectrum. They're streaming their initial HTML response in chunks and have known a significant time to first byte improvements. Another benefit of this approach is that it allowed them to use a single server instance to render multiple pages at once, improving how they scale up their entire app architecture. So streaming is great, but it's important to remember that it is not a silver bullet for performance. The big sell for streaming server-side rendering is that it provides a mechanism for handling back pressure. However, it's important to be aware that the high watermark in Node.js for streams like HTTP responses is 16 kilobytes. And this is essentially the target buffer size that React is going to be filling as each HTML chunk is requested. And this means that if your page is relatively small on the order of 16 kilobytes, you might not notice a large performance difference. In the future, this may change with things like asynchronous rendering and React suspense. But for now, keep this in mind. In our Spectrum analogy, we assume the trade-offs that we're making apply to the entire page or to our full application. We talked about some tools and techniques that can help increase the performance of server-side rendering and reduce its impact on client-side rendering. But what happens when we boot up our JavaScript application on the client? Yes, we've server-rendered the HTML. We've got that fast, first, contentful paint. But the page isn't really ready to do much until we've downloaded all the JavaScript and evaluated it in the browser. Here's the thing. Server-side rendering can actually help us reduce the amount of client-side JavaScript we have to run in order to get the page interactive, using a technique called progressive hydration. Progressive hydration starts by breaking up the page along meaningful component boundaries using code splitting. With those pieces of the page now controlled by separate scripts, we now have the opportunity to hydrate them independently based on some priority that we determine. This means that we can strike a balance between server-side rendering and client-side rendering differently for each section of our app. Let's see what this actually looks like. Here's a visualization of a typical full-page hydration solution. The grayed-out pieces of the user interface indicate the experience that is not yet interactive yet because the corresponding code hasn't been executed on the client. Once it turns to color, the entire application is loaded, and the whole page becomes interactive at once. Contrasting this with a progressive hydrated solution, we can see individual parts of the page get hydrated and become interactive as their code is downloaded. Also, notice that there's some sections of the page that remain uninteractive. With progressive hydration, we now have the option of deferring hydration for a component until it comes into view or is needed for user interaction. Here's a snippet of a server-rendered HTML page. At the top, we have our application's bundled JavaScript, which needs to be downloaded and parsed before the page gets interactive, though at least in this case, it's not blocking parsing because it's marked with defer. At the bottom, we can see we've injected some data when generating the page. The server already needed this in order to render the app, and passing it to the client avoids refreshing that same data again and wasting bandwidth and time. Let's see what this looks like using progressive hydration. Instead of sending the server's data for the whole page, data is now co-located with the initial server-rendered HTML inside of our component's root in the DOM. In fact, the entire component is now an isolated unit that can theoretically be booted up separately from our app. Progressive hydration is actually something that's on the React team's roadmap for suspense. However, it's possible to implement this today using some clever workarounds provided by the community. Let's take a peek at one of them. So we're going to walk through a hydrator component that we could use to break up rendering and enable progressive hydration. The first step here is to prevent re-rendering. This gives us a root in the DOM that avoids renders cascading through our hydrator boundary. Next, we render an element with dangerously set inner HTML set to an empty value. In React, dangerously set inner HTML is actually ignored during hydration, effectively letting us bypass diffing for the server-rendered DOM that exists inside of our component root. Finally, once the hydrator component is mounted, we listen for an indication that it should be hydrated. In this case, we're using intersection observer to detect when the component is coming into the viewport. Once the hydrator comes into view, the component is imported and hydrated in place against the server-rendered DOM that we captured by bypassing diffing. If the component was already in viewport when it was mounted, intersection observer will fire right away, and our hydration will happen quickly. Now, it's worth noting that the actual implementation of hydrator is a little bit more complex than we've shown here. It needs to have a separate code path to take when rendering on the server so that the target component can either be pre-loaded or required synchronously since server-side rendering tends to happen in a single pass. Still, the basic idea remains the same. So let's take a look at an example of using our hydrator component. Here, we have a product listing page. There is a suggested component at the bottom of the list that shows maybe recommended products that the user might be interested in. And while this component might be important, the list is usually fairly long, and this means this, there's a high likelihood that suggested will not be in the viewport on initial load. We can replace the component's static import with our small hydrator implementation and then replace the suggested component itself with our hydrator wrapper around it. Now, the suggested UI is rendered on the server, but its client-side implementation only loads when the user scrolls down. It's also worth noting that React.lazy could help fill some of the gaps in our hydrator implementation. We'll still need custom logic for determining when to trigger component import and hydration, but React.lazy gives us a new primitive for deferring subtree rendering. Soon, there might not be a need for that dangerously set inner HTML workaround. Airbnb has actually been experimenting with this, and the results are looking really promising. They've used something like the hydrator component we just saw to defer the download and rendering of components that aren't in the viewport on initial load. Then, they load these in when they're coming into view or even during idle time. They've been testing this out, and early data shows a huge improvement in their client-side time-to-interactive metric, which essentially tracks the time taken to load a given screen when navigating purely on the client. Of course, this also translates to improved first load performance. So we can see the results of this work using the profiler in Chrome DevTools. Here's a timeline showing the rendering work for navigating to a home's listing page without progressive hydration in place. All of the work has to complete before the page becomes interactive. Applying progressive hydration to a few key components reduces the time required to get the majority of the page interactive. Even though the same amount of work is still being done, we've moved lots of it into idle time, and this ensures the page can get interactive quickly before going off and doing what is now speculative work. So the Angular team is also looking at supporting progressive hydration. This is still also very experimental, but the possible outcome looks pretty exciting. They're utilizing benefits of IV, their new rendering engine, to load progressively on the client, picking up from server-rendered markup as each component is downloaded and hydrated. Only a relatively small initial framework bundle is fetched for first load, while the full framework only gets required after the user begins interacting with the page. And underneath the hood, it uses custom elements to represent components in the DOM that can be hydrated and wired up to the lifecycle. Here's a visual of a sample app that's already using this approach today. Take a look at it with the link below. A complete server-rendered markup is shipped to the browser as fast as possible, and only after the user begins to interact with the page, components code, along with any needed runtime, is fetched piece by piece by piece. So with this, we're getting the best of both worlds, a fast first-name experience, and a fast time to interactive based on prioritizing what gets loaded for the user. Vikram on the Angular team is leading these efforts, and it's worth watching us talk with Stephen what's new in Angular to learn a lot more. Here's a possible example of how custom elements could probably be used to defer and hydrate a component. Now, this isn't the code used within Angular's experimental demo, but it can just sort of visualize how custom elements might just be able to be used as a split point similar to React components like Jason just showed. Now, there has been some interesting progress in the view community to see whether this pattern can also work with view server rendered applications. Marcus, a member of the view ecosystem, wrote a view lazy hydration plugin that allows for progressive hydration of elements only when they're actually needed. Although this is still also very early stages, it's amazing to see how it enables component hydration on either visibility or specific user interaction. Here's an example of how its API makes this possible. You can wrap any components you would like to hydrate progressively and use an on interaction property to fetch a component based on a specific user event. The amazing thing about this entire progressive hydration model is that although it's still a relatively new concept, we're seeing all these frameworks plan ahead for the near future. And they're planning to take advantage of the capability of server rendering to only load the JavaScript the user needs when they actually need it. Take a look at any of these sample apps to get a better idea of how they work under the hood. Now, another reason why a lot of us decide to render content on the server is for SEO. Now, the thing about SEO is that there are a lot of interesting opinions around how bots and crawlers see your page if it happens to be mostly client-side rendered. Before we begin talking about some of these opinions, it's important to differentiate between two different types of sites like we did earlier in this talk. A static site that works without JavaScript and has all or most of its content in its initial HTML payload and an entirely client-side rendered site which does not work without JavaScript and only ships a minimum shell of HTML in the very beginning. The thing to note is that with a static site, its content will be indexed in the first round of indexing by the crawler. While in a client-rendered page, it can only be indexed after it gets rendered. The first misconception that we hear quite often is that client-side rendering will negatively affect SEO and discoverability. But the fact of the matter is that it will only do so if the crawler does not support any JavaScript whatsoever. Here's an example of the same fresh tomato zap in both conditions. Entirely server-side rendered or client-side rendered run through mobile-friendly tests. Now, mobile-friendly tests is a tool that lets you find out how Google sees your page on a mobile device. But if JavaScript is supported by Googlebot, why on earth are we seeing a blank page for the client-rendered example? Well, for this specific app, we are using ES6 features like Let and Const. Previously with Googlebot, it only used the Chrome 41 build, which does not support any ES6 features or newer APIs. But thankfully, we just announced that Google Search will now always use the latest Chrome version to render websites. This means any ES6 features or newer APIs, like Intersection Observer, are supported. Now, what this all means is that regardless of whether you're shipping a fully client-side rendered experience or an entirely static site, crawlers that support JavaScript like Googlebot should pick them both up just fine. But what about other crawlers beside Googlebot that does not support JavaScript just yet? Another misconception is that complete server-side rendering is needed for those types of crawlers and bots. The truth here is that there are more ways to improve discoverability of client-rendered sites without going the full server-side rendered approach. The first thing you can and should consider doing is making sure that you have useful meta and title tags at the head of your HTML document. If you're using a framework like React, libraries like React Helmet and React Helmet Async can make this easier. And if you're using Vue, we can rely on the Vue meta library. With Angular, you can use the built-in meta service to add and change these tags. The second thing that's important that you should also consider doing is to include critical content in your HTML markup. If the content on your site does not change much, pre-rendering is a simple approach to make this happen without worrying about building a full server-side rendered backend. Libraries like React Snap, pre-render SPA plugin can make it easy to generate HTML files of all your routes for your React and Vue sites. And with Angular, you can use Universal to not only serve rendered pages at runtime, but have them created at build time as well. Another thing you can also consider doing is dynamic render. With dynamic rendering, you can conditionally decide to only serve pre-rendered pages if you detect that a bot is using your page and fall back, disturbing your app to your users as normal otherwise. So server-side rendering is pretty useful for making sure that the markup we deliver to bots when they request a page contains as much information as possible. But that information depends on the server having a bunch of data, which brings us to one of the lesser talked about implications of server-side rendering, its effect on data fetching. We know that getting content delivered to the client as soon as possible has value, but I'd like to suggest that server-side rendering could still hold value even if you were to send no application body content at all. When we render an application on the server in response to requests, we give the server a lot of knowledge about that application's needs on the client. With this knowledge, the server can push or preload our JavaScript bundle, shifting the entire network waterfall forward in time. And if we know about the application's modules, we can also push any necessary dependencies. Taking this one step further, rendering the app lets the server know about data requirements for a page which it can then preload. All of this combines to have a dramatic effect in the reduction on time to interactive. This gets increasingly worthwhile as you scale up to larger applications, where pushing or prefetching resources that would have only started loading after being requested by something else really matters. It can even help simply by getting connections opened earlier when resources are coming from other origins. The thing is, if we have a server that knows what data is going to be needed by the client for every page, we can use that to do much more than just preload. In a setup like this, the server can actually do all the data fetching on behalf of the client for our initial request. Like preloading, this means the client doesn't have to wait until components are booted up before fetching their data. But it also unlocks one of the most useful optimizations of all. The server now has an opportunity to reduce the overall amount of data that it sends to the client because it knows what pieces are actually necessary for first view. Coming back to our spectrum analogy, we've seen how streaming and progressive hydration can help us move towards the center of the spectrum, which is ultimately where we want to be. But it's important to realize that server-side rendering is about more than just rendering the HTML for pages on the server or making your site work when JavaScript is disabled. The techniques that we've shown here today can be combined to create a prioritized, progressive loading story for your application. And this is really important because it's making difficult trade-offs like these that lets us deliver on one of the web's core values. Content and applications that are universally accessible across all devices, capabilities, and connections. Check out the links on the right for more information on the techniques we presented. And feel free to reach out to Hussein and I on Twitter with any questions. Thanks.