Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
In this tutorial, we will take a deeper look at the two types of rendering for web apps: server- and client-side. I will walk you through basic setup, testing, and comparison of two pages:
- SSR page (server-side rendered page)
- CSR page (client-side rendered page)
Once you understand the pros and cons of SSR and CSR, you will have a better framework to decide whether to use SSR or CSR for particular pages in your web application.
In this tutorial, you will run experiments using a basic React-Next-Express app. To clone this app to your local machine, run:
git clone git@github.com:builderbook/builderbook.git
Navigate to tutorials/3-end . Inside this folder, run yarn or npm install to install all packages. Start the app with yarn dev .
If you don’t have time to run the app locally, I deployed this app at: https://ssr-csr.builderbook.org
This tutorial has two sections:
- Setup of SSR and CSR pages
- Testing and comparison
If you find this article useful, consider giving a star to our Github repo and checking out our book where we cover this and many other topics in detail.
Setup of SSR and CSR pages
Here, I’ll give an overview of the setup for our SSR and CSR pages. If you want to dig deeper, check out the complete code for each page:
The SSR page code is at tutorials/3-end/pages/ssr.js.
The CSR page code is at tutorials/3-end/pages/csr.js.
To save time on the setup for server-side rendering, I use the Next framework to render a React page on the server. Internally, Next uses React’s server-side methods renderTostring() and renderToStaticMarkup() to render pages on the server.
On both SSR and CSR pages, we will display a list of items.
For the SSR page, we use the getInitialProps() method of Next with async/await to call and wait for a getList API method. The method getInitialProps() populates props of the SSR page component with data. We define the page component as a child of ES6 class:
class SSR extends React.Component { static async getInitialProps() { const list = await getList(); return { list }; } render() { const { list } = this.props; return ( // some HTML code that displays list of items ); }}
Note that the getList API method is a universally available method (located at lib/api/public.js), meaning it’s available to both client and server. Check out the code for this method. The method sends a GET request to an API endpoint and waits for a response with data:
export const getList = () => sendRequest('/api/v1/public/list', { method: 'GET', });
We discussed sendRequest in detail in our previous tutorial. If you want to learn more, check out the link or our book.
Let’s build the CSR page. For this page, we will use the lifecycle method componentDidMount(). We could’ve used componentWillMount(), however this method is in the process of depreciation. In addition to that, our component is a simple list. React renders it quickly, so the time gain from using componentWillMount() would be small. componentWillMount() gets called before rendering. Read more about both lifecycle methods in the official docs.
At first, we will render a CSRWithData component without data. Instead of data, we will show:
<p>loading...(CSR page without data)</p>
Then we will call componentDidMount(), which calls and waits for the getList API method to return data. Once getList returns data, we re-render the CSRWithData component by using this.setState.
class CSRWithData extends React.Component { state = { list: null, loading: true, }; async componentDidMount() { NProgress.start(); try { const list = await getList(); // console.log(list.listOfItems); this.setState({ // eslint-disable-line list, loading: false, }); NProgress.done(); } catch (err) { this.setState({ loading: false, error: err.message || err.toString() }); // eslint-disable-line NProgress.done(); } } render() { return <CSR {...this.props} {...this.state} />; }}
CSR child component that returns either loading UI or data:
function CSR({ list, loading }) { if (loading) { return ( <div style={{ padding: '10px 45px' }}> <p>loading...(CSR page without data)</p> </div> ); } return ( // some HTML code that displays list of items );}
Important note about CSR: similar to SSR, we will use async/await for componentDidMount (instead of getInitialProps) to call and wait for the same getList API method. This universally available getList API method runs on the server for a SSR page and on the client for a CSR page. Later in this tutorial, we will come back to this fact when we test the loading behavior of both pages.
Both pages are ready for testing. Before we test, let’s briefly mention the Express route that handles the request from the getList API method and sends a response that contains data (the list of items). Here’s the Express route with async/await (for more detail, see server/app.js):
server.get('/api/v1/public/list', async (req, res) => { try { const listOfItems = await list(); res.json({ listOfItems }); // console.log(listOfItems); } catch (err) { res.json({ error: err.message || err.toString() }); }});
And here’s the server-side method list that generates an array of 20,000 objects (for more detail, see server/list.js):
export default function list() { const n = 20000; const array = []; for (let i = 0; i < n; i += 1) { array.push({ name: `Item ${i + 1} of ${n}` }); } // console.log(array); return array;}
Time to test. Either clone our repo and run the app locally with yarn dev. Or navigate to https://ssr-csr.builderbook.org.
Testing and comparison
In this section, we will load the SSR and CSR pages and compare their loading behavior and metrics.
Our index page looks like this:
This page has two links in its Header. The links open in new tabs. Click the SSR link and observe the loading behavior:
Next, click the CSR link and observe the loading behavior:
There is a striking difference between SSR and CSR in terms of loading UX. In the SSR case, we see a completely blank page for a short period of time before the server-side rendered page with data arrives to the client. In the case of CSR, we see a client-side rendered page without data. We show a placeholder “loading..(CSR page without date” while the CSRWithData component waits for data and re-renders. Once data from the list is loaded, both pages look the same.
Thus, UX-wise, your choice is to either initially show a brief flash of a blank page (SSR) or, for a bit longer, a page with a loading placeholder (CSR).
To compare the loading time, we need to inspect both pages with Chrome Developer tools. Qualitatively, a list of 20,000 items appears noticeably faster on an SSR page, but we want to confirm is this is quantitatively true. In this tutorial, we inspect pages using Developer tools > Network and Developer tools > Performance.
For each page, do the following:
- Open the page
- Open Developer tools > Network while on the page
- Check the box Disable cache
- Reload the tab
Developer tools > Network for SSR pageDeveloper tools > Network for CSR
The CSR page makes just one more request (9 vs 8) than the SSR page. The client-side method getList sends this extra request to the server to fetch the list (see the last item of the requests for the CSR page). However, this request starts with ~1 sec delay, after main.js from Next.js finishes loading. In other words, the SSR page requires one less trip over the network and thus saves time.
On the other hand, the server takes time to render a page before sending a response, and the first response from the server already has data that takes time to upload/download over the network. This results in a delay. See the first item on the the list of requests for both SSR and CSR pages. I drew a sketch to summarize these observations:
High-level outline of req-res cycles for SSR and CSR pages
Another observation is the amount of total data transferred. For SSR, the total amount is 172KB. For CSR, it is 157KB. That’s expected, since the response for SSR returns both page HTML (+other data) + list data, and the response for CSR returns page HTML (+other data) only.
So far, we can make two conclusions. If the network is very slow, the extra trip over the network will be slow, and a SSR page will show list data sooner. The larger main.js will cause a longer delay for an extra request for the CSR page.
Developer tools > Network is useful for inspecting timing for req-res cycles but does not tell us when a user actually starts seeing our list of items. To measure this metric, we will use Developer tools > Performance.
For each page, do the following:
- Open the page
- Open Developer tools > Performance while on the page
- Check the box Screenshots
- Click the icon Start profiling and reload page
Developer tools > Performance for SSR pageDeveloper tools > Performance for CSR page
Take a look at the row with the sequence of screenshots. On your actual time traces, hover over with your mouse to find the time point at which our list of items first appears on the page. For the SSR page, it is 1320 ms. For the CSR page, it is 2760 ms. Previously, we only had a qualitative conclusion, but now we have a quantitative metric that confirms that our list of items appears sooner on the SSR page. This difference becomes larger if you try sending a larger list, for example 50,000 items instead of 20,000 items. In a real situation, your data (dynamic data, not page HTML and app bundle) is probably way smaller. Thus, testing for larger data makes little sense.
Let’s make actionable conclusions based on what we learned in this tutorial.
Consider using a SSR page when:
- The network is slow.
- The server has resources to render the page with data. Remember, Node is single-threaded and blocking.
- The delay before data shows up is short. In other words, the brief flash of a blank page is perceived well by users.
- SEO is important. Googlebot and other search engine bots properly index a SSR page.
Consider using CSR page when:
- The network is fast.
- The server has few resources to spare for server-side rendering.
- The delay before data shows up is significant. In other words, users need to see some reassurance that the page is load, for example a progress bar or loading spinner.
The choice does not have to be binary. In Next.js, if a page uses getInitialProps, then the page renders with data on the server for the initial load. However, for subsequent loads, when a user navigates to the page via <Link> or Router.push, getInitialProps runs on the client and the page is client-side rendered. Thus, the app uses the best of both rendering worlds.
In addition to the above setup, you may choose to use <Link prefetch> for navigation (read more about prefetch). This ensures that page HTML is prefetched in the background (though without dynamic data). If we used <Link prefetch> for the CSR link instead of the <a> tag (inside the Header component), then the list of items would appear sooner on our CSR page. Because HTML of the CSR page would be prefetched in the background, the getList method will be called sooner. As a result, a page without dynamic data would load almost instantly after a user clicks on the link.
If you learned something from this article, consider giving a star to our Github repo and checking out our book where we cover this and many other topics in detail.
Server-side vs client-side rendering in React apps was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.