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
Which CSS strategy is busy.org using? I have some issue with SSR when I use
styled-component
alongside with React, the css just won't show up.We use .less files (due to the fact we use
antd
and we have to use Less to modify it), and just use webpack import's to import them. After we build our app, we have .html files that includes all stylesheets. This file is then used as a template on the server.If you want to use
styled-components
on the server you have to wrap your entire application inServerStyleSheet
collector that would collect all of your styles from components. After it's done you can just retrieve normal style tags from it and add it to your page. Take a look here: https://www.styled-components.com/docs/advanced#server-side-renderingI had been using sass for a while. Transition to
styled-component
took me sometime to get used to it haha.I like the way styled-jsx works. It feels like normal CSS but has some great features (scoped to current component, works on SSR).
If you are working on new app and want to have SSR I recommend checking out Next.js.
Currently I am still using Express or Koa for SSR haha.
I am also looking on this jss where Material-UI is using this.
It's incredible to me, that such thing like implicit conversion could speed up our lovely Busy that much!
Have you encounterd any other 'performance hacks' during process of implementing SSR?
I've noticed couple of potential improvements that we could make to make rendering faster. I didn't implement them yet, because it won't make much of a difference. Currently the slowest thing on the server is actually Steem API. It can even take more than a second to fetch data (for example post contents). Rendering our app doesn't take that much compared to that.
We plan to migrate to our own API, so we can fetch data in exact format we need. In addition it should be way faster than Steem API, so after we migrate to our own solution we can work on improving the performance even more.
The most obvious one I notice (and it is worth considering no matter if you are running React server-side or client-side) is to process and render data only on-demand. Take sorting votes as an example. In feed you have 20 posts, sometimes they have more than 10 thousands of votes. Does it make sense to sort them right now, even though user might not even consider taking a look at them? I don't think so. It makes sense to do this when we know this is necessary - when user opens votes list. We can show loading icon for a while and present user a final result.
This is very simple change, but can make a difference. You might have slow-running tasks, but if you don't run them (unnecessarily) every time user visits a website it's not big deal.
Yeah, definitely agreed. 'Caching' things that are uncritical to the current demands of user are is strongly unefficient - in the big picture, obviously.
I imagine that migrating an API to your own is going to be quite a challenge. But getting under consideration all the things you've already done I think you will manage :)
Nonetheless, thank you for answer! It's very satisfying to get answers about the Busy straight from its developer.
There is always a room for improvement. We are constantly working to make Busy and entire platform even better.
Glad you liked it, see you soon 😄
Thanx sekhmet for sharing valueable informations.. Nice post.. Keep it up
Very best tutorial
Thank you for the contribution. It has been approved.
You can contact us on Discord.
[utopian-moderator]
Hey @sekhmet I am @utopian-io. I have just upvoted you!
Achievements
Community-Driven Witness!
I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!
Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x