I've spent the last couple of years developing an SSR application with Next.js and Apollo. The project features a mixture of job boards, a large amount of content (from a CMS) and user profiles. I thought I'd share some of the challenges, mistakes and thing's I wish I knew before I started.
Some of these to you might be quite obvious, however what I listed here was really important to the success of our project. Hopefully it will be of use to some.
tldr;
- Do you really need to SSR?
- Monitor Performance Early
- getInitialProps Optimisation is Vital
- Apollo Cache Key Optimisation is Vital
- Watch Out for Styled Component Nested Props
- async/await in getInitialProps
- Consider Building a Version Notification System Early On
Do you really need to SSR?
The first hurdle was determining if we really needed server side rendering. Obviously there are many pros and cons to analyse. It's really important to weigh these options and understand the trade-offs between a static, dynamic or server side build.
For us, SEO was a determining factor. We needed to display source HTML for search engines, as well as managing meta and redirects. Alternative methods, such as pre-generated content just wasn't viable, as we had huge amounts of multi-tenant content that's published and changed frequently.
You should ask yourself if the benefits of SSR (eg: SEO, request/response control) outweigh the cons (eg: time to first byte, complexity, server infrastructure).
As a general guideline, I try to favour static generated sites for performance reasons. Study getStaticPaths
to see if it can work for you.
Monitor Performance Early
This aspect is not tied to SSR rendering, but an important fundamental. It's too easy to make a decision and be completely unaware that you've made a huge impact on performance.
For example, you've got a requirement that needs to support timezones. You use moment.js, build your feature, test it, deploy it. Boom. Easy. However, you didn't realise that you accidentally used all locales, which means 4MB of uncompressed JSON is now being distributed to your users.
This mistake may be obvious to you, but maybe not to a team member.
So many mistakes could of been prevented by implementing a fitness function that analyses performance as part of the CI pipeline. Automatic budgets on time to first byte, bundle size and HTML page size are really important to prevent performance blunders being deployed to production. Use something like Google Lighthouse to track those metrics.
This optimisation will help find many problems mentioned later in this article.
getInitialProps
Optimisation is Vital
Everything that you pass from getInitialProps
gets hydrated. It's printed directly to HTML, which means the user has to retrieve and parse it first before anything else can happen.
Always keep this is the back of your head. Do you need that data? It's very easy to pass a large amount of data that isn't used, forcing users to download and wait for a whole bunch of data they don't need.
This is something that should be tracked in the CI pipeline.
Apollo Cache Key Optimisation is Vital
When using Apollo, you should have a basic awareness on how normalisation works. Apollo will attempt to break up and normalise objects on a type and ID. Failing that, it will attempt to store the object based on how it's queried.
Apollo will try to pass the cache during the hydration process, by printing it to HTML. This is a huge optimisation opportunity for SSR. Without effectively normalised data, the HTML size can really grow quickly.
A lot of our content uses an entityId
unique identifier, instead of the default _id
or id
identifier. The cache passed via HTML was huge in some cases. By implementing a cache key, large amounts of raw HTML was trimmed.
This is something that should be tracked in the CI pipeline.
Watch Out for Styled Component Nested Props
When using styled components, you can pass dynamic props to a component:
import styled from "styled-components"
const Style = styled.div`
border: 1px solid blue;
.title {
color: ${props => props.color};
}
`
const Card = props => (
<Style>
<h1 className="title" color="blue">Card</h1>
</Style>
)
This feature is powerful, however because the prop is on a nested selector, this caused large amounts of duplicated styles to be printed directly to HTML, which again, forced users to download and wait for data they didn't need.
Be very vary of using props on nested styles.
This can be resolved by using an additional styled component:
<Title className="title" color="blue">Card</Title>
Or by using plain old CSS (my preferred approach):
.title {
color: attr(data-color);
}
<h1 className="title" data-color="blue">Card</h1>
async/await
in getInitialProps
Using async
/await
in getInitialProps
will block the entire request until the response has been resolved.
Content.getInitialProps = async () => {
const response = await fetch() // ...
const meta = response.data.meta
return { meta }
}
Avoid doing this as much as you can, be aware of the cost. Every millisecond waiting for that response will an extra millisecond on the time to first byte.
Consider Building a Version Notification System Early On
So any headaches were caused because a lot of user's leave tabs open and do not refresh the page. The Next.js application is cached in memory over client side navigation, and may not receive a new version until the page has been refreshed. If your application communicates with an API, this situation gets complicated.
Example
A new version of the CMS removes a data type from the content API, which the frontend relies on. The change gets deployed on both platforms. However, the user has the original version in the browser. They navigate to a page that relies on that now removed data type, theres an error.
Yes, there are many ways to handle this error and could be alleviated with API versioning. However, the problem is complex, you may easily find yourself fall into this trap accidentally.
It may not be immediately obvious to the user that they need to refresh, especially if the error is silent. The situation becomes worse if that request is critical, such as authentication.
Notifying users that a new version is available can really help improve the situation. This solution may not be for you, but a notification system is a simplistic implementation that can help drastically.
That's All
If you got this far, thanks for taking the time to read my first published article. Hopefully this helps some avoid the traps and pitfalls made.
Top comments (0)