This content originally appeared on Bits and Pieces - Medium and was authored by Fernando Doglio
A look behind the curtain at what servers like Next.js and Remix are doing for us
As part of my learning process I like to challenge myself to reverse engineer logic that seems to be doing “magic” or at least, acting as a black box when we’re using it.
I’ve done it with the reactive behavior of React, and I’ve also done it with bundlers, now it’s time for Server Side Rendering.
In this article, I’m going to show you how you can write an SSR framework that allows for static site generation and dynamic content. As an added bonus, I’m also going to showcase a potential way of mimicking Remix’s data loading flow.
Disclaimer: Then end result of this article is not going to be a production-ready framework, but rather a starting point for you to keep going if you want. The focus will be on the learning, not on the code.
What is SSR?
SSR or Server Side Rendering is the process through which all your HTML is rendered on the server (no surprise there!) and then delivered back to the client.
If you’re thinking about a static website where you directly write all your HTML into .html files, then this is really nothing special. But, if you’re dealing with modern websites, where there is a lot of dynamically generated HTML using JavaScript, then a normal server would work like this:
- It would deliver the existing HTML
- It would deliver all the JS you wrote
- It would cause your browser to parse and run the JS
- The JS would generate the missing HTML right in front of you.
Now, mind you, there is nothing intrinsically wrong with this approach, it’s been working just fine for the past decade, however, there are a few drawbacks in some situations, especially those that relate to a slow internet connection on the client-side (essentially, when you as a user, have a slow internet connection) and a big and JavaScript-heavy application on the server side.
When that happens, you end up:
- Transmitting a lot of bytes in the form of JS code to your client (who are suffering from a slow connection)
- You’re delaying their time-to-interaction, given the fact that they have to wait for that JS to be downloaded and parsed.
- The device they’re using will have to parse and run all that JS. This is especially relevant if the device is a mobile device.
Taking those 3 points into account, you can see how many people can claim that not all bytes are alike. In fact, JavaScript bytes are considered the most expensive bytes to transmit from server to client (in this interview with Luca Casonato, he explains this to be one of the main reasons for the creation of Fresh). This is because they can cause:
- Extra money spent on a mobile connection.
- Extra battery drain during parsing and running of the code.
And you can avoid all of that, or rather, most of that, by using SSR!
SSR will run the JavaScript code that generates that extra HTML on the server, and it will only deliver HTML to your client application. That way you save a lot of bytes and processing time to your clients. You also improve the time-to-interaction, because the browser only needs to show the HTML, and nothing else.
What do we need to build our own SSR?
It’s actually not as hard as you would think, at least not for what I’m going to be doing, I’m sure frameworks like Remix, Next and others are doing a lot of more complex things, to achieve more complex behaviors.
But for the time being, our main dependency is going to be Puppeteer.
Puppeteer allows you to use a headless (optionally) chromium browser to go through your pages, interact with them and even capture the HTML that the browser receives.
So essentially, what we’re going to be doing is:
- We’ll create a basic web server using ExpressJS.
- We’ll create a script that visits every page of our server with puppeteer.
- We’ll capture the HTML the headless browser gets from the server, and we’ll save it as .html files somewhere public.
As an added bonus, because I wanted to make this more interesting, we’ll also look at how to provide some data loading capabilities to our web server that resembles Remix’s loader function.
So let’s get down to building this thing.
Did you like what you read? Consider subscribing to my FREE newsletter where I share my 2 decades’ worth of wisdom in the IT industry with everyone. Join “The Rambling of an old developer” !
Building our web server first
Alright, so our server is going to be simple, yet, somewhat powerful.
We’re going to implement:
- Path-based routing. This means, the place where we place our files will determine the route. This will be very basic, we will not implement sub-folder support. This is to keep things simple, since they’re not part of what we’re trying to achieve here.
- All GET requests to our routes will be handled by an exported function called handler .
- All POST requests to our routes, if required, will be handled by a function called data .
- We’ll provide a function called useData to use the data gathered by our data-loading function (the data function).
To understand how this all works together, let’s take a look at the content of the file located inside /pages/users.js , which translates to http://localhost:3000/users :
As users of our “framework”, all we need to remember is that our main function to export is going to be handler and that if we need to load external data, we’ll also have to define and export the data function that returns the result of the request.
To use the external data, we have the “hook” useData and that’s it.
As you’ve probably imagined, the “magic” behind this, is that our useData hook is actually sending a POST requests to our route, and the data function is in fact, the handler function for our POST requests.
Let’s build the server
With this understanding, let’s now review the server’s code:
The server is very simple, it takes advantage of Express to avoid having to deal with the boilerplate code of building a web server, which in Node.js is not that difficult. Then it configures the CORS headers, and makes sure that any content served from within the /public folder is considered static.
Finally, it defines two handlers, one for GET requests and the other one for POST request. They both do pretty much the same thing:
- They get the :name attribute from the URL (essentially, the route you’re trying to visit)
- They try to dynamically load the corresponding file inside the pages folder using the name they got from the URL.
- If they succeed, then they’ll either use the handler method, or the data method from the module they just imported, depending on whether it’s a GET or a POST request.
Additionally, the GET handler adds an html method to the response object (see line 16). This might not be the prettiest way of doing it, but this way we can then call resp.html and return a full page back to the client, even though we’re just writing a portion of it in our route.
This is the renderHTML function, and it looks like this:
It’s more syntactic sugar than anything else, but it’s a lot nicer to go resp.html(...) than to have to write that code everywhere.
And that’s it, we now have our server ready. But where is the SSR?
Nowhere… yet. Let’s add it.
Adding our “build” step
Alright, here is where the “fun” starts.
The renderer, as I’ve called it, is going to inspect the content of the pages folder, and for each file, it’ll send a request to our server using the Puppeteer package.
Once we get the HTML back, we’ll save it inside the dist folder located in our public folder.
Let’s take a look at the script:
The “magic” isn’t here, but rather inside the ssr.js file. However, here we get to see the overall logic of the process.
Let’s see how I’m using Puppeteer:
This package is quite easy to use, which is a great thing for us!
All I’m doing is:
- Launching the headless browser (line 9)
- Opening a new blank page (line 10)
- Visiting the URL I received with that page (line 11)
- Waiting until the body is loaded (line 12)
- Adding a data-rendered attribute to the body (lines 13–15). This is to signal this HTML was server-side rendered. This way if we wanted to inject some JavaScript to run on the client side, it would be able to tell when the content was pre-rendered (prod) or when it was running in dev mode.
- Get the HTML of the page (line 17)
- Close the browser (line 18)
- And finally, return the HTML (line 19)
That’s it, I think the only “complicated” step would be step 5, because it’s doing something extra, and totally optional. It’s just a simple flag, or watermark, if you will, that we leave inside the code to make sure we know when it’s been server-side rendered.
The output from executing this script looks like this:
And the HTML generated and saved inside /public/dist/users.html is this:
Notice the data-rendered attribute on the body tag. And also notice that there is no JavaScript to be seen. Now, this is an extreme case, since we could also want some JS in our build. Then you could take the code for the rendered and use it to inject some custom JS code into the generated HTML. That code could check for the data- attribute on the body and determine what to do then (like reloading the data after some time by sending another POST request to the proper path).
Well, that’s IT.
As I always say, once you understand how these things work they stop being so magical. Granted, this SSR server is not super useful as it is right now, but it’s definitely a good first step in the right direction. You can take this code and adapt it to whatever suits your needs.
Have you ever implemented SSR using something else? Share your experience in the comments section so we all learn from each other!
Build apps with reusable components like Lego
Bit’s open-source tool help 250,000+ devs to build apps with components.
Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.
Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:
→ Micro-Frontends
→ Design System
→ Code-Sharing and reuse
→ Monorepo
Learn more
- How We Build Micro Frontends
- How we Build a Component Design System
- The Bit Blog
- 5 Ways to Build a React Monorepo
- How to Create a Composable React App with Bit
Demystifying Server Side Rendering: Let’s build our own SSR server with Node.js was originally published in Bits and Pieces on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Bits and Pieces - Medium and was authored by Fernando Doglio
Fernando Doglio | Sciencx (2022-11-28T07:32:02+00:00) Demystifying Server Side Rendering: Let’s build our own SSR server with Node.js. Retrieved from https://www.scien.cx/2022/11/28/demystifying-server-side-rendering-lets-build-our-own-ssr-server-with-node-js/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.