Introduction
If you are a developer, especially one working in Front-end development, you probably know that the most modern way of building applications today is by using Server-Side Rendering, commonly known as SSR. This approach significantly enhances your site's indexation (SEO) across key market players.
As with everything in programming, SSR has its pros and cons. For you, the developer who may only be familiar with its advantages, I decided to list some of its disadvantages as well.
Understanding SSR
First, let’s understand why the need for server-side rendering emerged and why it’s now uncommon to see applications being built with Client-Side Rendering (CSR).
No, Next.js was not the creator of SSR. In fact, long before frameworks like React, Vue, and Angular existed, the web relied on either static rendering or server-side rendering. Let’s look at a few examples:
- Static Sites: HTML, CSS, JavaScript, and jQuery: These sites were entirely rendered in the user's browser. All the information appeared instantly on the user’s screen, and the browser executed the scripts embedded in the site to populate the screen. When you inspected the code, you would see something like this:
What user and you see
<html>
<body>
<h1>List of best players of the world</h1>
<script>
async fetchListOfBestPlayersOfTheWorld() {
const res = await fetch(...);
const data = res.json();
// loop to add content in html
}
fetchListOfBestPlayersOfTheWorld()
</script>
</body>
</html>
First Content Paint (FCP)
- Server-Rendered Sites: PHP or JSP: These sites were rendered by the language interpreter and displayed the result on the user’s screen. This meant the information was already filled in when displayed. If you inspected the code, you’d see something like this:
What you see:
<%
List<PlayerDTO> bestPlayersOfTheWorld = APIService.getBestPlayersOfTheWorld();
%>
<html>
<body>
<h1>List of best players of the world</h1>
<ul>
<% for (PlayerDTO player : bestPlayersOfTheWorld) { %>
<%-- loop to add content in html --%>
<% } %>
</ul>
</body>
</html>
What user see:
<html>
<body>
<h1>List of best players of the world</h1>
<ul>
<li>Cristiano Ronaldo</li>
</ul>
</body>
</html>
First Content Paint (FCP)
It’s clear that both types of sites achieve the same goal, but the static site doesn’t render the list until it’s loaded in the browser, unlike JSP, which makes the API call on the server, renders the page in the interpreter, and only then delivers it to the user.
From this, we can conclude that Next.js didn’t invent SSR. This approach has existed for years across many languages like PHP, Java (using JSP or other frameworks), ASP.NET, and more.
The Chaos
When React, Vue, and Angular became popular, there was a shift in the market. Nobody wanted to write countless lines of code anymore to create a basic site using jQuery, JSP, PHP, etc. Why not use these modern tools instead?
And so, we humble developers abandoned those old tools and started using React, Vue, and Angular to build our websites. Now, Front-end development had a single responsibility: creating pages and reusable components while reducing the load time we used to experience with tools like jQuery and Bootstrap.
Let’s explore the advantages these tools offered:
- Code reuse that brought more readability and longevity to projects.
- Complete builds that eliminated unused code and improved page load times.
- CSS compilers that removed unused styles and added cross-browser compatibility.
Pretty cool advantages, right? I agree. But we had three major issues:
- These tools were WORSE than static sites in terms of SEO:
Despite being similar to static sites, the modern tools didn’t write your code directly into the HTML. Instead, your site relied on JavaScript generated by library (called the bundle). After being downloaded, this bundle would inject your code into the site. For example:
With these tools, we wouldn’t even see the "List of Best Players of the World" until the browser had finished downloading the bundle and executing the script, as shown in the example below:
Your code
const ListOfBestPlayersOfTheWorld = () => {
const { data: players } = useAPI(api.listOfBestPlayersOfWorld);
return (
<>
<h1>List of Best Players of the World</h1>
<ul>
{players?.length && players.map((player) => {
// loop to add content in html
})}
</ul>
</>
)
}
What user see
<html>
<body>
<script src="/assets/bundle.js"></script>
</body>
</html>
First Content Paint (FCP)
- The build file size could exceed 100MB, causing both download and load-time slowdowns:
In combination with the previous issue, if your site was very large, the generated bundle could become excessively large, causing delays for users and potentially being slower than loading the full jQuery script.
Furthermore, these tools relied on internal routing, meaning the bundle would load information for all pages, even if the user was only on the homepage, for instance.
One of the solutions to address this problem was the use of Lazy Loading, which delayed content presentation for the user but broke the code into smaller secondary bundles. Additionally, CDNs with caching were employed to ensure that users would only load the bundle once (with the cache being revalidated when changes were made to the site).
However, depending on the size of the primary bundle, these solutions still didn’t fully resolve the issue.
- Excessive Use of JavaScript, neglecting native browser solutions (a topic addressed in the HTML-first approach):
How 'reacters' do
const ModalComponent = () => {
const [open, setOpen] = useState(false);
const onToggle = () => {
// using useState that render after each change
setOpen((open) => !open);
}
return <Modal open={open} onClose={onToggle} onOpen={onToggle} />
}
How 'html-firsters do'
const ModalComponent = () => {
const ref = useRef<HTMLDialogElement>(null);
const onOpen = () => {
// using native solutions, no rerender
ref?.current.showModal();
}
const onClose = () => {
// using native solutions, no rerender
ref?.current.close();
}
return <dialog ref={ref} />
}
The solution to the problem we created
Don’t be too upset with these tools—they genuinely improved the lives of front-end developers. However, certain issues started to grow too large to ignore, such as oversized bundles that led to high CDN costs, usability problems when JavaScript failed due to state management issues, and non-existent SEO, which negatively impacted site indexing.
That’s when we decided to take a step back and revisit the Server-Side Rendering (SSR) approach, using tools like Next.js, Nuxt, Qwik, Gatsby, and @angular/ssr.
Now that you’ve learned a bit about how Client-Side Rendering (CSR), Server-Side Rendering (SSR), and Single Page Applications (SPAs) work, I’ll share my perspective as a senior software engineer on this approach, along with its advantages and disadvantages:
Advantages:
- Effective SEO:
Since the site uses the server, you can configure all metadata () and load all or most of the site’s information before rendering it in the browser. This generates the First Content Paint, which is the moment when the indexing bot "takes a snapshot" of your code and extracts information from the HTML to index your page on search engines.
- Performance:
There’s a trade-off between performing tasks on the server or delegating them to the client. If you choose the client-side approach, the user’s experience might be negatively affected, as it demands high computational resources and can cause issues if they have a poor internet connection or weak hardware. It also increases the size of the bundle.
On the other hand, opting for server-side tasks improves user fluidity since the server handles the heavy lifting. This avoids overloading the user's browser and prevents generating a larger bundle.
- Server Network Optimization:
If you make API calls within your domain, particularly within your private network (VPC), you’ll get faster responses because you can bypass much of the DNS resolution process.
When making a server-side call, instead of calling https://api.mydomain.com, you can directly call the internal IP or DNS of the service, reducing response time. This is because there are fewer DNS resolutions to reach the final address. Depending on the endpoint called, this can also reduce cloud costs (such as Route 53, Cloud DNS).
Note: This approach only works for server-side calls where your application is in the same network and region as your service. If attempted on the client-side, you must ensure users are part of the same private network, such as via a VPN.
- Client Network Optimization:
If the user does not have a very fast internet connection, performing calls and processes on the server-side will save time for the page to load completely for the user.
- Security:
Since your code runs on the server, you ensure that sensitive code or information—such as environment variables or internal logic—is not exposed to attackers. This adds an extra layer of security to your application.
Disadvantages:
- Costs:
If a server exists, someone is paying to keep it active. Server-Side Rendering can be expensive, depending on the complexity of your application, generating resource costs in the Cloud and, for more advanced cases, entire infrastructure costs to maintain it scalably, such as the use of Load Balancers.
In programming, trade-offs are always present, and in this case, you need to analyze whether the advantages compensate for the cost.
- Static Files
Even when using a server-side rendering approach, some frameworks can generate static files to be loaded in the browser. For example, Next.js. If this is your scenario, you must decide whether to use the front-end server itself to serve these static files or host them on a CDN.
If you choose to let your server serve the static files, remember that if your application has 5 static files, each request will result in 5 additional requests to load those files. Some frameworks include features for caching these files to prevent users from downloading them every time they access your application. However, this scenario should still be carefully considered.
- Infrastructure:
Complementing the previous disadvantage, using this approach requires more cloud knowledge in certain scenarios, as you'll need to maintain an infrastructure for the application to function. If you adopt this approach, you'll likely encounter the following costs:
- Container image storage
- Load balancers
- Container orchestration
- Machine instances
- Networking
- CDN
If you misconfigure the application or allocate insufficient resources without considering the expected load, you'll likely end up with an unstable application that provides a poor user experience, since the server renders the content.
- Visual Bugs:
When using a framework that employs HTML hydration (e.g., Next.js), you may inadvertently introduce production bugs if you're not careful with application hydration. You must always ensure that the server is synchronized with the client and guarantee proper control and hydration synchronization between them.
The Tip
After providing this entire context to help make more assertive decisions, I'll share my approach to choosing languages, frameworks, and strategies for my applications:
This flowchart takes into consideration a front-end that will communicate with a REST backend, with no delivery deadlines, and will be hosted on a cloud platform such as AWS, GCP, or Azure, without delving into detailed questions.
Note: These are basic questions and cannot answer all scenarios.
Top comments (3)
Amazing post. Thanks for sharing your knowledge.
Nice post! That's probably the main reason why React removed 'create-react-app' from the official documentation and Next became the primary tool for creating new React projects.
thanks for the post.