Client-side rendering vs. server-side rendering

Initially, web frameworks had views rendered at the server. Now it’s happening on the client. Let’s explore the advantages and disadvantages of each approach.

Performance

With server-side rendering, whenever you want to see a new web page, you have to go out and get it:

Diagram of how server-side rendering works

This is analogous to you driving over to the super market every time you want to eat.

With client-side rendering, you go to the super market once and spend 45 minutes walking around buying a bunch of food for the month. Then, whenever you want to eat, you just open the fridge.

Diagram of how client-side rendering works

Each approach has its advantages and disadvantages when it comes to performance:

  • With client-side rendering, the initial page load is going to be slow. Because communicating over the network is slow, and it takes two round trips to the server before you can start displaying content to the user. However, after that, every subsequent page load will be blazingly fast.
  • With server-side rendering, the initial page load won’t be terribly slow. But it won’t be fast. And neither will any of your other requests.

To be more specific, with client-side rendering, the initial page will look something like this:

<html>
<head>
<script src="client-side-framework.js"></script>
<script src="app.js"></script>
</head>
<body>
<div class="container"></div>
</body>
</html>

app.js will have all the HTML pages in JavaScript as strings. Something like this:

var pages = {
'/': '<html> ... </html>',
'/foo': '<html> ... </html>',
'/bar': '<html> ... </html>',
};

Then, when the page is loaded, the framework will look at the URL bar, get the string at pages['/'] , and insert it into <div class="container"></div>. Also, when you click links, the framework will intercept the event, insert the new string (say, pages['/foo'] ) into the container, and prevent the browser from firing off the HTTP request like it normally does.

SEO

Suppose our web crawler starts off making a request for reddit.com:

var request = require('request');
request.get('reddit.com', function (error, response, body) {
// body looks something like this:
// <html>
// <head> ... </head>
// <body>
// <a href="espn.com">ESPN</a>
// <a href="news.ycombinator.com">Hacker News</a>
// ... other <a> tags ...
});

The crawler then uses the <a href> stuff in the response body to generate new requests:

var request = require('request');
request.get('reddit.com', function (error, response, body) {
// body looks something like this:
// <html>
// <head> ... </head>
// <body>
// <a href="espn.com">ESPN</a>
// <a href="news.ycombinator.com">Hacker News</a>
// ... other <a> tags ...
  request.get('espn.com', function () { ... });
request.get('news.ycombinator.com', function () { ... });

});

After that, the crawler continues the process by using the links on espn.com and news.ycombinator.com to keep crawling.

Here is some recursive code for doing that:

var request = require('request');
function crawlUrl(url) {
request.get(url, function (error, response, body) {
var linkUrls = getLinkUrls(body);
    linkUrls.forEach(function (linkUrl) {
crawlUrl(linkUrl);
});
});
}
crawlUrl('reddit.com');

What would happen if the response body looked like this:

<html>
<head>
<script src="client-side-framework.js"></script>
<script src="app.js"></script>
</head>
<body>
<div class="container"></div>
</body>
</html>

Well, there aren’t any <a href> tags to follow. Also, this web page looks pretty bland, so we probably won’t want to prioritize it when we show search results.

Little does the crawler know, Client Side Framework is about to fill <div class="container"></div> with a bunch of awesome content.

This is why client-side rendering can be bad for SEO.

Prerendering

In 2009, Google introduced a way of getting around this.

https://webmasters.googleblog.com/2009/10/proposal-for-making-ajax-crawlable.html

When the crawler comes across www.example.com/page?query#!mystate, it’d convert it to www.example.com/page?query&_escaped_fragment_=mystate. This way, when your server gets a request with _escaped_fragment_, it knows the request is coming from a crawler, not a human.

Remember — the server wants the crawler to see <div class="container"> ... </div> (with the content inside), not <div class="container"></div>. So then:

  • When the request comes from a crawler, we could serve <div class="container"> ... </div>.
  • When the request comes from a regular human, we could just serve <div class="container"></div> and let the JavaScript insert content inside.

There’s a problem though: the server doesn’t know what’s going to go inside the <div class="container"></div>. To figure out what goes inside, it’d have to run the JavaScript, create a DOM, and manipulate that DOM. Since traditional web servers don’t know how to do that, they employ a service known as a Headless Browser to do so.

Smarter crawlers

Six years later, Google announced that its crawler leveled up! When Crawler 2.0, sees <script> tags, it actually makes a request, runs the code, and manipulates the DOM. Just like a web browser.

So instead of just seeing:

<div class="container"></div>

It sees:

<div class="container">
...
...
...
...
...
</div>

You can use Fetch as Google to determine what Google’s crawler sees when it visits a certain URL.

Relevant excerpt from the announcement:

Back then, our systems were not able to render and understand pages that use JavaScript to present content to users. Because “crawlers … [were] not able to see any content … created dynamically,” we proposed a set of practices that webmasters can follow in order to ensure that their AJAX-based applications are indexed by search engines.
Times have changed. Today, as long as you’re not blocking Googlebot from crawling your JavaScript or CSS files, we are generally able to render and understand your web pages like modern browsers.

Less smart crawlers

Unfortunately, Google isn’t the only search engine. There’s also Bing, Yahoo, Duck Duck Go, Baidu, etc. Yes, people actually use these search engines too.

https://www.netmarketshare.com/search-engine-market-share.aspx?qprid=4&qpcustomd=0

The other search engines aren’t as good at handling JavaScript. See SEO vs. React: Web Crawlers are Smarter Than You Think for more information.

Best of both worlds

To get the best of both worlds, you can do the following:

  1. Use server-side rendering for the first page load.
  2. Use client-side rendering for all subsequent page loads.

Consider what the implications of this are:

  • For the first page load, it doesn’t take two round trips to the server before the user sees content.
  • Subsequent page loads are lightening fast.
  • Crawlers get their simple HTML. Just like the old days. No need to do the work of running JavaScript. Or dealing with _escaped_fragment_.

However, it does take a bit of work to set this up on the server. There is added complexity.

Angular, React and Ember all have moved to this approach.

Discussion

First, some things to consider:

  • Roughly 2% of users have JavaScript disabled, in which case client-side rendering won’t work at all.
  • About 1/4 of web searches are done with engines other than Google.
  • Not everyone has fast internet connection.
  • People on their phones usually don’t have fast internet connection.
  • A UI that is too fast can be confusing! Suppose the user clicks a link. The app takes them to a new view. But the new view is only subtly different from the previous view. And the change happened instantaneously (as client-side rendering folks like to brag about). The user may not notice that a new view actually loaded. Or maybe the user did notice, but since it was relatively subtle, the user had to apply some effort to detect whether or not the transition actually happened. Sometimes it’s nice to see a little loading spinner and full page re-render. It prevents us from having to squint to see changes.
  • To some extent, it makes sense to program to where the performance puck is going to be. Your users are going to be a mix of people who live in the year 2017, 2019, 2020, etc. It doesn’t make sense to pretend that they all live in the year 2017. Yes, you could update your app next year once the improvements in speed happen… but taking the time to do so certainly isn’t free.
  • Caching is a thing. So with server-side rendering, often times the user doesn’t actually have to go all the way out to the server. And sometimes they only need to go to a server nearby, rather than the “official” one across the ocean.
  • #perfmatters.
https://www.slideshare.net/phaithful/seo-and-js-new-challenges
  • Actually, with regards to performance, sometimes It Just Doesn’t Matter. Sometimes the speed is Good Enough, and marginal increases in speed don’t really make life any better.

With all of that said, I believe that for most cases, it’s best to KISS and just go with server-side rendering. Remember:

Most of your users will have decent internet connection, and it’ll be fast enough. Especially if you’re targeting yuppies with Macbook Pros. You won’t have to worry about a terribly long initial load time that causes you to lose users. You won’t have to worry about usability issues where users don’t notice that a new page actually loaded when they clicked a link.

However, there are certainly use cases for client-side rendering with server-side rendering on the initial page load. For bigger companies, it’s often the case that #perfMatters, you have users with slow internet connection, and you have a large enough engineering team to spend time on optimizations.

In the future, I expect these isomorphic web frameworks (that do client-side rendering with server-side rendering on the initial page load) to become more stable and easier to use. At that point, perhaps the added complexity will be minimal. However, today, all of this is very new, and I expect there to be plenty of leaky abstractions. Even further in the future, I expect internet connection to become good enough where client-side rendering isn’t needed, and thus the tables will turn back to server-side rendering.