Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't hydrate certain components in client #397

Closed
wants to merge 3 commits into from

Conversation

GHaberis
Copy link
Contributor

@GHaberis GHaberis commented Jan 28, 2019

This is an experimental feature!

We have a performance concern around React unnecessarily hydrating parts of our application which render exactly the same in the client as they do on the server. The same issues has been raised here facebook/react#12617

One of the options put forward in this issue involves using "dangerouslySetInnerHTML with empty content" because React won't try to manipulate the tree of a dangerouslySetInnerHTML node on the client.

This PR introduces a <Freeze> component. On the client the render function of Freeze returns an empty div with dangerouslySetInnerHTML set on it meaning React doesn't touch it, whilst on the server it renders the children nested within it. The idea is any children wrapped in this component will not be hydrated on the client by React, avoiding the processing overhead involved in hydrating these components.

To be wrapped by a <Freeze> a component must satisfy the following conditions:

  1. Does not make use of any lifecycle methods
  2. Does not have any nested descendant components that use lifecycle methods.
  3. Does not accept any properties driven by a state change from an ancestor component.

At the moment the onus is entirely on the developer using <Freeze> to make sure these conditions are met, if they're not the only way you'd know there's an issue is via runtime bugs!

I've tested this branch against master on CODE using https://www.webpagetest.org and have found the following:

gh-freeze-components - Moto G4 - Chrome - 3GSlow - 18 runs
First Interactive (Median) 12.8s

master - Moto G4 - Chrome - 3GSlow - 18 runs
First Interactive (Median) 14.8s

To do
An additional improvement (not applied here) of omitting certain components from the render cycle in the client is that we could remove the article data currently inline within the <head> that's currently used during hydration. This should reduce the weight of the HTML document.

@PRBuilds
Copy link

PRbuilds results:

LightHouse Reporting
1548687436.report.html

--automated message

const { children } = this.props;

return typeof window === 'undefined' ? (
<div>{children}</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably be a <Fragment> instead of a div

@SiAdcock
Copy link
Contributor

SiAdcock commented Jan 29, 2019

Thanks @GHaberis! The reduction in time to interactive is promising. I'd be really keen to investigate the developer experience a bit more, especially how we can help the developer identify the 3 conditions you specify.

Firstly what happens at runtime if the conditions aren't satisfied? Do things break or do you see an error message in the console?

Secondly, are there any additional checks we can run inside the Freeze component to ensure these conditions are met? For instance, if NODE_ENV==='development' could we recurse over all React.children and determine whether the type of any of them has a constructor property, and is hence a class? Throw an error if there are any children that use class constructors. There might be a better way of doing this!

@AWare
Copy link
Contributor

AWare commented Jan 29, 2019

I like the way this is its own component, this should let us use any new Suspense features if we get them. Could we use this with tree shaking or changes to the imports to lessen the js bundle size too?

@nicl
Copy link
Contributor

nicl commented Jan 29, 2019

Thanks @GHaberis for looking at this.

How do we feel about relying on an implementation detail of React here, which is not guaranteed to be stable? It seems like a fragile base for key parts of our site behaviour and performance going forward.

I've also got a few other concerns with this kind of approach:

  • forcing us to whitelist the preferred behaviour - it feels like the wrong default. What about if we found a way to mark things that did require client-side hydration instead? I've no idea how we might do this, but it does highlight that we are again introducing a source of discipline here that goes against the grain of what we want performance-wise.
  • it breaks the React isomorphic model of not caring about how/when things are rendered, which is part of the value proposition of our current approach
  • breaks the ability to compose/move components (as you need to fully understand the parent context), whereas a key benefit of the whole components approach is that components work, and can be understood, in isolation of specific context

Are there any alternatives we can explore to this approach?

Or would we simply be better off leaving as is until we hit closer to production and can get a fuller sense of the performance cost of client-side React+emotion?

@AWare
Copy link
Contributor

AWare commented Jan 30, 2019

I think we discussed this yesterday, but if Atoms need to be fully fledged components then we lose quite a lot of the benefit here. :(

@SiAdcock
Copy link
Contributor

if Atoms need to be fully fledged components then we lose quite a lot of the benefit here. :(

For those who weren't around, is there a tl;dr version of this discussion?

@nicl
Copy link
Contributor

nicl commented Jan 30, 2019

@AWare @SiAdcock is this a specific example of point three in my list of concerns above?

'breaks the ability to compose/move components (as you need to fully understand the parent context), whereas a key benefit of the whole components approach is that components work, and can be understood, in isolation of specific context'

@GHaberis
Copy link
Contributor Author

relying on an implementation detail of React here, which is not guaranteed to be stable? It seems like a fragile base for key parts of our site behaviour and performance going forward.

@nicl I agree, it's far from ideal to rely on a coincidental consequence of a React implementation detail. We can spend more time exploring alternatives to the dangerouslySetInnerHTML loop hole, it's unfortunate that there doesn't seem to be much word on delivering this as a feature and that Dan Abramov himself 🙌 suggests it as the best approach to address the issue.

if Atoms need to be fully fledged components then we lose quite a lot of the benefit here.

@alex this is true, as discussed once we implement these components we'd have to look at wrapping each block within an article body individually with Freeze rather than the body as a whole, so text blocks get wrapped and atoms don't. We could put together a registry of blocks which are 'static' and those that require clientside behaviour.

How we can help the developer identify the 3 conditions you specify.

@SiAdcock I was thinking about this. One thing we discussed last week was that all components that require clientside behaviour are situated in their own directory, lint rules could prevent lifecycle methods being used on components outside this directory. All static components could be imported via some kind of proxy, like a static component registry which wraps Freeze around all components it exports by default.

@nicl
Copy link
Contributor

nicl commented Jan 30, 2019

@GHaberis you mean this Dan?! https://dan.church/

@GHaberis
Copy link
Contributor Author

Closing this for now as we evaluate clientside JS options.

@GHaberis GHaberis closed this Feb 14, 2019
@GHaberis GHaberis deleted the gh-freeze-compoments branch June 25, 2019 13:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants