[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]
they show the progressive hydration demo for react here: https://bit.ly/react-progressive. use your network tab!
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!