SSR with Isomorphic JavaScript

SSR with Isomorphic JavaScript

Server-side rendering, or SSR, is a term that you often hear in the community of frontend development. Server-side rendering is just as it means, at the most simple level: rendering applications on the server. You navigate to a website, it makes a server request, it renders some HTML, and you get your browser back with the completely rendered result. Quite straightforward. You may be asking yourself why the community even has a buzzword for this.

All web applications were largely server-rendered before the emergence of rich and dynamic web applications that relied heavily on JavaScript and jQuery. Examples of this include PHP, WordPress, and even just simple HTML pages. You get all the HTML data and everything back when you visit a page on one of these pages. If you click a page, another request will be made to the server by the browser. The browser will refresh following the answer and render the next page from the ground up. This method works well, and it has for years; browsers make static HTML spectacularly fast. What’s been changing?

The use of JavaScript has gone from a little sprinkle here and there for website interactivity to the undisputed language of choice for web app development since the turn of the century. We are continuously shipping to the browser more logic and JavaScript. This new era of dynamic, complex, data-driven client-rendered web applications has been ushered in by single-page frameworks like React and Vue. These SPAs vary from server-rendered applications because before rendering on the computer, they do not fetch fully rendered content complete with data from the server.

Using JavaScript, client-side rendered applications render their content in the browser. They simply fetch a barebone HTML page with no body content and render all the content inside using JavaScript instead of fetching all the content from the server. The advantage of this is that with completely server-rendered applications, you skip the entire page refreshes, which can be a little jarring for the user.

Client-rendered single-page apps can update content on your computer, retrieve API data, and update right in front of you without updating any kind of page at all. This attribute is what makes modern web applications when you communicate with them feel snappy and “native.”

Client-side Rendering Trade-offs

In the client-side-rendered SPA universe, it isn’t all sunshine and rainbows. There are some trade-offs on the client-side that come with rendering the submission. SEO and initial load output are two prime examples.

SEO

Until JavaScript kicks in and renders the rest, since client-rendered apps return a bare-bones HTML page with very little content, it can be hard for search engine crawlers to grasp your page’s HTML structure, which is detrimental to your site’s search rankings. Google has done a lot of good work on this, but if SEO is important, it is always recommended to avoid client-side rendering.

Have a Project Idea?

Want to convert your idea into a successful app or website? Schedule your free call with our expert now.

Initial Load Performance

With client-rendered applications, when you first open the tab, you usually see the following things happen:

  • Any basic HTML, like an app shell or static navigation bar, is loaded by the app.
  • You see some sort of loading indicator,
  • Then your content is rendered

The problem with this is that once the JavaScript loads entirely from the network and finishes rendering elements on your computer, your application will not display anything.

In a nutshell, the issue with client-side output, in general, is that, whether it is their state-of-the-art smartphone, efficient high-end desktop computer, or $100 lower-end smartphone, you can not monitor what client system someone uses your application on.

We do monitor the server, though. We can almost always offer more CPU and memory to our server and tweak it to make it function better for users.

Getting The Best Of Both Worlds

When using server-side rendering with current front-end technology, we can get the best of both worlds for web app development. The way this normally works is that, on the first load, the server renders and sends back the completely rendered application. The next step, known as hydration, is where you download and execute your JavaScript package. This links event handlers and, like your client-side router, wires things up.

With this technique, you get all the advantages of SSR on the initial load, then client-side JavaScript can handle any interaction from that point forward. This provides for an initial load that is fast, SEO-friendly, followed by the dynamic one-page web app experience we know and love.

Applications like this are known as universal applications because on the client and the server the same JavaScript runs. You can also hear the fancier word “isomorphic” being used, which means the same thing exactly.

How To Implement SSR?

SSR is not, either, without its trade-offs. By adding a more complex setup, it adds overhead to your creation as well as having to host and maintain your own server. These issues are why incredible frameworks such as Next.js and Razzle are so popular: they abstract the SSR configuration aspect away and let you concentrate on writing UI code.

We are not going to use any SSR frameworks in this tutorial. By actually designing it, the only way to learn how something works is to build it, so we can learn how to construct the easiest SSR setup that we can possibly provide:

  • Global CDN
  • Entirely functional API backend
  • No servers or manageable networks
  • Single-Command Deployment 

We will deploy a universal server-rendered React application built on Amazon Web Services with create-react-app (AWS). To follow along, you don’t need to have experience with AWS.

We are going to make use of a few different AWS resources in order to develop our application.

  1. AWS Amplify: a high-level AWS services management system, specifically for mobile and web growth.
  2. AWS Lambda: Run code without handling servers in the cloud
  3. AWS Cloudfront (CDN): A service delivery network responsible for the worldwide distribution and caching of content
  4. AWS Simple Storage Service (S3): Where our static properties are stored: (JS, CSS, etc.)
  5. AWS Lambda: Run code without handling servers in the cloud
  6. AWS Cloudfront (CDN): A service delivery network responsible for the worldwide distribution and caching of content
  7. AWS Simple Storage Service (S3): Where our static properties are stored: (JS, CSS, etc.)

Architecture Diagram

The responsibility for server-rendering our React application is our Lambda function. We’re going to use S3 to store our static content and to support it using Cloudfront CDN. As AWS Amplify will make it super easy for us to build them, you do not need to have any previous knowledge of these services.

Building our application

First of all, you need to install the AWS Amplify CLI and create an AWS account if you don’t have one already.

Project Setup 

We can start setting up our React project now that Amplify is configured. To assist us, we are going to use the fantastic create-react-app. Assuming you have installed Node.js and npm, we will run:

Select the default options in the AWS Amplify wizard.

Our React project is now bootstrapped with Amplify and ready for us to add our “server” for SSR. 

This will generate the necessary models, files, and code that our AWS infrastructure and backend need: an AWS Lambda feature that runs a small Express server that is responsible for our React application rendering.

There are some adjustments that we need to make within our React framework to prepare it for server-side rendering before we deploy our infrastructure. Open src/App.js.js (the main app component for your React application) and paste in the following:

Next, we need to build a script on the server-side that will render our React app. In the react-dom/server kit, this is achieved with the renderToString feature. This feature is the responsibility of taking our component and rendering it as a string on the server-side, ready to be returned to the client as completely rendered HTML.

Create a file at src/render.js with the following code:

Now, our client-side React software has all the code on the server-side that needs to be made. This means that we now have to code the endpoint on the server-side that will make our React application.

However, we have a problem. We need to run the src/render function and our component code on the server-side. By default, the server knows nothing about React or even ES modules. For this reason, we are going to transpile the code from the React application using Babel into the server-side.

To do this, let’s install a few Babel dependencies into our project.

Next, create a .babelrc at the root of your project. This file is used to configure Babel and tell it which plugins/presets to use.

Finally, as part of the compilation phase, let’s update our package.json to transpile our code. This will transpose the files into the directory of amplify/backend/function/amplifyssr/src/client, where all the universal JavaScript that needs to be run on the client-side, as well as the SSR server, will be stored.

Rendering the app in Lambda

The build configuration is complete! Let’s jump into amplify/backend/function/amplifyssr/src and install react and react-dom, as they will both be required for the Lambda to perform SSR.

Now, to set up our Express server running on Lambda. When we completed the amplify add API phase earlier and selected a REST and ExpressJS API, the Lambda function was auto-generated for us.

The Express server has already been designed for us to run on Lambda by Amplify, so all we need to do now is add an endpoint to server-render our React application when anyone hits the browser API URL. Update your amplify/backend/function/amplifyssr/src/app.js file to contain the following code:

Our Express server is now SSR-ready, and we can deploy our React application.

Hosting and final touches

If we get the server-rendered HTML back from the initial rendering of our software, we can then fetch the JavaScript client-side package to take over from there and give us a fully immersive SPA.

We need to host our client-side JavaScript and static files somewhere. In AWS, S3 (Simple Storage Infrastructure), a massively scalable cloud object store, is the service commonly used for this.

We will also be putting a CDN for global caching and output in front of it. With Amplify, by running a few commands from our project root directory, we can generate both of these tools for our project:

By running the Amplify Publish command, you can now deploy your entire infrastructure, including the Lambda Express server, S3 bucket, and CDN functions.

Your console performance will view all the applicable tools that Amplify will build for you from your models. Please notice that it may take a while to generate a Cloudfront CDN, so be patient. Once your resources are created, your Cloudfront CDN URL will be displayed in the terminal.

The last thing we have to do is tell Respond where to get our client-side bundle from after the server makes the app. This is achieved using the PUBLIC URL environment variable in create-react-app. To look like the following, let’s change our React app package.json scripts again:

Rebuild and deploy your application to AWS with this updated configuration.

We should now have a fully server-side-rendered React app running on AWS!

Running our app

It is possible to find your SSR API URL at amplify/backend/amplify-meta.json. In your JSON file, look for RootUrl, and you should see the URL where you can visit your new server-rendered application. It needs to look more like the following:

Visit your browser’s API Gateway URL at <your-api-url>/ssr and you can see your genius new React server-rendered application! You will find that the request to /ssr has a completely rendered HTML response with our React framework rendered within the <body> of the document if you dive into the Network tab of your browser of choice and view the requests.

You will also note the requests from the browser to your Cloudfront URL to load the client-side JavaScript that will take over the rendering from here, giving us the best of both the rendering worlds on the client and server-side.

Next Steps

This tutorial is meant to get you up and running as easily as possible with server-side rendering without thinking about handling infrastructure, CDNs, and more. There are a few nice changes we can make to our setup having used the serverless method.

Provisioned concurrency

One way that AWS Lambda will stay incredibly low-cost is that “idle” can go to Lambda functions that have not been reached in a while. This effectively means that there will be what is known as a “cold start” when we run them again, an initialization pause that must occur before the Lambda responds.

After this, for a period of time, the lambda is then again ‘hot’ and will respond quickly to subsequent requests before the next long idle period. This can trigger reaction times that are slightly unreliable.

Lambda uses lightweight containers to handle any requests, despite being “serverless.” At any given time, every container can process only one request.

The same is also true when several concurrent requests hit the same Lambda feature, in addition to the cold-start issue after an idle time, causing more concurrent containers or staff to be cold-started before reacting.

A lot of engineers have solved this problem in the past by writing scripts to regularly ping the Lambda to keep it warm. There is now a much easier way of addressing this with AWS-native, and it is known as Provisioned Concurrency.

With Provisioned Concurrency, for a particular Lambda function, you can very easily request a specified number of dedicated containers to stay warm. This will give you a much more consistent SSR response time in times of high and sporadic load.

Lambda versions

For your features, you can build multiple Lambda versions and divide the traffic between them. In our SSR framework, this is very powerful as it helps us to make changes with a smaller portion of users on the Lambda side and A/B evaluate them.

You can publish several versions of your Lambda and break traffic by the weight you specify between them.

Full-stack web application

AWS Amplify already provides for us a REST API and an Express server, as explained previously, in which we have built an endpoint to server-render our React application. At amplify/backend/function/amplifyssr/src/app.js we can still add more code and endpoints to this Express server, allowing us to convert our app into a full-stack web application, complete with database, authentication, and more.

Even if it is not hosted on AWS, you can make use of the fantastic suite of AWS Amplify software to build these services or plug in your own infrastructure. You can manage and construct on top of your AWS Lambda backend like every other Express server.

By running amplify publish, you already have your entire deployment pipeline set up so that you can concentrate on writing code. In this tutorial, the starting point gives you complete freedom to do what you want from here.

Conclusion

Server-side rendering doesn’t need to be complicated. We can use completely controlled tools such as Next or Razzle, which are fantastic in their own right, but with their current code or specifications, this can be just too big a paradigm shift for many teams. Using a simple, low-maintenance, custom approach, especially if you are already using AWS or Amplify for your project, may make life easier.

SSR will add a lot of value and provide a much-needed output or SEO boost to your web applications. With a few commands or clicks, we are lucky in the web development community to have tools that can build CDNs, serverless backends, and fully hosted web applications.

Even if you don’t think you need SSR, in the JavaScript ecosystem, it is a very prevalent and common topic. For almost anyone interested in the web development sphere, getting an appreciation of its advantages and trade-offs would come in handy.