Skip to content

Partial rendering and hydration of react on the client - #893

Merged
oliverlloyd merged 28 commits into
masterfrom
oliver/partial-react
Nov 6, 2019
Merged

oliverlloyd merged 28 commits into
masterfrom
oliver/partial-react

Conversation

@oliverlloyd

@oliverlloyd oliverlloyd commented Oct 29, 2019 •

Copy link
Copy Markdown
Contributor

What does this change?

This replaces our current approach of fully hydrating the client app with partial hydration of separate react nodes, only where required.

React Islands

Currently we render content on the server and then hydrate the entire app on the client. This technique only hydrates the specific areas of the app that require react, leaving the rest as static html.

The components using this approach are:

Nav (the main pillar menu in the Header)
EditionDropdown
MostViewed
ReaderRevenueLinks
RichLinks
ShareCount

The user experience is that the page will load and whatever content that exited the server render will be present on the page. React is then added to each dynamic part of the page with the following impacts:

Nav (the main pillar menu in the Header)

The main pillar menu is immediately present on page load. The More button only starts working after react is loaded

EditionDropdown

The dropdown control is visible on the page but does not become active until react is added

MostViewed

The entire component is missing on initial load and only appears after react is loaded and the api call to get the content returns

ReaderRevenueLinks

This component is missing on initial render and only appears after react is loaded

RichLinks

These are hidden on initial load and pop in once react is present and the api call returns

ShareCount

This is hidden on initial load and appears once react is present and the api call returns

Next steps

This code tests well and is functional. It is logical that it should be more performant but the improvement is difficult to measure so discussion around an approach for implementation is required.

With this PR in place it would be logical to review the data we pass to the client using window.guardian. This could likely be substantially reduced.

Why?

Performance

Link to supporting Trello card

https://trello.com/c/cA7ymUYg/781-spike-portals

@oliverlloyd
oliverlloyd requested a review from nicl October 29, 2019 13:11
@PRBuilds

PRBuilds commented Oct 29, 2019 •

Copy link
Copy Markdown

PRbuilds results:

💚 AMP validation
amp-report.txt

LightHouse Reporting
1573042149.report.html

--automated message

@SiAdcock

Copy link
Copy Markdown
Contributor

Interesting stuff @oliverlloyd, thanks for the detailed description!

Why not use Portals for Nav?

To complete the circle I have to ask, why not Islands for everything? Islands sounds closer to a progressive enhancement ideal (render as much as possible including placeholders on the server, enhance on the client). With Portals it sounds like we're presenting loading spinners and waiting for everything to be ready before displaying anything useful.

Even if progressive enhancement is not possible for a particular piece of content, isn't it better (i.e. less complex) to have "one way of doing things"? I can't think of any tradeoffs with the Islands approach that necessitates using Portals in some circumstances.

This is my naive reading, please correct me if I have misunderstood 😄

@oliverlloyd

Copy link
Copy Markdown
Contributor Author

why not Islands for everything?

You're right, replacing Portals with Islands would make no discernible difference to the user experience so having one approach makes sense. This content won't be rendered on the server in any case, not the way things are currently setup, so the initial render would still be empty for these elements.

The advantage to Portals is it's just one instance of react, not one per island. So that means you can share state between different portals. This is nice, but as things stand we don't have a requirement for this. Changes in context (navigation, changing edition, etc.,) trigger page loads so there's no state to manage.

I'll have a look at migrating everything to islands in this PR.

@SiAdcock

Copy link
Copy Markdown
Contributor

Thanks @oliverlloyd, interesting points.

I agree with the YAGNI argument against shared context. However, I wonder if there's a hidden cost in called ReactDom.render() for a whole bunch of Islands, vs calling it once for Portals?

@oliverlloyd

Copy link
Copy Markdown
Contributor Author

I had the same thought. I guess it depends on how fast

5 x ReactDom.hydrate(React.createElement( ... ), ...)

is vs.

1 x ReactDom.render( ... ) + 5 x ReactDom.createPortal( ... )

...defaultStyles,
Comment: palette.opinion.faded,
AdvertisementFeature: palette.neutral[85],
AdvertisementFeature: palette.neutral[86],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is this related?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think that's where I'm polluting this PR by merging in master, sorry!

// },
// true,
// );
window.guardian.modules.sentry.reportError(error, 'rich-link');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nice :D


const richLinkContainer = css`
width: 8.125rem;
${until.wide} {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

hmm it's weird that these changes are showing up here when they were made on my branch/merge

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's me merging in master to fix conflicts. Sorry, I should have waiting for the reviews

@philmcmahon philmcmahon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I've looked through this and agree with the motivation behind the PR and the code looks good to me. I would:

Deploy to CODE dotcom:rendering

@philmcmahon

Copy link
Copy Markdown
Contributor

(also looks like cypress tests are failing for some reason in teamcity, I'm happy to help debug this)

@nicl nicl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think I am for this as an improvement over what we have now. But would be great to get numbers for the performance impact before we merge as that is the primary motivation for the change.

@oliverlloyd

Copy link
Copy Markdown
Contributor Author

There was a talk by Yuzhi Zheng, the React team manager, at ReactConf the other day about a feature they're working on as part of the react core which is very relevant to this PR: Progressive/Selective Hydration. It's only a few minutes long but a really interesting window into how they think our problem should be solved.

Essentially Progressive means it lazy loads islands (renders a section, then hydrates it, then renders the next section, then hydrates it...). And Selective means it will decide the order to render/hydrate islands based on mouse position.

This made me realise we should think about the order that we hydrate islands. For example: it makes a lot of sense to hydrate the Pillar menu and EditionDropdown before the ShareCount or MostViewed.

@oliverlloyd

oliverlloyd commented Oct 31, 2019 •

Copy link
Copy Markdown
Contributor Author

would be great to get numbers

@nicl Totally agree.

I did try some comparisons using our internal webpagetest server but there was no notable difference in the reported numbers. I suspect wpt isn't giving us a true TTI value.

I also worked with Phil to setup our performance monitoring on Code so we could use that to compare but the granularity isn't great (once a day) and there's a real risk other deploys will impact our control.

I'm interested in the script you demo'd at the client side meeting the other day, maybe we can see if that helps here?

I suspect the key will be gathering stats using a slower device. These are the users who we're doing this work for, people like you and I with modern laptops and phones are unlikely to feel much benefit, esp. not at this stage of the project where we have relatively few interactive elements on the page..

@oliverlloyd

oliverlloyd commented Nov 1, 2019 •

Copy link
Copy Markdown
Contributor Author

@nicl I used your fast lib to run some tests on my laptop and there's a positive impact. tti Drops from 9.26 to 8.27

| branch        | time  | ps   | tti  | kb     |
|---------------|-------|------|------|--------|
| master        | 14:40 | 0.67 | 9.44 | 583916 |
| master        | 14:45 | 0.66 | 9.09 | 584351 |
| master        | 14:53 | 0.66 | 9.24 | 583990 |
| master        | 15:26 | 0.68 | 9.27 | 583370 |
| master        | 15:31 | 0.68 | 9.24 | 583613 |
|---------------|-------|------|------|--------|
|               |  Avg: | 0.67 | 9.26 | 583848 |
|               |       |      |      |        |
| partial-react | 14:54 | 0.69 | 8.44 | 567966 |
| partial-react | 15:01 | 0.68 | 9.13 | 567973 |
| partial-react | 14:32 | 0.65 | 9.01 | 568239 |
| partial-react | 14:32 | 0.70 | 9.32 | 567376 |
| partial-react | 15:24 | 0.68 | 8.94 | 567325 |
|---------------|-------|------|------|--------|
|               |  Avg: | 0.68 | 8.97 | 567776 |

@oliverlloyd

Copy link
Copy Markdown
Contributor Author

Another win here is that the react bundle size drops from 76kbs to 60kbs

Full hydration

Screenshot 2019-11-01 at 21 04 19

Partial hydration

Screenshot 2019-11-01 at 21 02 09

🎉

@SiAdcock

SiAdcock commented Nov 2, 2019

Copy link
Copy Markdown
Contributor

Another win here is that the react bundle size drops from 76kbs to 60kbs

Wow! Is the reduction accounted for by components that are rendered on the server only? Or do you think something else is being excluded?

@oliverlloyd

Copy link
Copy Markdown
Contributor Author

I guess it's a bit like we've implemented code splitting. Before, the code path traversed the whole app, importing everything into the bundle. Now, with the change in this PR, it is limited to certain routes, so many components are never touched. The number of components touched drops from 94 to 54.

These images show the webpack bundle analyser output for the react bundle before and after. You can see that with islands there are substantially fewer files under init.ts, the point where this PR changed the code.

Before (without islands)

Screenshot 2019-11-02 at 22 50 37
(init.ts - 20kb)

After (with islands)

Screenshot 2019-11-02 at 22 50 18
(init.ts - 32kb)

@gtrufitt

gtrufitt commented Nov 5, 2019

Copy link
Copy Markdown
Contributor

With this PR in place it would be logical to review the data we pass to the client using window.guardian

This will be another big advantage. Right now we pass a lot that we don't need, so reducing that and being more specific will be great.

And Selective means it will decide the order to render/hydrate islands based on mouse position.

This is another key IMO - Islands mean we don't have to do any hydration of bottom of article related content until we reach a scroll position (and probably some other components only if use mouse position or even interaction!)

@gtrufitt

gtrufitt commented Nov 5, 2019

Copy link
Copy Markdown
Contributor

Oh and this means we can include useful libraries on the server, without having to use them in the client. In particular, this example: #694

component: IslandType['component'];
// props: any because TS is locking it's checks to the first type
// in the IslandProps union, and then failing subsequent items
// in out array of islands

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// in out array of islands
// in our array of islands


<body>
<div id="app">${html}</div>
${html}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

tenor-38965271

@oliverlloyd
oliverlloyd merged commit 12663c4 into master Nov 6, 2019
@oliverlloyd
oliverlloyd deleted the oliver/partial-react branch November 6, 2019 12:17
@oliverlloyd oliverlloyd mentioned this pull request Nov 12, 2021
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.

6 participants