If you have any questions or feedback, pleasefill out this form
Table of Contents
- Helping SEO and Search Engines Crawl Pages
- Generating Open Graph Content / Managing
- Optimizing User Experience
- Differences from Traditional Template Engines
- How Do Front-End Frameworks Achieve SSR?
- Common Misconceptions: Dynamic Imports and AJAX
- Is There a Significant Difference with or Without SSR?
- Alternative Solutions and Considerations
This post is translated by ChatGPT and originally written in Mandarin, so there may be some inaccuracies or mistakes.
In the early days, when front-end development was not yet mature and interactivity was less demanding, page rendering typically relied on template engines provided by back-end programming languages (notable ones include ejs
, pug
, erb
, thymeleaf
, etc.). This involved rendering HTML on the server-side and returning it to the browser, which would then use JavaScript for various interactions.
Although this approach seemed logical at the time, as browsers and technology evolved, a number of drawbacks emerged:
- Each time a user navigates to another page, a new request must be sent, meaning that HTML, JavaScript, CSS, and the current state all need to be reloaded and executed again (assuming no caching).
- The flexibility of the view is often limited by the syntax provided by the template engine.
- Sometimes, it's preferable for the view and interactions to be tightly coupled, and separating them can be inconvenient.
- As interactions become more numerous and intricate, a reactive mechanism is often required.
The evolution of Single Page Applications (SPAs) allows us to move all interactions and displays to JavaScript, making it easier to maintain code through componentization and reactive mechanisms. However, JavaScript alone cannot achieve certain tasks that traditional server-side rendering can, such as:
- Assisting with SEO and enabling search engines to crawl pages.
- Generating Open Graph content.
- Optimizing user experience.
Let's discuss each of these points in detail. Before diving into this article, readers unfamiliar with the concepts of SPA and SSR can refer to Huli's article, Understanding Technical Terms with Xiao Ming: MVC, SPA, and SSR. This section will focus on the benefits of SSR itself and its practical applications (using React as an example).
Helping SEO and Search Engines Crawl Pages
When a search engine crawls the web page, it will retrieve the HTML content, generate data from it, and cache it in its database for periodic updates. This means that without SSR, the HTML file itself might be blank, and users would have to wait for main.js
to be parsed and executed to see the actual page.
For example:
<html>
<head>
</head>
<body>
<div id="app"></div>
<script src="main.js"></script>
</body>
</html>
Although Google claims it can parse and execute JavaScript, the effectiveness is still limited, and it cannot perform actions requiring requests, such as fetch
.
Taking 17 Live as an example, when searching for 17 Live on Google:
In the search results, search engines typically display the <title>
and <meta name="description" content="xxx"/>
. This part can be generated directly by the server or hardcoded into the HTML file. Upon clicking the archived page, you'll find that the result is blank:
Upon further inspection of the source code, it turns out to be an HTML file with an empty body:
<!DOCTYPE html>
<html>
<head>
<title>17LIVE - Live Streaming Interactive Entertainment Platform</title>
<meta charset="utf-8">
<meta name="description" content="17LIVE brings live interaction at zero distance. Various unique talents share every moment of their lives; diverse program content available for free online viewing!" />
...
</head>
<body></body>
</html>
Generating Open Graph Content / Managing <head>
When sharing links on platforms like Facebook, Twitter, or LINE, crawlers will send requests to the link's URL and parse the <meta>
tags to determine how to display the preview (see Open Graph Protocol for details). This data must be returned from the server; otherwise, crawlers will still see blank content.
In React
, implementing content to be placed in <head>
can be achieved in several ways:
- react-helmet: Allows you to manage content in
<head>
through components and also implements server-side rendering. - next/head: Next.js also provides a built-in way to manage
<head>
.
Optimizing User Experience
When a webpage is parsed, it first receives the HTML and then parses the JavaScript. Until the JavaScript execution is complete, content cannot be seen. While the difference may not be significant on mainstream computer devices (CPU i5 and above), here are a few considerations:
- Users might not always browse on computers or smartphones; they could use IoT devices, e-readers (like Kindle), PS4, TVs, etc. These devices often cannot parse JavaScript and have limited performance.
- Not every user will have JavaScript enabled (though this is a relatively small number). If users encounter a blank page, they may leave and miss potential engagement.
- With the help of SSR, the framework can match content as JavaScript executes, saving the performance cost of invoking
document.appendChild
and other DOM APIs.
Differences from Traditional Template Engines
When performing SSR with front-end frameworks like React and Vue, several differences from traditional template engines become apparent:
- The result rendered by traditional template engines is a pure static HTML string, with variables injected by the server; in contrast, front-end frameworks render HTML strings on the server and dynamically call DOM APIs on the client side, injecting corresponding event listeners (click, change, etc.) and executing lifecycle methods after SSR rendering.
- Traditional template engines do not have a reactive concept, meaning they do not automatically update the DOM when variables change.
- Since the rendered HTML must match the front-end code, both front-end and back-end code need to share (when rendering HTML). Therefore, SSR in front-end frameworks needs to be used alongside Node.js; traditional template engines do not necessarily require this and can be determined based on the back-end programming language.
How Do Front-End Frameworks Achieve SSR?
We will not discuss the implementation details of specific frameworks here. Instead, we will introduce how mainstream front-end frameworks achieve SSR.
For front-end frameworks to perform SSR, the first consideration is state. Given the assumption that the same state will render the same view, to achieve a consistent initial screen between the front-end and back-end, we need to ensure that both sides reach the same state when rendering HTML.
Generally, we prepare a global store during server rendering:
route.get('/', (req, res) => {
const store = {
posts: [],
user: {
name: 'kalan',
...
},
};
const html = ReactDOMServer.renderToString(<App />);
res.render('index', {
html: html,
store: JSON.stringify(store);
})
});
Then, we place the store data in a global variable:
<div id="app">
<%- html %>
</div>
<script>
window.GLOBAL_STORE = store;
</script>
Finally, in app.js
, we write:
ReactDOM.hydrate(<App store={window.GLOBAL_STORE} />, document.getElementById('app'));
Here, we utilize the hydrate API to inform React that we have pre-rendered the content from the server. React will skip the DOM API process and begin adding corresponding event listeners and executing lifecycle events and useEffect
, etc. It's important to note that "SSR" only refers to the initial rendering (the HTML received after the request).
As the App grows in complexity, preparing a global variable may not be the best idea. At this point, it's worth considering frameworks like next.js
to help simplify the issues and complexity related to SSR implementation.
Common Misconceptions: Dynamic Imports and AJAX
As the scale of an app grows, the characteristics of SPA—where views and interaction logic are entirely on the front-end—can lead to a critical bundle size that impacts initial load performance. At this point, you can use dynamic import mechanisms to split less critical components or pages into separate files, requesting them only when needed.
Currently, React's React.lazy
does not support SSR. For SSR guidelines (using loadable-components), refer to the official SSR Guide.
The current approach of loadable-components
involves collecting instances of dynamic imports used within the <App/>
component to generate a manifest file, then loading these files using <script src="xxx.js">
. If components on the page are all rendered through dynamic imports, the initial rendering result may still be blank (as the files must still be loaded via <script>
).
Moreover, if data is fetched on the front-end using AJAX without fetching data on the server, the initial rendering will also be blank or display a loading screen. For example, consider a simple component like this:
const App = () => {
const [data, setData] = useState([]);
useEffect(() => {
fetch('/api/data').then(res => res.json())
.then(data => setData(data));
}, [])
if (data.length === 0) {
return <span>loading</span>
}
return <div>
{renderData()}
</div>
}
When React performs SSR, it does not execute the code within useEffect
, simply rendering the server's data into the component and producing markup. The actual request will only be sent once the browser renders the page. Thus, the rendered code might look like this:
<span data-reactroot="">loading</span>
For SSR, this kind of page may reduce some of the initial rendering performance burden, but it is not ideal for SEO and user experience. After all, one of the purposes of implementing SSR is to enhance user experience (avoiding loading screens) and improve SEO (ensuring the browser sees an actual page rather than a loading message). A better approach would be to use a global store to send server data to the front-end or leverage the getStaticProps()
mechanism in next.js
for assistance.
Is There a Significant Difference with or Without SSR?
This largely depends on your perspective on SSR and the specific use case. For instance, in the case of 17 Live, many pages are primarily dynamic video content; the benefits of implementing SSR may be limited, and the conversion cost could be relatively high, which is one consideration.
Additionally, when developing back-end systems where most users are internal company personnel, SEO considerations may not be necessary, and we can assume most users have devices with good performance. Therefore, it may not be essential to implement SSR. There are many factors to consider when implementing SSR, and if it was not considered from the start, the conversion cost could increase over time. Thus, it's best to evaluate whether to introduce SSR early in the development process.
For applications like blogs, e-commerce sites, article viewing platforms, and homepages—where SEO is a significant concern—SSR should be a key consideration.
However, rather than debating the necessity of SSR, I believe it's more productive to approach the discussion from the perspective of user needs. After all, the primary goal of any technology or product is to serve humanity.
For example:
- Users may access our websites through e-readers, IoT devices, or lower-performance equipment, so we need to minimize unnecessary performance waste.
- Users may use this service to achieve specific goals, necessitating optimization of processes or design, and enhancing the experience through browser and JavaScript support.
Alternative Solutions and Considerations
Here, we won't delve into so-called best practices; instead, let's consider how to apply this within practical scenarios and constraints.
- If introducing SSR directly is not cost-effective, perhaps the back-end can provide a simplified version of the view for crawlers and use JavaScript rendering when users browse the site. Although this may require maintaining two different views, sometimes it could be simpler.
- If the content remains the same each time, static HTML can be generated directly.
- If the conversion cost is high, you can use puppeteer or similar headless Chrome tools to browse the web and cache the rendered HTML on the server side. This can be updated on a scheduled basis.
- Using SPA often means that the complexity of front-end JavaScript and bundle size will inevitably increase, so sometimes opting for traditional rendering methods combined with a
prefetch
mechanism may yield better performance and experience.
If you found this article helpful, please consider buying me a coffee ☕ It'll make my ordinary day shine ✨
☕Buy me a coffee