
Scaling React Server Side Rendering - boernard
https://arkwright.github.io/scaling-react-server-side-rendering.html
======
yoz
This is a fantastic article. I was daunted when I saw the length, but
everything about it - the prose style, the mixture of drawings, the pace - led
me through and out with a whole load of valuable lessons about load balancing,
caching, isomorphic rendering and more. Thank you!

One question for the author, if they're reading: how did you prep for writing
this piece and gather the story details? It's quite a journey, and - if I was
writing something like this - I would have a hard time keeping track of all
the different twists, stats and lessons as they happen so they can be written
up later. Do you keep a notebook, or did you rebuild the story from artifacts?

~~~
arkwright
Thanks for the kind words! You made my day.

I didn't really do any preparation. I had recently been thinking that pretty
much every website I had ever built was sadly no longer in existence, and so I
wanted to start producing real, "tangible" artifacts from my work; something
that might have a shelf life of more than a couple of years. I had those
recent SSR adventures in mind, and wanted to write them down before they faded
from memory.

I usually begin with a bulleted todo list of insights or topics I want to
cover. Then I dive in, and write in a more or less stream of consciousness
fashion, which causes me to think of more topics to add to that list. I comb
over the article and the list iteratively, reordering stories, editing, and
and adding context as I go, until the result feels right. In this case I
didn't have any notes, the content was rebuilt from memory.

~~~
forrestthewoods
Great post! What tool(s) did you use for the drawings?

~~~
frandroid
The response, much further down:
[https://news.ycombinator.com/item?id=21916700](https://news.ycombinator.com/item?id=21916700)

------
frandroid
An amusing tidbit about optimization, toward the end:

> Because we were seeking to improve performance, we became interested in
> benchmarking upgrades to major dependencies. While your mileage may vary,
> upgrading from Node 4 to Node 6 decreased our response times by about 20%.
> Upgrading from Node 6 to Node 8 brought a 30% improvement. Finally,
> upgrading from React 15 to 16 yielded a 25% improvement. The cumulative
> effect of these upgrades is to more than double our performance, and
> therefore our service capacity.

Free optimization, ripe for the taking!

~~~
mr__y
Depending on the code and the dependencies it might not be that free if you
run on some unexpected problems after those upgrades due to some
incompatibilities or even reliance on now-fixed bug. While I understand that
the latter should not be the case given the code is written properly but with
sufficiently large projects this is not as uncommon as we would like it to be.

Also there is a more common scenario where updating one thing requires
updating other packages and through a long chain of denependencies one of the
pieces being updated has something missing in the new version (that was
available in the previous version) and anything that relies on that will stop
working.

Anyway, even the best case scenario where everything is perfectly fine after
the updates still requires detailed testing to ensure that really everything
is as OK as it seems. So even then this is not totally _free_

But then of course, it may still be the easiest path for improving the
performance.

~~~
chii
> easiest path for improving the performance.

upgrading should not be seen as an alternative to performance engineering
though. Even if upgrading _does_ bring in some performance improvements.

Upgrading should be because of reasons such as security updates, and bug-
fixes, and to continue to reap the improvements/features in the next version.

~~~
pferde
No, not an alternative, but I'd argue that considering new versions should be
a part of performance engineering. If one of the "improvements/features in the
next version" is more optimized and performant code, you want to "reap" it
(after of course considering and testing the upgrade from other points of
view.

------
eliseumds
I developed the JS architecture of
[https://www.productreview.com.au](https://www.productreview.com.au) and we
have faced tons of issues getting SSR right, but it was worth it. We're
getting more than 10M pageviews a month and I'd like to share our experience:

* Upgrading NodeJS indeed gives us massive performance boosts, but apply with caution. Ideally have a set of visual regression tests just to be safe

* Profile your NodeJS code just like you do with browser code. Sometimes the bottleneck could be in an Express middleware or in reading a massive Webpack manifest file

* If a component doesn't need to rendered on the server, don't do it. Don't waste CPU cycles (for ex, out-of-view content). Just make sure you got your SEO meta tags right

* Don't load more data than you need. It takes time to parse, it takes time to loop through and it takes time to stringify for rehydration

* Enable BabelJS's debug mode and remove unnecessary plugins

* Don't import more stuff than you need. Tree-shaking is important on the server-side too

* If you're using CSS modules, use the Webpack loader `css-loader/locals` on the server so that it doesn't emit CSS files (useless). The client compiler should do so

* Monitor your server-to-server requests. They're usually what take the longest, so cache the most important ones

* As with the majority of websites, cache is king

* Properly serialize your JSON strings. That's what we use: [https://gist.github.com/eliseumds/6192135660267e2c64180a8a9c...](https://gist.github.com/eliseumds/6192135660267e2c64180a8a9cdb7dd1)

* It can be worth it to return a dangerous HTML string from a component instead of a tree of React nodes. We do that when we render SVGs and microdata tags

Again, it's a pain-in-the-butt. You'll have checksum errors, need to
synchronize clock, polyfill Intl APIs because they're inconsistent and so on.

~~~
minitech
> * Properly serialize your JSON strings. That's what we use:
> [https://gist.github.com/eliseumds/6192135660267e2c64180a8a9c...](https://gist.github.com/eliseumds/6192135660267e2c64180a8a9c..).

That doesn’t look like “properly”. The double escaping is overcomplicated and
no safer compared to a direct

    
    
      window.__productreview_data = ${escapedReduxStateJsonString};
    

(and forgets about \v, maybe others), the transformation doesn’t preserve
“</_escaped_script”, and it doesn’t address a vulnerability involving <!--
that’s contrivable.

Closer to correct:

    
    
      JSON.stringify(data)
        .replace(/\u2028/g, '\\u2028')
        .replace(/\u2029/g, '\\u2029')
        .replace(/</g, '\\x3c')
    

Better, if you put the JSON in an inert <script> (type="application/json"),
it’s only necessary to escape < (or /<[/!]/g). This is a good idea so you can
use restrictive CSPs.

~~~
eliseumds
Thanks for pointing out the `<!--` vulnerability. In regards to rendering the
string inside a JSON.parse, we do that because of performance:
[https://v8.dev/blog/cost-of-javascript-2019](https://v8.dev/blog/cost-of-
javascript-2019). From what I remember, we had some issues with IE11, thus the
replacement for the other characters.

We'll consider "application/json", makes sense.

~~~
minitech
Given a correct function that converts a JSON-representable value to embed-
safe JSON, you can use it on the JSON to get your JSON.parse performance:

    
    
      const inlineJSON = data =>
        JSON.stringify(data)
          .replace(/\u2028/g, '\\u2028')
          .replace(/\u2029/g, '\\u2029')
          .replace(/</g, '\\x3c');
    

with:

    
    
      const escapedReduxStateJsonString = inlineJSON(JSON.stringify(data));
    

But yeah, the isolated <script> thing is usually even better (more compact in
addition to the security benefit).

------
lewisjoe
This looks like a great piece of article. Kudos to the people who wrote it
because the most sure-shot problem in SSR is running to scaling issues and
this is a much needed one.

But here's an unpopular opinion: Server side rendering shouldn't even be a
thing. Running a language as dynamic as Javascript on servers, is at best - a
problem that _can be_ dealt with, but not necessarily the solution.

I'm saying this as a full-time Javascript developer. We can do better than
mandating JS on the servers.

#1. SPA, Components and functional programming is the best thing that happened
to web development in the recent past. So, let's stick with it.

#2. But we are stuck with Javascript to embrace these otherwise abstract
engineering methods, because browsers are stuck with JS.

#3. Webassembly is here. So why not a UI-framework, that embraces components,
SPAs and functional programming but with a better language (something like
Elm). A language that compiles to webassembly for browsers to run logic &
build UI and runs natively on servers? This hypothetical system should compile
to HTML on the servers and support smooth progressive hydration.

Running a bunch of JS on the servers, on a piece so critical like rendering
HTML will always be a suboptimal solution. Imagine saving all that server-
scaling costs with a much server-cost-friendly language like Rust or Swift?

~~~
BinaryIdiot
Rather than running JS on the server, one way I found things to be effective
is using any server side templating capability to basically "bootstrap" what
the JS might typically do on start-up. So the server renders you a page that
looks well formed and then the JS hooks into it for client interactions.

The issue that can be runned into here is possibly duplicating efforts on the
server and JS side. You can keep them separate enough but it's tough if you're
used to creating everything either through the server or through JS on the
client.

The last web app I worked on like this is unfortunately not public but
performed rather well and wasn't all that difficult to maintain either.

~~~
irq11
This is called “progressive enhancement”, and is, in fact, the way that you’re
_supposed_ to write webpages: they should come pre-rendered, and only if the
client has JS enabled do you enable more dynamic features. You fall back to
full pageloads to handle form submissions with not-very-clever coding, and can
usually preserve _most_ of the functionality of a webapp even without JS.

Until about 5-6 years ago, this was the way people tried to write webapps
(they didn’t always get there, but at least they _aimed_ for it). It’s really
startling that it’s something that people have already forgotten.

It really isn’t terribly difficult to do this. Frameworks like Rails make it
pretty easy to do, out of the box.

~~~
arvinsim
> This is called “graceful degradation”,

Isn't that strategy "progressive enhancement"?

[https://www.w3.org/wiki/Graceful_degradation_versus_progress...](https://www.w3.org/wiki/Graceful_degradation_versus_progressive_enhancement)

~~~
irq11
Yes, it is. I corrected my post around the same time you replied. I’ve
colloquially used the term I originally cited, but the correct term is
progressive enhancement.

------
wayneftw
There was no mention of why server-side rendering was needed at all. Based on
my companies research Google and Bing do just fine without it.

> when the server is under high load, skip the server-side render, and force
> the browser to perform the initial render.

With this type of contingency plan, I don't see any reason to use server-side
rendering at all. We build all of our sites and apps with React and don't do
it at all, for any reason at this time.

Is there something we're missing?

~~~
simonw
Try using client-side rendered website on a cheap Android phone on a 3G
connection sometime - or even a cheap Android phone on LTE.

That's how most of the world's population will experience your site.

I'm baffled that so many sites have invested in multiple megabytes of
JavaScript to render their pages. It's like our entire industry has forgotten
how to build sites that can be used by anyone who's not on an LTE iPhone.

~~~
unlinked_dll
>That's how most of the world's population will experience your site.

Unless you're FAANG you probably don't care about _most_ of the world's
population, but the tiny slice that is _most likely_ to see your site and
generate revenue for you.

It's not baffling that most developers don't make things for most people.
That's just a waste of time and money.

~~~
kalyantm
What people fail to consider is even with the high end smartphones, how do we
know you are guranteed full speed 4G all the time? The network speed varies
greatly in various places (in the subway, through the tunnels etc) and more
often that not, we have sucky network speeds even though we are on "4G"

~~~
true_religion
I suppose it depends on how much you want to put effort into optimizing a
single app for all users, rather than having a lite version of the site.

For my company, we serve images and video. Even multi megabyte JavaScript only
equals one minute of a 720p videos runtime.

For people on restricted connections, we have a lite versions with less than
100k JavaScript and videos transcoded to 260p.

------
superkuh
The easiest way to do this is to not use javascript for everything in the
first place and to actually generate html instead of expecting people to run
your code, then having to do it yourself in a convoluted workaround when they
won't or can't.

~~~
a13n
This is probably the worst possible takeaway from this article.

> not use javascript for everything

Using JS for frontend + backend has significant advantages. Your developers
only need to know one language/codebase. You don't need to hire/maintain
separate frontend/backend teams who need to figure out how to coordinate with
each other.

> actually generate html

That's what server-side rendering does.

> expecting people to run your code

This is how the web works, for the vast majority of users and markets worth
serving.

> having to do it yourself in a convoluted workaround

Running your frontend code on the backend to generate HTML is an elegant
solution and extremely easy to implement. These days it works out of the box.
Not sure where you got the idea it was a "convoluted workaround".

~~~
dvt
> Running your frontend code on the backend to generate HTML is an elegant
> solution and extremely easy to implement. These days it works out of the
> box. Not sure where you got the idea it was a "convoluted workaround".

Oh boy, this must be the overstatement of the decade. I've done SSR with a few
frameworks now, including Meteor, Nest, and Next. Saying that "it works out of
the box" is so disingenuous, it borders on fake news. Even ignoring the
trillion edge cases involving authentication, cookies, localstorage, dynamic
components, promises/futures, async components, and so on, it will take dozens
of man-hours to get properly-rendered server output that works with server-
side routing, hydration and looks good on Google's SEO crawlers.

~~~
a13n
This hasn't been my experience. How does SSR not work out of the box with
NextJS?

~~~
dvt
Simple example: I worked on an app where I wanted to use the @elastic/eui UI
framework[1]. That framework (a fairly popular/vetted one) used some DOM
manipulations somewhere deep for some thing or other and Next broke with some
bizarre error. Had to find this snippet in someone's reported (Gatsby) issue
and stick it in my next.config.js file:

    
    
          /**
           * We have to force the bundling of @elastic/eui and react-ace
           * as Gatsby, then all loaders below will match and avoid HTMLElement issues
           */
          config.externals = config.externals.map(fn => {
            return (context, request, callback) => {
              if (request.indexOf('@elastic/eui') > -1 || request.indexOf('react-ace') > -1) {
                return callback();
              }
    
              return fn(context, request, callback);
            };
          });
    
          config.module.rules.push(
            {
              test: /react-ace/,
              use: 'null-loader',
            },
          );
    

And this was just my _first_ SSR issue. Definitely not "out of the box."

[1] [https://github.com/elastic/eui](https://github.com/elastic/eui)

------
chrismmay
Nice article. It does a great job of explaining just how much unproductive
work Google has created for developers. They created SPAs with Angular in
2010, but 10 years later, their search engine still can't properly index
client-side-rendered SPA applications, forcing you to jump through all these
hoops, to undo the benefits of client-side rendering, just to fix what they
should be fixing with their search engine. It's truly insane. What a waste of
time and energy. I hope they are working on a solution. I guess this
represents an opportunity for a competitor to come in and do it better.

Following your journey through troubleshooting load balancing and caching
brought back memories for me. I don't know what you're using for caching, but
JSR-107 has been around for nearly 20 years. You might want to check out
[https://commons.apache.org/proper/commons-
jcs/](https://commons.apache.org/proper/commons-jcs/). I know it's not
Javascript, but it will solve your caching problem in an orderly way. You
shouldn't have to start from scratch on caching. You might even consider
telling your content creators something like "updates to the site will only
take effect the next day" so you can just invalidate the entire cache once a
day and be done with it. Keep it simple.

------
simonw
This is from October 2017. It remains a fantastic explanation of load
balancing and server-side React rendering performance techniques.

------
bcherny
This is really well written, the illustrations help visualize the content, and
the content itself is largely novel.

Really nicely done. Thanks for taking the time to write this — I enjoyed
reading it!

------
e12e
Isn't the first half of this: "use haproxy" (eg: see [1]) (or any other real
load balancer)?

I'm not sure if the second half is "... And squid or another caching web
proxy" \- but I'm open to the ssr pipeline being far enough from REST (the
architectural pattern) that caching is broken, and something more application
level, like redis/memcache or a custom cache is needed.

[1] [https://www.haproxy.com/blog/four-examples-of-haproxy-
rate-l...](https://www.haproxy.com/blog/four-examples-of-haproxy-rate-
limiting/)

------
jaequery
How about just generate and serve static pages? Aren’t most apps on Netlify
and serverless doing this now?

~~~
noahtallen
If you want an “app,” it makes sense to do a lot of stuff client side —
especially if you’re trying to edit or create something in the app (I.e.
Google Docs, Gutenberg editor in WordPress, Microsoft office online apps). If
you want a landing page for your new startup, static pages on a serverless
architecture make way more sense. It really depends what the use case is.

Probably more of the confusion lies in the line between those things — like a
CRUD app or a dashboard or management tools. You could do them server-side for
better initial performance, but you could also get better interactivity
client-side.

I think a lot of new projects go towards the interactivity and “slick UI” side
of things which is partially why we see more focus on client-side things these
days. Speaking to myself (a full stack dev with a front end focus), we front
end devs would really benefit from caring about performance and stability
more.

------
hnbreak
People debate SPA vs SSR without any context. Both have their use case:

 _Everything before a login = > SSR, everything after => SPA._

Why? SSR is proven to be much better at SEO. But SPAs offer best UIs. Nobody
wants to click through stuttery SSR dashboards in 2019, wait for page loads,
submits, etc. People prefer slick UIs, that was one of the reasons
DigitalOcean got big (because of their then stunning dashboard or after-login-
experience [1]) and hence every other hoster copied their interface.

[1] DigitalOcean's dashboard experience was for a long time the main teaser
(as an animated gif/video) on their landing page.

~~~
mhd
> But SPAs offer best UIs

I don't consider this a settled conclusion. Mostly because it's a false
dichotomy. There are alternatives to both SSR of component sites and SPAs, and
every solution has its inherent advantages and disadvantages.

UI isn't even the primary feature of an SPA, that would be offline usage.

Having more interactive elements without page loads is a feature of "DHTML",
and that can be had from anything between small embedded snippets of vanilla
JS to full-fledged SPA frameworks. Intermediate solutions like StimulusJS or
AlpineJS seem to be getting a bit more popular, too.

But in this Fallen World, it seems we're usually getting the worst of either
extreme usually. Either a long rendering time for a whole page, then delivered
in one "flash" (assuming you've got a fast connection), or multiple elements
popping in and out while several JSON-RPC requests are made.

You can optimize both cases, of course. Proper caching/DB views etc., or
things like HTTP2/GraphQl/React Suspense etc.

But it's definitely not an either/or answer. Few things in fiddling around
with computers are.

~~~
hnbreak
Good point and I agree that the borders between SPA and SSR are blurry.
However, I just wanted to stress that a debate without having requirements is
useless, it's like saying a racing car is better than a truck. But for what?
Building websites is not like building websites 20 years ago. There are many
uses cases and saying one is better than the other rather shows that you never
experienced the other side. I mean, there are still people out who never
touched react, how should they fully grok what mighty system and ecosystem
react has created and choosing another stack comes with much smaller or dying
ecosystems.

To your points, I still think a proper SPA without any quirks such as DHTML,
React Suspense, etc. gives the best UI for dashboard and logged-in kind of
uses cases. However, having mixed environments is from a production and dev
perspective subpar and hence you end up with setups like Next (SSR) with some
Next pages having a stronger SPA notion (SPA within SSR).

~~~
mhd
Yeah, I never quite liked the currenly en vogue mixed approaches, where it's
harder to draw a line, you often have to serve two masters and you feel it's
mostly done that way because React developers don't want to learn anything
else, despite cases where a complete server-side approach with an old-school
template language might be a better fit, despite how hip functional reactive
component based development is.

But about the smaller stacks other environments have: That's quite often
because you either don't _need_ additional parts (e.g. there's no desire for a
huge Django template "community") and/or because other stacks are more full-
fledged and thus the horizontal size is a lot smaller, with no need for
umpteen state management solutions, state management solution helpers and
state management solution application templates.

Dashboards could probably serve as a whole different topic. It was easy to
beat the old school ones, where Perl CGIs roamed the prehistoric landscape.
More modern CSS, and JS graphs alone beat the old rrdtool setups you often
saw.

~~~
hnbreak
Re your second point: How is the SSR scene in Go land? Are there thriving
ecosystems?

Besides, it took me a long to leave pug/stylus, I'am still not sure if a pug-
based SSR is still the best way to get stuff out of the door. But again,
opting for an 10 years old stack let you miss lot of things (eg
maintainability of react code is top-notch).

~~~
mhd
I don't think I heard "SSR" in a context where it's just about backend HTML
generation like we did in the olden days, only when it comes to reify JS views
on the server. Haven't heard of an embedded JS interpreter or transpiler that
does that.

When it comes to generating dynamic or static web page content, the
pathological framework-aversion of the Go community strikes hard. Probably
nothing that doesn't use the built-in Go templates with any sufficient user
backing. This doesn't appear to be a language that can birth something like
RoR.

As for the maintainability of react, I'm not so excited. It's a pretty decent
templating system, and it seems easy enough to compose components, but beyond
that it's each to their own, with some approaches being better than others.
And redux still doesn't grab me as that great, it's just the sheer amount of
developers resulted in a nice toolset. Whether it's frontend or backend, the
twin async and dependency hells of JS don't manage to make me sleep any
better, either.

I don't give a flying frick for age myself. Sure, there's less tooling for
partials than for components, but that might have a reason.

------
nwsm
Great article.

Only thing that I thought was out of scope/unrealistic for most teams was
having a 6 month time bomb on traffic and deciding to build a load balancer.

------
boernard
Incedibly nice writeup! I really like how the drawings help to better
understand the concepts. A lot of time must have gone into crafting this
article.

------
barbarbar
I think it would be a good idea to see this setup in a larger comparison like
[https://www.techempower.com/benchmarks](https://www.techempower.com/benchmarks).
It is not my impression that node based apps did particulary well here in
these tests. So if performance is important why not use something that has
proven to be fast.

------
Twirrim
Great article, interesting read indeed.

> The cumulative effect of these upgrades is to more than double our
> performance, and therefore our service capacity.

That's a risky conclusion, in that it's likely over-generalised.

The upgrades may have improved the average performance, but they might
introduce some performance impact on less well trodden paths, things that may
strike at the least convenient time. There are performance gotchas that show
their faces when load increases (system cache inefficiencies, etc. etc.) Some
of the times I've been most hurt, operationally, have come when what looks
great in generalised circumstances turns out to have a nastier under-load
behaviour.

That said, always watch out for upgrades and make patching/upgrading a
priority task. If there is a CVE attached to an upgrade, you want to be
deploying that as fast as humanly possible. That means making sure there are
as few human-involved steps as possible in your build/test/deployment chain.

------
AHTERIX5000
Great article and interesting points about backpressure and load balancing.
But not sure what to think about the end result where you have a cluster of
machines transforming HTML and you still have to drop requests. A huge
simplicity booster?

------
Ozzie_osman
Awesome article! Out of curiosity, can you share the site this work was for?

------
idclip
Lovely, and informative. Style Reminds me of learn you a haskell, did you
consider putting this out as a book?

Web app production optimization with this kind of style would be a godsend.

------
mychael
The Table of Contents should be first, then the content. I clicked
Introduction expecting to link to something new, but it took me backwards.

~~~
nothrabannosir
Isn’t that a matter of style? It’s not uncommon for books to have the
introduction preceding the table of contents. I’m literally looking at one
right now that does this (publisher Wordsworth Classics).

------
zyngaro
Great read ! What did you use to draw the diagrams ?

~~~
arkwright
Diagrams are drawn with a Pilot Fineliner (greatest pen ever) in an artist's
sketchbook. I then take a photo with my phone, crop to size, and run the image
through a two stage conversion process. First ImageMagick converts the image
to grayscale and cranks up the contrast. Second, Potrace converts the
grayscale bitmap to an SVG. This was my hack way of avoiding the purchase of a
tablet.

One interesting consequence of this process is that every drawing needs to be
perfect the first time. I didn't realize how much I lean on undo/redo until it
wasn't there any more!

~~~
thescribbblr
Even my diagram sucks! Thanks for this amazing trick!!!

------
pkstn
I have easy solution: don't use React!

