Effective responsiveness
See original GitHub issueFor reasons such as efficiency (4) and SEO, we want to do server-side rendering in such a way that the response sent to the client is immediately usable without the need to re-render.
Alas our current solution, which essentially boils down to if/else branching per breakpoint, doesn’t fulfil these needs as it is practically impossible (1) to know what the breakpoint is that the client will want to display at SSR time.
Additionally, even when getting the initial breakpoint right, the performance of changing breakpoints will be worse when a browser has to evaluate JS, recalculate a component tree, diff DOM elements, and update the DOM; as opposed to the browser only needing to re-render the styling without any DOM changes. (2)
Example
Consider a desktop browser requesting a page that is rendered as follows:
<Responsive>
{({ xs }) => {
if (xs) {
return <Box width={1} />
} else {
return <Box width={1/2} />
}
}}
</Responsive>
On the server we’ll render a xs
version (because that’s currently our default), then once the browser receives the response it may start to render that version.
However, once React is fully booted it starts to render a virtual DOM version based on an actual media query that may result in a different breakpoint than xs
. If that vDOM does not match what the server sent, the server version is thrown away and the new version is used instead.
Options
Note that these are just some bare-bones examples. Where possible it would be great to come up with prettier declarative APIs that use these techniques under the hood.
Pure CSS
In cases where all differences in layout can be expressed in styling, we should be able to do something like:
const ResponsiveBox = styled.div`
@media screen and (max-width: 767px) {
width: 100%;
}
@media screen and (min-width: 768px) {
width: 50%;
}
`
<ResponsiveBox />
When some components should only render on certain breakpoints, these may simply be hidden on those breakpoints using CSS. (3)
styled-system
We’re using styled-system, which out of the box comes with responsive options that, I assume–and we should verify this, do exactly what the above option does. For instance, rather than the above example, we should write <Box width={[1, 1/2]} />
.
More…
Please suggest yours!
Notes
- That is, without resorting to added complexity such as sniffing user-agent and even then it won’t be foolproof.
- While this performance penalty is even noticeable on desktop machines, it’s even more important for mobile devices.
- In the case of mobile devices, this may lead to documents being sent to the client that include markup that will never be used, which presumably could add up for large pages. Seeing as in some of these cases, such as iPhones, we can pretty reliably determine the client never needs larger breakpoints, it would be great to have an API that could omit certain breakpoints completely.
- Besides the aforementioned re-rendering of the browser, there are also examples such as page caching, which could be done for all unauthenticated requests of some pages.
Issue Analytics
- State:
- Created 5 years ago
- Comments:45 (45 by maintainers)
Top GitHub Comments
This
renderOnlyAt
pattern works well! One thing I noticed is we’ll need to use<MockBoot>
in the tests and set initial breakpoints otherwise it will by default render out two (or however many) items.Alright, did a quick and dirty spike to verify my idea and it seems to work. These are the steps:
renderOnlyAt
) the rendered components to the matching breakpoints.Responsive
component, we register event listeners with the browser to notify theMediaContextProvider
when a different breakpoint is matched and then re-render the tree using the new value forrenderOnlyAt
.I added logs to some components’
componentDidMount
lifecycle methods that shouldn’t both be visible at the same time.The server renders all breakpoints and lifecycle methods such as
componentDidMount
will never be invoked, so all good:The browser renders the markup, boots React and the app, which ends up only rendering for the currently matching breakpoint (
lg
) and thus yields a hydration error as expected, but importantly only mounts 1 component (LargeArtistHeader
):Resizing the browser window triggers re-renders at new breakpoints, again only mounting 1 component at a time (
LargeArtistHeader
, thenSmallArtistHeader
):