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

feat(server): add extractCriticalToChunks #2334

Merged
merged 36 commits into from
May 7, 2021

Conversation

mnajdova
Copy link
Contributor

@mnajdova mnajdova commented Apr 6, 2021

What:

This PR aims to fix #2158 and possibly #2049

Why:

The new implementation of extractCritical should allow developers to solve issues with SSR, mostly with the order of injecting the global css as well as fix dynamic re-writes of the global styles.

How:

It is implemented by adding a new extractCriticalToChunks utility that will return HTML and an array of styles objects each containing css string and an array of ids. Each injectGlobal or <Global /> component will be one item in the array, so that it can be treated independently. The css for the regular styles is added to one item of the list.

Checklist:

  • Documentation
  • Tests
  • Code complete
  • Changeset

@changeset-bot
Copy link

changeset-bot bot commented Apr 6, 2021

🦋 Changeset detected

Latest commit: 2cc16da

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@emotion/react Minor
@emotion/server Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

docs/ssr.mdx Outdated Show resolved Hide resolved
@mnajdova
Copy link
Contributor Author

mnajdova commented Apr 6, 2021

I am not sure if this is all that we need, to be honest. I think that one problem may be that the ids still contain the ids of the global styles too, not sure if this will be problematic.

@codesandbox-ci
Copy link

codesandbox-ci bot commented Apr 6, 2021

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 2cc16da:

Sandbox Source
Emotion Configuration
trusting-elbakyan-4k964 Issue #2158

docs/ssr.mdx Outdated Show resolved Hide resolved
packages/server/src/create-instance/extract-critical2.js Outdated Show resolved Hide resolved
@mnajdova mnajdova marked this pull request as ready for review April 9, 2021 21:43
@mnajdova mnajdova requested a review from Andarist April 9, 2021 22:14
@mnajdova
Copy link
Contributor Author

mnajdova commented Apr 9, 2021

I may have a bit of a different proposal. As emotion is already automatically adding and removing the <style data-emotion="global-css" /> maybe we should just not include the global styles in the result of extractCritical. The general problem is that, when some of the <Global /> components will be removed, extractCritical is not invoked again, so the global styles are still present in the <head />. I've created mui/material-ui#25690 for easier testing, as everything is already set up. I've updated here the root of the docs to return one global styles that sets * { color: white } and I am removing it after 5s. In order to make this scenario work, I just needed to add the regular css from the list I am returning now with the new extractCritical. Adding all style tags separately doesn't help, as this method is not being invoked again, when the <Global /> is being removed, so the previously added <style /> tags for the global styles are still there, see https://github.com/mui-org/material-ui/pull/25690/files#diff-51bb0d154187975c21ed74b384db0dac87cc6ea7079dfa2e7402e0ad8f40a563R160-R163

Copy link
Member

@Andarist Andarist left a comment

Choose a reason for hiding this comment

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

We'd need to a test like this:

  1. SSR <Global/><Global/><div css={{}}/> using extractCritical2
  2. rehydrate that
  3. unmount one of <Global/>s

The expected outcome is that only those styles would be removed and that they would be removed successfully (which I might sometimes refer to as flushing).

I think that this might require adding key to the items returned by extractCritical2 as <Global/> styles have keys suffixed with *-global (from what I recall).

A second concern is that we should check how this is going to work when rehydrating with @emotion/css. It would be great if we could add the ability to unmount injectGlobal styles:

insertWithoutScoping(cache, serialized)
. However, right now @emotion/css assumes a single sheet so flush is easy and straightforward:
flush() {
cache.registered = {}
cache.inserted = {}
cache.sheet.flush()
},

The ability to unmount injectGlobal styles would require maintaining multiple sheets (this is what <Global/> kinda does) and this might be a little bit tricky/unclear for @emotion/css

@emotion/css stuff is not a must here but would be great if we could at least take a closer look at it to assess how problematic implementing this for it would be.

packages/server/types/create-instance.d.ts Outdated Show resolved Hide resolved
packages/server/src/create-instance/extract-critical2.js Outdated Show resolved Hide resolved
packages/server/src/create-instance/inline.js Outdated Show resolved Hide resolved
packages/server/src/create-instance/extract-critical2.js Outdated Show resolved Hide resolved
packages/server/test/util.js Outdated Show resolved Hide resolved
packages/server/src/create-instance/extract-critical2.js Outdated Show resolved Hide resolved
@mnajdova
Copy link
Contributor Author

I'll need some help with the test.

We'd need to a test like this:

  1. SSR
    using extractCritical2
  2. rehydrate that
  3. unmount one of s

The expected outcome is that only those styles would be removed and that they would be removed successfully (which I might sometimes refer to as flushing).

I've tried implementing this with d7e7f8c (I've updated the components), but I am missing the step 2

  1. rehydrate that

Will it be enough if I just call emotion.hydrate(ids)? Currently the result of the second re-rendering is identical with the first one, which is because of the input (cache) coming in extractCritical2.

@mnajdova mnajdova requested a review from Andarist April 12, 2021 13:01
@mnajdova

This comment has been minimized.

@Andarist
Copy link
Member

I've tried implementing this with d7e7f8c (I've updated the components), but I am missing the step 2

I've first done this but this wasn't quite "e2e" just yet so I've followed up with this. In the process I've fixed some small~ issues that were in there - so even if my test is somewhat complicated, it was still worth doing this. I've pushed those commits to your branch so please fetch them before you continue working on this :)

I'll leave some additional comments in a moment but those will just be about things I've noticed so far - gonna try to follow up with a full review in the following days.

packages/react/__tests__/rehydration.js Outdated Show resolved Hide resolved
packages/react/__tests__/rehydration.js Show resolved Hide resolved
Comment on lines 277 to 290
<style
data-emotion="mui-global"
data-s=""
>

body{color:white;}
</style>
<style
data-emotion="mui-global"
data-s=""
>

html{background:red;}
</style>
Copy link
Member

Choose a reason for hiding this comment

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

SSRed global style elements were replaced here by client-rendered ones, this all happens very quickly so this might not be noticeable (although I'm slightly worried about unmounting @font-face and stuff like that, even for a brief moment) but I would still try to check what could be done about it

Why does this happen? SSR style is immediately flushed here:

if (sheet.tags.length) {
// if this doesn't exist then it will be null so the style element will be appended
let element = sheet.tags[sheet.tags.length - 1].nextElementSibling
sheet.before = ((element: any): Element | null)
sheet.flush()
}

since we already have some rehydrated sheet.tags coming from here:
if (node !== null) {
sheet.hydrate([node])
}

I believe that the original intention was to flush this on updates but right now we are also flushing it when rehydrating 😕

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, this may be the reason for the flickering here - https://deploy-preview-25690--material-ui.netlify.app/ The text is black for a split second, then it changes to white, which is coming from the global styles. Although on this example, I am just omitting the global styles coming from extractCritical2, so it may be a different issue. Will have to double-check.

Copy link
Member

Choose a reason for hiding this comment

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

Hm, I can only see the color:white declaration in the SSRed HTML 🤔 Has this site been updated since then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I cannot spot the flickering too this one time.. I’ve updated the example, we should see the borders after 5s too as that Global component stays mounted. Only the color should change to black (default)

Copy link
Member

Choose a reason for hiding this comment

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

@mitchellhamilton I would like to fix this issue with flushing and reinjecting Global styles when rehydrating. The easiest fix for that would be to just merge those 2 layout effects. Any objections to that? The first effect just avoids recreating StyleSheet (from what I can tell) based on the serialized.name but since sheet.flush is called anyway when it changes and highly dynamic global styles are rather rare I don't think we must maintain that split between two effects 🤔

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good

Copy link
Member

Choose a reason for hiding this comment

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

I've implemented this change - could you take a look @mitchellhamilton ? I doubt that this can break anything but better to recheck this

Copy link
Member

Choose a reason for hiding this comment

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

Hmmm, reading the code, it seems as though the change doesn't preserve the behavior of inserting the new style tag in the same position as the previous one. Could you add a test based on https://codesandbox.io/s/still-leaf-invs2?file=/src/App.js?

packages/server/src/create-instance/inline.js Show resolved Hide resolved
packages/server/types/create-instance.d.ts Outdated Show resolved Hide resolved
packages/react/__tests__/rehydration.js Show resolved Hide resolved
}
}

export const prettifyCritical2 = ({
Copy link
Member

Choose a reason for hiding this comment

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

as this critical2 was kinda renamed this should be adjusted as well

export interface EmotionServer {
extractCritical(html: string): EmotionCritical
renderStylesToString(html: string): string
renderStylesToNodeStream(): NodeJS.ReadWriteStream
constructStyleTags(html: string): EmotionCriticalToChunks
Copy link
Member

Choose a reason for hiding this comment

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

This is not correct. The return type of this is a string but the interface definition is missing a declaration for extractCriticalToChunks - for which this return type would be correct.

wdyt about renaming constructStyleTags to constructStyleTagsFromChunks? That way it would be more clear that those 2 APIs can be used together (extractCriticalToChunks and constructStyleTagsFromChunks)

@mnajdova
Copy link
Contributor Author

@Andarist I've tried resolving last comments with 124cf3f, 88de62d

@mnajdova
Copy link
Contributor Author

@Andarist I've upgraded our test branch on Material-UI, and I got the error below when navigating from one page to another. I think it's related to 00ce87b. It fails on sheet.flush(). I haven't seen the issue before that commit.

Unhandled Runtime Error
TypeError: Cannot read property 'removeChild' of null

Call Stack
<unknown>
node_modules\@emotion\sheet\dist\emotion-sheet.browser.esm.js (133:0)
Array.forEach
<anonymous>
StyleSheet.flush
/_next/static/chunks/pages/_app.js (2676:15)
<unknown>
node_modules\@emotion\react\dist\emotion-react.browser.esm.js (158:0)
HTMLUnknownElement.callCallback

Repro steps:

  1. Visit: https://deploy-preview-25690--material-ui.netlify.app/branding/about/
  2. Click the Docs link on the top of the page
  3. Error is thrown

We use different <Global /> components on these two pages.

@Andarist
Copy link
Member

@mnajdova I believe that I've fixed the issue & added tests for this - I've also confirmed that in your repository.

`)
})

test('duplicated global styles can be removed safely after rehydrating HTML SSRed with zero config approach', () => {
Copy link
Member

Choose a reason for hiding this comment

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

I thought that this case could be failing so I've added a test for this but it turned out that it wasn't failing.

I was under the impression that this query would select the same node twice when looking for the same hash:

let node: HTMLStyleElement | null = document.querySelector(
`style[data-emotion="${key} ${serialized.name}"]`
)

but:

  1. we SSR those style elements as [0, 1]
  2. they are moved automatically to head when initializing, and the order is preserved: [0, 1]
  3. when rehydrating the first global it grabs the element 0 and rehydrates it which moves that element before all other tags from the main cache so we end up temporarily with [1 , 0, ...mainCache] order
  4. the second global thus now grabs the element 1 and also moves It before other tags from the main chance and we end up with [0, 1, ...mainCache]

I think this works OK and I couldn't find a situation in which it would break but maybe @mitchellhamilton would have some idea for an edge case here that could be tested.

@mnajdova
Copy link
Contributor Author

@mnajdova I believe that I've fixed the issue & added tests for this - I've also confirmed that in your repository.

Great! I double-checked all failing scenarios we had, no issues found 🚀

@Andarist
Copy link
Member

Andarist commented May 2, 2021

Ok, I'll be taking a last look at this in the following days but I think it's in a good shape and that there are no major blockers against merging this. If you, @mitchellhamilton, have some spare time I would also appreciate your review here since this is touching some core logic that we need to get right.

@emmatown emmatown merged commit 7d9e74f into emotion-js:main May 7, 2021
@github-actions github-actions bot mentioned this pull request May 7, 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.

Dynamic global styles with SSR issue
4 participants