Rendering on the Web: Performance Implications of Application Architecture (Google I/O ’19)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments

they show the progressive hydration demo for react here: https://bit.ly/react-progressive. use your network tab!

πŸ‘οΈŽ︎ 3 πŸ‘€οΈŽ︎ u/swyx πŸ“…οΈŽ︎ May 13 2019 πŸ—«︎ replies

Thanks for the Spectrum shoutout (~15:00 mark)! You can find our entire server-side rendering server, including streaming, here: https://github.com/withspectrum/spectrum/blob/alpha/hyperion/renderer/index.js (it's named "hyperion" after one of the twelve Titan children of Gaia and Uranus)

If you have any questions about it, let me know!

πŸ‘οΈŽ︎ 2 πŸ‘€οΈŽ︎ u/mstoiber πŸ“…οΈŽ︎ May 13 2019 πŸ—«︎ replies
Captions
[MUSIC PLAYING] HOUSSEIN DJIRDEH: Hello, everybody. My name's Houssein. JASON MILLER: My name's Jason. We work on the Chrome team to improve speed and performance on the web. HOUSSEIN DJIRDEH: And our talk today will 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 and 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-side 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 is entirely client-side rendered. Meaning that it only shipped the shell of HTML content at the first request, and then it uses the client library or framework that blocks rendering to entirely create the Dom at an attach root. 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 it is 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, is client-side rendering really a problem in the general sense? To try and answer the question let's try again to visualize the process of loading for a 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 page 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 your users, is that the larger the bundle, the longer they use 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 archived 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 it's just a few years, the median amount of JavaScript for 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. JASON MILLER: 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 begun to 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 render 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 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 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? HOUSSEIN DJIRDEH: So there are a number of frameworks that are providing 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 the string method to render the top level components 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 a library to render a Vue instance into HTML using render to string. With Angular, Universal makes it possible to turn client requests into fully service rendered HTML page. The ngExpressEngine 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 a 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 Vue, 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 byte since we have to account for additional and unavoidable server think time. Or time to interactive 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 to begin 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. JASON MILLER: 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. HOUSSEIN DJIRDEH: It's a great slide. He worked on it for quite awhile. JASON MILLER: 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. Free 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. HOUSSEIN DJIRDEH: 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 could 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-doms rendered 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 pre-fetching 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 [INAUDIBLE].. 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 easily pass 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 could 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-to-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 could do the same with Vue-server render where switching to render to 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 initially some response in chunks and have known a significant time to first byte improvements. Another benefit of this approach is that it allow them to use a single serve instance to render multiple pages at once, improving how they scale up their entire app architecture. JASON MILLER: 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 HP 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 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 are some sections of the page that remain uninteractive. With progressive hydration, we now have the option of deferring hydration for our 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 applications 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 retouching 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 work arounds 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 in our HTML set to an empty value. In React, dangerously set in our 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 IntersectionObserver 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, IntersectionObserver 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 preloaded 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 dot 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 dot lazy gives us a new primitive for differing subtree rendering. Soon, there might not be a need for that dangerously-set in our HTML workaround. Airbnb has actually been experimenting with this. And the results are looking really promising. They've used something, like the hydrated 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 could 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. HOUSSEIN DJIRDEH: 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 and the dom that can be hydrated and wired up to the lifecycle. Here's a visual of a sample app. It's already using this approach today. You could 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 paint 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 component, like Jason just showed. Now, there has been some interesting progress in the Vue community to see whether this pattern can also work with Vue server-rendered applications. Marcus, a member of the Vue ecosystem wrote the vue-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 components 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 that there are a lot of interesting opinions are on how bots and crawlers see your page if it happens to be mostly client-side rendering. Before we begin talking about somebody's 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 the 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 tomatoes app in both conditions-- entirely server-side rendered or client-side rendered run through mobile-friendly test. Now, mobile-friendly test 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 were using ES6 features like let and const. Previously with Googlebot, if it only used the Chrome 41 build, which does not support any ES6 features or in 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 IntersectionObserver, are supporting. Now, what this all means is that regardless of whether you are shipping a fully client-side rendered experience or an entirely static site, crawlers that support JavaScript by 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 to 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 text. The second thing that's important that you should also consider doing is include critical content in your age demo markup. If the content on your site does not change much, pre-rendering is a simpler approach to make this happen without worrying about building a full-service side-rendered back end. Libraries, like react-snap prerender-spa-plugin can make it easy to generate HTML files of all your outs for your Vue for your reactive Vue sites. And with Angular, you can use Universal to not only server-render pages at runtime, but have them create a build time as well. Another thing you can also consider doing is dynamic rendering. 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 to serving your app through users as normal, otherwise. JASON MILLER: 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 pre-fetching 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 Houssein and I on Twitter with any questions. Thanks. [MUSIC PLAYING]
Info
Channel: Google Chrome Developers
Views: 27,561
Rating: 4.9058824 out of 5
Keywords: type: Conference Talk (Full production);, pr_pr: Google I/O, purpose: Educate
Id: k-A2VfuUROg
Channel Id: undefined
Length: 33min 3sec (1983 seconds)
Published: Thu May 09 2019
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.