-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Changes from 16 commits
d98b963
f30982d
bba6038
55b8c0e
aca34c9
afdcbe5
52521b4
c7ac36c
d7e7f8c
c39f42a
ee704e1
104d539
6b5489e
cdd74fd
d5dad23
de5acc6
da4d7d9
f3b63f4
8345672
e69351c
70301b5
584216a
00ce87b
124cf3f
88de62d
26b6254
c550bc1
9596d32
b51b8d1
1ba5ab8
8d6fc69
634c863
2025657
eb70989
9537994
2cc16da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -11,22 +11,54 @@ afterEach(() => { | |||||||||||||||||||
jest.clearAllMocks() | ||||||||||||||||||||
}) | ||||||||||||||||||||
|
||||||||||||||||||||
let React | ||||||||||||||||||||
let ReactDOM | ||||||||||||||||||||
let ReactDOMServer | ||||||||||||||||||||
let createCache | ||||||||||||||||||||
let css | ||||||||||||||||||||
let jsx | ||||||||||||||||||||
let CacheProvider | ||||||||||||||||||||
let Global | ||||||||||||||||||||
let createEmotionServer | ||||||||||||||||||||
|
||||||||||||||||||||
const resetAllModules = () => { | ||||||||||||||||||||
jest.resetModules() | ||||||||||||||||||||
|
||||||||||||||||||||
createCache = require('@emotion/cache').default | ||||||||||||||||||||
React = require('react') | ||||||||||||||||||||
ReactDOM = require('react-dom') | ||||||||||||||||||||
ReactDOMServer = require('react-dom/server') | ||||||||||||||||||||
|
||||||||||||||||||||
const emotionReact = require('@emotion/react') | ||||||||||||||||||||
css = emotionReact.css | ||||||||||||||||||||
jsx = emotionReact.jsx | ||||||||||||||||||||
CacheProvider = emotionReact.CacheProvider | ||||||||||||||||||||
Global = emotionReact.Global | ||||||||||||||||||||
createEmotionServer = require('@emotion/server/create-instance').default | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
const removeGlobalProp = prop => { | ||||||||||||||||||||
let descriptor = Object.getOwnPropertyDescriptor(global, prop) | ||||||||||||||||||||
Object.defineProperty(global, prop, { | ||||||||||||||||||||
value: undefined, | ||||||||||||||||||||
writable: true, | ||||||||||||||||||||
configurable: true | ||||||||||||||||||||
}) | ||||||||||||||||||||
// $FlowFixMe | ||||||||||||||||||||
return () => Object.defineProperty(global, prop, descriptor) | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
const disableBrowserEnvTemporarily = fn => { | ||||||||||||||||||||
let restoreDocument = removeGlobalProp('document') | ||||||||||||||||||||
let restoreWindow = removeGlobalProp('window') | ||||||||||||||||||||
let restoreHTMLElement = removeGlobalProp('HTMLElement') | ||||||||||||||||||||
try { | ||||||||||||||||||||
return fn() | ||||||||||||||||||||
} finally { | ||||||||||||||||||||
restoreDocument() | ||||||||||||||||||||
restoreWindow() | ||||||||||||||||||||
restoreHTMLElement() | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
test("cache created in render doesn't cause a hydration mismatch", () => { | ||||||||||||||||||||
|
@@ -166,3 +198,131 @@ test('initializing another Emotion instance should not move already moved styles | |||||||||||||||||||
</head> | ||||||||||||||||||||
`) | ||||||||||||||||||||
}) | ||||||||||||||||||||
|
||||||||||||||||||||
test('xxx', () => { | ||||||||||||||||||||
mnajdova marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
const { app, styles } = disableBrowserEnvTemporarily(() => { | ||||||||||||||||||||
resetAllModules() | ||||||||||||||||||||
|
||||||||||||||||||||
let cache = createCache({ key: 'mui' }) | ||||||||||||||||||||
let { extractCritical2, constructStyleTags } = createEmotionServer(cache) | ||||||||||||||||||||
|
||||||||||||||||||||
const rendered = ReactDOMServer.renderToString( | ||||||||||||||||||||
<CacheProvider value={cache}> | ||||||||||||||||||||
<Global styles={{ body: { color: 'white' } }} /> | ||||||||||||||||||||
<Global styles={{ html: { background: 'red' } }} /> | ||||||||||||||||||||
<main css={{ color: 'green' }}> | ||||||||||||||||||||
<div css={{ color: 'hotpink' }} /> | ||||||||||||||||||||
</main> | ||||||||||||||||||||
</CacheProvider> | ||||||||||||||||||||
) | ||||||||||||||||||||
const extracted = extractCritical2(rendered) | ||||||||||||||||||||
return { | ||||||||||||||||||||
app: extracted.html, | ||||||||||||||||||||
styles: constructStyleTags(extracted) | ||||||||||||||||||||
} | ||||||||||||||||||||
}) | ||||||||||||||||||||
|
||||||||||||||||||||
safeQuerySelector('head').innerHTML = styles | ||||||||||||||||||||
safeQuerySelector('body').innerHTML = `<div id="root">${app}</div>` | ||||||||||||||||||||
expect(safeQuerySelector('html')).toMatchInlineSnapshot(` | ||||||||||||||||||||
<html> | ||||||||||||||||||||
<head> | ||||||||||||||||||||
<style | ||||||||||||||||||||
data-emotion="mui-global l6h" | ||||||||||||||||||||
mnajdova marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
> | ||||||||||||||||||||
body{color:white;} | ||||||||||||||||||||
</style> | ||||||||||||||||||||
<style | ||||||||||||||||||||
data-emotion="mui-global 10q49a4" | ||||||||||||||||||||
emmatown marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
> | ||||||||||||||||||||
html{background:red;} | ||||||||||||||||||||
</style> | ||||||||||||||||||||
<style | ||||||||||||||||||||
data-emotion="mui bjcoli 1lrxbo5" | ||||||||||||||||||||
> | ||||||||||||||||||||
.mui-bjcoli{color:green;}.mui-1lrxbo5{color:hotpink;} | ||||||||||||||||||||
</style> | ||||||||||||||||||||
</head> | ||||||||||||||||||||
<body> | ||||||||||||||||||||
<div | ||||||||||||||||||||
id="root" | ||||||||||||||||||||
> | ||||||||||||||||||||
<main | ||||||||||||||||||||
class="mui-bjcoli" | ||||||||||||||||||||
> | ||||||||||||||||||||
<div | ||||||||||||||||||||
class="mui-1lrxbo5" | ||||||||||||||||||||
/> | ||||||||||||||||||||
</main> | ||||||||||||||||||||
</div> | ||||||||||||||||||||
</body> | ||||||||||||||||||||
</html> | ||||||||||||||||||||
`) | ||||||||||||||||||||
|
||||||||||||||||||||
resetAllModules() | ||||||||||||||||||||
const cache = createCache({ key: 'mui' }) | ||||||||||||||||||||
|
||||||||||||||||||||
ReactDOM.render( | ||||||||||||||||||||
<CacheProvider value={cache}> | ||||||||||||||||||||
<Global styles={{ body: { color: 'white' } }} /> | ||||||||||||||||||||
<Global styles={{ html: { background: 'red' } }} /> | ||||||||||||||||||||
<main css={{ color: 'green' }}> | ||||||||||||||||||||
<div css={{ color: 'hotpink' }} /> | ||||||||||||||||||||
</main> | ||||||||||||||||||||
</CacheProvider>, | ||||||||||||||||||||
safeQuerySelector('#root') | ||||||||||||||||||||
) | ||||||||||||||||||||
|
||||||||||||||||||||
expect(safeQuerySelector('head')).toMatchInlineSnapshot(` | ||||||||||||||||||||
<head> | ||||||||||||||||||||
<style | ||||||||||||||||||||
data-emotion="mui-global" | ||||||||||||||||||||
data-s="" | ||||||||||||||||||||
> | ||||||||||||||||||||
|
||||||||||||||||||||
body{color:white;} | ||||||||||||||||||||
</style> | ||||||||||||||||||||
<style | ||||||||||||||||||||
data-emotion="mui-global" | ||||||||||||||||||||
data-s="" | ||||||||||||||||||||
> | ||||||||||||||||||||
|
||||||||||||||||||||
html{background:red;} | ||||||||||||||||||||
</style> | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Why does this happen? SSR style is immediately flushed here: emotion/packages/react/src/global.js Lines 126 to 131 in 4d7efcb
since we already have some rehydrated sheet.tags coming from here:emotion/packages/react/src/global.js Lines 108 to 110 in 4d7efcb
I believe that the original intention was to flush this on updates but right now we are also flushing it when rehydrating 😕 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, I can only see the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||||||||||||||||||||
<style | ||||||||||||||||||||
data-emotion="mui bjcoli 1lrxbo5" | ||||||||||||||||||||
data-s="" | ||||||||||||||||||||
> | ||||||||||||||||||||
.mui-bjcoli{color:green;}.mui-1lrxbo5{color:hotpink;} | ||||||||||||||||||||
</style> | ||||||||||||||||||||
</head> | ||||||||||||||||||||
`) | ||||||||||||||||||||
|
||||||||||||||||||||
ReactDOM.render( | ||||||||||||||||||||
<CacheProvider value={cache}> | ||||||||||||||||||||
<Global styles={{ html: { background: 'red' } }} /> | ||||||||||||||||||||
<main css={{ color: 'green' }}> | ||||||||||||||||||||
<div css={{ color: 'hotpink' }} /> | ||||||||||||||||||||
</main> | ||||||||||||||||||||
</CacheProvider>, | ||||||||||||||||||||
safeQuerySelector('#root') | ||||||||||||||||||||
) | ||||||||||||||||||||
|
||||||||||||||||||||
expect(safeQuerySelector('head')).toMatchInlineSnapshot(` | ||||||||||||||||||||
<head> | ||||||||||||||||||||
<style | ||||||||||||||||||||
data-emotion="mui-global" | ||||||||||||||||||||
data-s="" | ||||||||||||||||||||
> | ||||||||||||||||||||
|
||||||||||||||||||||
html{background:red;} | ||||||||||||||||||||
</style> | ||||||||||||||||||||
<style | ||||||||||||||||||||
data-emotion="mui bjcoli 1lrxbo5" | ||||||||||||||||||||
data-s="" | ||||||||||||||||||||
> | ||||||||||||||||||||
.mui-bjcoli{color:green;}.mui-1lrxbo5{color:hotpink;} | ||||||||||||||||||||
</style> | ||||||||||||||||||||
</head> | ||||||||||||||||||||
`) | ||||||||||||||||||||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// @flow | ||
import type { EmotionCache } from '@emotion/utils' | ||
import { generateStyleTag } from './utils' | ||
|
||
const createConstructStyleTags = ( | ||
cache: EmotionCache, | ||
nonceString: string | ||
) => (criticalData: { | ||
html: string, | ||
styles: Array<{ key: string, ids: Array<string>, css: string }> | ||
}) => { | ||
let styleTagsString = '' | ||
|
||
criticalData.styles.forEach(item => { | ||
styleTagsString += generateStyleTag( | ||
item.key, | ||
item.ids.join(' '), | ||
item.css, | ||
nonceString | ||
) | ||
}) | ||
|
||
return styleTagsString | ||
} | ||
|
||
export default createConstructStyleTags |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// @flow | ||
import type { EmotionCache } from '@emotion/utils' | ||
|
||
const createExtractCritical2 = (cache: EmotionCache) => (html: string) => { | ||
// parse out ids from html | ||
// reconstruct css/rules/cache to pass | ||
let RGX = new RegExp(`${cache.key}-([a-zA-Z0-9-_]+)`, 'gm') | ||
|
||
let o = { html, styles: [] } | ||
let match | ||
let ids = {} | ||
while ((match = RGX.exec(html)) !== null) { | ||
// $FlowFixMe | ||
if (ids[match[1]] === undefined) { | ||
// $FlowFixMe | ||
ids[match[1]] = true | ||
} | ||
} | ||
|
||
const regularCssIds = [] | ||
let regularCss = '' | ||
|
||
Object.keys(cache.inserted).forEach(id => { | ||
if ( | ||
(ids[id] !== undefined || | ||
cache.registered[`${cache.key}-${id}`] === undefined) && | ||
cache.inserted[id] !== true | ||
) { | ||
if (cache.registered[`${cache.key}-${id}`]) { | ||
// regular css can be added in one style tag | ||
regularCssIds.push(id) | ||
// $FlowFixMe | ||
regularCss += cache.inserted[id].toString() | ||
mnajdova marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else { | ||
Andarist marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// each global styles require a new entry so it can be independently flushed | ||
o.styles.push({ | ||
key: `${cache.key}-global`, | ||
ids: [id], | ||
css: cache.inserted[id] | ||
}) | ||
} | ||
} | ||
}) | ||
|
||
// make sure that regular css is added after the global styles | ||
o.styles.push({ key: cache.key, ids: regularCssIds, css: regularCss }) | ||
|
||
return o | ||
} | ||
|
||
export default createExtractCritical2 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
// @flow | ||
|
||
export function generateStyleTag( | ||
cssKey: string, | ||
ids: string, | ||
styles: string, | ||
nonceString: string | ||
) { | ||
return `<style data-emotion="${cssKey} ${ids}"${nonceString}>${styles}</style>` | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, this won't get deprecated - the recommendation would be that:
extractCriticalToChunks
should be used when using@emotion/react
extractCritical
should be used when using@emotion/css
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated, you can re-check the wording.