This post contains a little bit of story of how I handled Server-Side Rendering and some tips that I wish I knew before starting to work on this. If you are interested in implementing SSR (Server-Side Rendering) you might want to read it, if not you can just ignore that. A condensed list of stuff I did is at the bottom of this post.
Server-Side Rendering
As probably most people know, we use React (with Redux and other great libraries) to build website behind Busy.org. React is a great framework for building client-side applications in JavaScript (and it's pretty common in our ecosystem - Steemit and Utopian are using it as well). Problem with client-side applications is that they are rendered entirely in the browser, so they perform worse at SEO (Search Engine Optimization) than traditional websites. Computers don't tend to run fully-fledged browsers just to figure out how does it look like for users and just read it as rather an empty HTML file. This was a bummer for us. Our goal is to build a platform for content creators, that they can use to share their work. However, sharing was always a problematic aspect of Busy - when you share a link to your Busy article on some social platform (Twitter, Facebook, Telegram, Messenger, Discord et.al), instead of a preview of your work you would get just generic message that most likely won't bring a wide audience.
One of our priorities lately was to fix this issue, so users could share their content without any obstacles.
To make it a little bit easier to understand you can check those awesome illustrations from equally awesome article from Walmart.
SSR
CSR
Just a few years ago the only solution was to use some sort of service that would visit your website periodically, render HTML code and serve that instead of your normal website if you detect that it's visited by some kind of bot (Google, Facebook etc.). Luckily, with recent work made by React community, React can be used in non-browser environments such as Node.js.
One challenge of running React on the server is that if you don't handle some kind of error properly, you don't only crash the website in user's browser, but the most likely entire server, effectively blocking thousands of users from accessing your website. For this reason, we had to prepare our code to handle potential unhandled errors and find bottlenecks.
One of such bottlenecks that got fixed was the way, we pared numbers while sorting votes. Previously, we used parseInt
to parse rshares
to Number, but it's really slow compared to converting it implicitly. After the change instead of sorting votes for whopping 1.3 seconds every time a new page was loaded in the feed it only took relatively small 70ms. Not fixing this bottleneck could potentially increase response time by 1 second, so fixing this way an obvious choice.
Another thing that is worth doing is to upgrade React to the latest version - React 16. It not only has way better support for SSR, but it works way faster in the browser as well. If you want to read more about what has changed in React 16 you can read this article on React blog.
Upgrading React should be fairly easy as long as you use frequently updated dependencies. Built-in React.PropTypes
was removed and now you have to use separate prop-types
package instead. We already were using it in our codebase, however, some of our libraries were not updated recently, so we had to either use something else or fork it and update it ourselves, which is what I effectively did for ReduxInfiniteScroll.
After those preparations, we were ready to start implementing the logic on the server. Rendering React app on the server is simple thanks to renderToString
method from ReactDOM
. It works almost the same way standard render
method works. You potentially could have functional SSR implemented in few lines of code, but if you are using dynamic data (e.g. loaded from API) it just won't be there. The reason why is that renderToString
doesn't wait for your Promises to fulfil. It just renders document tree and returns it. If you want to use asynchronously loaded content - you have to handle this yourself.
Few things to note:
componentWillMount
is the only lifecycle method that would be called on the server. If you are loading data here you can't really access it. If you do - it will just use your server resources for nothing.- If you want to load some content on both server and client you should do it in
componentDidMount
(it get's only called on the client), and in some static method that can be called on the server. - You probably want to have some kind of global store to save your state. You could use global variables for this, but Redux makes everything way easier.
Almost everything we had inside componentWillMount
could just be moved to componentDidMount
. If you load some data asynchronously inside componentWillMount
there is no way that it will load before component gets rendered.
If we want some component to load data asynchronously, we add static fetchData
method that does just that. Those methods return promises, so we can wait for it to complete using Promise.all
- it either resolves when every promise is resolved or rejects when one of them is rejected. When Promise.all resolves we can feed our store that got created when loading data to our React app and render it to a string using renderToString
.
The last step is just to turn that string into the full website, add additional markup and pass an entire store, so it can be retrieved on client-side so you don't notice any inconsistencies.
If you want to see PR that added SSR click here. Keep in mind it's quite big because we decided to split our app into three folders: client
, common
, and server
.
List of all contributions in November and December:
- Update TopicSelector styles
- Remove storybooks
- Add babel-polyfill
- Use production and development index files
- Remove not used images from assets
- Merge previous jsonMetadata when saving post
- Add voting slider to comments
- Display image preview for short posts with image
- Image upload validation
- Fix StoryFull actions: save and edit
- Handle deleted posts
- Improve votes sorting
- Use proper steps in Dockerfile build
- Update Busy to React 16.1.0
- Remove parseInt from sortVotes (#1025)
- Server-Side Rendering
- Add Sentry's Raven for error handling
- Release 2.1.0
- Update package version
- Add HeroBanner
- Respect stickPosition in Affix calculations
- Don't load avatars on server
- Update error parsing
- Add feed fetched property
- Check if LOGIN is a refresh
- Try to load real avatar by default
- Log initial page view
- use targetUsername instead of username in userActions
- Migrate to new image provider
- Update error parsing (#1143)
- Log initial page view (#1162)
- Migrate from Steemjs to LightRPC
- Hotfix translations
- Server-side rendering for feed and proper error handling
- Fix all feed
- Add AMP
- Make posts loading state scoped to certain post
- Disable curation rewards on comments
- Update posts state from feed
- Use yarn
- Save reward and upvote settings in metadata
- Load translations asynchronously
- Release 2.2
- Don't update post state after liking. Fixes #1222
- Don't update post state after liking. Fixes #1222
- Update font style
- Increase Feed load threshold to 1500
- Display text-image if there is image in metadata. Fixes #1224
- Include rate in vote value calculation
- Add noindex, nofollow if not running at busy.org
- Add development scripts
- Don't index feed pages
- Don't broadcast comment_options when updating
- Update username style
- Support iframe for AMP pages
- AMP error handling
- AMP error handling
- Improve server building process
- Remove trailing comma
- Add vote value to user profile
- Updates styles
- Reformat codebase
- Profile settings
I think I should create script for generating those ^^
This was supposed to be monthly post, but I was short on time (I'm currently full-time student). When I got some free time it was already 15th, so I decided to create one post for two months.
Thanks for everyone in a team and for every Busy user that helps make us something really big. I appreciate your help!
Posted on Utopian.io - Rewarding Open Source Contributors