Skip to content

Commit e56952a

Browse files
authored
revise SSR+hydration functionality to be more fault-tolerant (styled-components#3018)
* Fix RULE_RE to reduce backtracking Fix styled-components#3017 * Add CHANGELOG entry * revise SSR emitter & rehydrator to use a split sequence instead of regex * Fix splitting to be per rule * Update CHANGELOG entry * Add newline to SPLITTER constant * skip garbage nodes * adjust snapshot Co-authored-by: Evan Jacobs <[email protected]>
1 parent d0e8eab commit e56952a

File tree

9 files changed

+113
-89
lines changed

9 files changed

+113
-89
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ _The format is based on [Keep a Changelog](http://keepachangelog.com/) and this
66

77
## Unreleased
88

9+
- Fix slow SSR Rehydration for malformed CSS and increase fault-tolerance (see [#3018](https://github.com/styled-components/styled-components/pull/3018))
10+
911
## [v5.0.1] - 2020-02-04
1012

1113
- Added useTheme hook to named exports for react native

packages/styled-components/src/constants.js

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const SC_ATTR =
1010
export const SC_ATTR_ACTIVE = 'active';
1111
export const SC_ATTR_VERSION = 'data-styled-version';
1212
export const SC_VERSION = __VERSION__;
13+
export const SPLITTER = '/*!sc*/\n';
1314

1415
export const IS_BROWSER = typeof window !== 'undefined' && 'HTMLElement' in window;
1516

packages/styled-components/src/sheet/GroupedTag.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/* eslint-disable no-use-before-define */
33

44
import type { GroupedTag, Tag } from './types';
5+
import { SPLITTER } from '../constants';
56
import throwStyledError from '../utils/error';
67

78
/** Create a GroupedTag with an underlying Tag implementation */
@@ -89,7 +90,7 @@ class DefaultGroupedTag implements GroupedTag {
8990
const endIndex = startIndex + length;
9091

9192
for (let i = startIndex; i < endIndex; i++) {
92-
css += `${this.tag.getRule(i)}\n`;
93+
css += `${this.tag.getRule(i)}${SPLITTER}`;
9394
}
9495

9596
return css;

packages/styled-components/src/sheet/Rehydration.js

+11-12
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
// @flow
22

3-
import { SC_ATTR, SC_ATTR_ACTIVE, SC_ATTR_VERSION, SC_VERSION } from '../constants';
3+
import { SPLITTER, SC_ATTR, SC_ATTR_ACTIVE, SC_ATTR_VERSION, SC_VERSION } from '../constants';
44
import { getIdForGroup, setGroupForId } from './GroupIDAllocator';
55
import type { Sheet } from './types';
66

77
const SELECTOR = `style[${SC_ATTR}][${SC_ATTR_VERSION}="${SC_VERSION}"]`;
8-
const RULE_RE = /(?:\s*)?(.*?){((?:{[^}]*}|(?!{).*?)*)}/g;
9-
const MARKER_RE = new RegExp(`^${SC_ATTR}\\.g(\\d+)\\[id="([\\w\\d-]+)"\\]`);
8+
const MARKER_RE = new RegExp(`^${SC_ATTR}\\.g(\\d+)\\[id="([\\w\\d-]+)"\\].*?"([^"]*)`);
109

1110
export const outputSheet = (sheet: Sheet) => {
1211
const tag = sheet.getTag();
@@ -34,7 +33,7 @@ export const outputSheet = (sheet: Sheet) => {
3433

3534
// NOTE: It's easier to collect rules and have the marker
3635
// after the actual rules to simplify the rehydration
37-
css += `${rules}${selector}{content:"${content}"}\n`;
36+
css += `${rules}${selector}{content:"${content}"}${SPLITTER}`;
3837
}
3938

4039
return css;
@@ -53,14 +52,14 @@ const rehydrateNamesFromContent = (sheet: Sheet, id: string, content: string) =>
5352
};
5453

5554
const rehydrateSheetFromTag = (sheet: Sheet, style: HTMLStyleElement) => {
56-
const rawHTML = style.innerHTML;
55+
const parts = style.innerHTML.split(SPLITTER);
5756
const rules: string[] = [];
58-
let parts;
5957

60-
// parts = [match, selector, content]
61-
// eslint-disable-next-line no-cond-assign
62-
while ((parts = RULE_RE.exec(rawHTML))) {
63-
const marker = parts[1].match(MARKER_RE);
58+
for (let i = 0, l = parts.length; i < l; i++) {
59+
const part = parts[i].trim();
60+
if (!part) continue;
61+
62+
const marker = part.match(MARKER_RE);
6463

6564
if (marker) {
6665
const group = parseInt(marker[1], 10) | 0;
@@ -71,13 +70,13 @@ const rehydrateSheetFromTag = (sheet: Sheet, style: HTMLStyleElement) => {
7170
setGroupForId(id, group);
7271
// Rehydrate names and rules
7372
// looks like: data-styled.g11[id="idA"]{content:"nameA,"}
74-
rehydrateNamesFromContent(sheet, id, parts[2].split('"')[1]);
73+
rehydrateNamesFromContent(sheet, id, marker[3]);
7574
sheet.getTag().insertRules(group, rules);
7675
}
7776

7877
rules.length = 0;
7978
} else {
80-
rules.push(parts[0].trim());
79+
rules.push(part);
8180
}
8281
}
8382
};

packages/styled-components/src/sheet/test/GroupedTag.test.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ it('inserts and retrieves rules by groups correctly', () => {
3333

3434
// Expect groups to contain inserted rules
3535
expect(groupedTag.getGroup(0)).toBe('');
36-
expect(groupedTag.getGroup(1)).toBe('.g1-a {}\n.g1-b {}\n');
36+
expect(groupedTag.getGroup(1)).toBe('.g1-a {}/*!sc*/\n.g1-b {}/*!sc*/\n');
3737
expect(groupedTag.getGroup(2)).toBe(
38-
'.g2-a {}\n.g2-b {}\n' +
39-
'.g2-c {}\n.g2-d {}\n'
38+
'.g2-a {}/*!sc*/\n.g2-b {}/*!sc*/\n' +
39+
'.g2-c {}/*!sc*/\n.g2-d {}/*!sc*/\n'
4040
);
4141

4242
// Check some rules in the tag as well
@@ -62,7 +62,7 @@ it('inserts rules at correct indices if some rules are dropped', () => {
6262
]);
6363

6464
expect(tag.length).toBe(1);
65-
expect(groupedTag.getGroup(1)).toBe('.inserted {}\n');
65+
expect(groupedTag.getGroup(1)).toBe('.inserted {}/*!sc*/\n');
6666
});
6767

6868
it('inserts and deletes groups correctly', () => {
@@ -89,7 +89,7 @@ it('does supports large group numbers', () => {
8989
expect(groupedTag.length).toBeGreaterThan(group);
9090
expect(tag.length).toBe(1);
9191
expect(groupedTag.indexOfGroup(group)).toBe(0);
92-
expect(groupedTag.getGroup(group)).toBe('.test {}\n');
92+
expect(groupedTag.getGroup(group)).toBe('.test {}/*!sc*/\n');
9393
});
9494

9595
it('throws when the upper group limit is reached', () => {

packages/styled-components/src/sheet/test/Rehydration.test.js

+38-17
Original file line numberDiff line numberDiff line change
@@ -23,31 +23,37 @@ describe('outputSheet', () => {
2323

2424
const output = outputSheet(sheet)
2525
.trim()
26-
.split('\n');
27-
28-
expect(output).toEqual([
29-
'.a {}',
30-
`${SC_ATTR}.g11[id="idA"]{content:"nameA,"}`,
31-
'.b {}',
32-
`${SC_ATTR}.g22[id="idB"]{content:"nameB,"}`,
33-
]);
26+
.split('/*!sc*/');
27+
28+
expect(output).toMatchInlineSnapshot(`
29+
Array [
30+
".a {}",
31+
"
32+
${SC_ATTR}.g11[id=\\"idA\\"]{content:\\"nameA,\\"}",
33+
"
34+
.b {}",
35+
"
36+
${SC_ATTR}.g22[id=\\"idB\\"]{content:\\"nameB,\\"}",
37+
"",
38+
]
39+
`);
3440
});
3541
});
3642

3743
describe('rehydrateSheet', () => {
3844
it('rehydrates sheets correctly', () => {
3945
document.head.innerHTML = `
4046
<style ${SC_ATTR} ${SC_ATTR_VERSION}="${SC_VERSION}">
41-
.a {}
42-
${SC_ATTR}.g11[id="idA"]{content:"nameA,"}
43-
${SC_ATTR}.g33[id="empty"]{content:""}
47+
.a {}/*!sc*/
48+
${SC_ATTR}.g11[id="idA"]{content:"nameA,"}/*!sc*/
49+
${SC_ATTR}.g33[id="empty"]{content:""}/*!sc*/
4450
</style>
4551
`;
4652

4753
document.body.innerHTML = `
4854
<style ${SC_ATTR} ${SC_ATTR_VERSION}="${SC_VERSION}">
49-
.b {}
50-
${SC_ATTR}.g22[id="idB"]{content:"nameB,"}
55+
.b {}/*!sc*/
56+
${SC_ATTR}.g22[id="idB"]{content:"nameB,"}/*!sc*/
5157
</style>
5258
`;
5359

@@ -69,8 +75,8 @@ describe('rehydrateSheet', () => {
6975
expect(sheet.hasNameForId('idB', 'nameB')).toBe(true);
7076
// Populates the underlying tag
7177
expect(sheet.getTag().tag.length).toBe(2);
72-
expect(sheet.getTag().getGroup(11)).toBe('.a {}\n');
73-
expect(sheet.getTag().getGroup(22)).toBe('.b {}\n');
78+
expect(sheet.getTag().getGroup(11)).toBe('.a {}/*!sc*/\n');
79+
expect(sheet.getTag().getGroup(22)).toBe('.b {}/*!sc*/\n');
7480
expect(sheet.getTag().getGroup(33)).toBe('');
7581
// Removes the old tags
7682
expect(styleHead.parentElement).toBe(null);
@@ -80,8 +86,8 @@ describe('rehydrateSheet', () => {
8086
it('ignores active style elements', () => {
8187
document.head.innerHTML = `
8288
<style ${SC_ATTR}="${SC_ATTR_ACTIVE}" ${SC_ATTR_VERSION}="${SC_VERSION}">
83-
.a {}
84-
${SC_ATTR}.g11[id="idA"]{content:"nameA,"}
89+
.a {}/*!sc*/
90+
${SC_ATTR}.g11[id="idA"]{content:"nameA,"}/*!sc*/
8591
</style>
8692
`;
8793

@@ -94,4 +100,19 @@ describe('rehydrateSheet', () => {
94100
expect(sheet.getTag().tag.length).toBe(0);
95101
expect(styleHead.parentElement).toBe(document.head);
96102
});
103+
104+
it('tolerates long, malformed CSS', () => {
105+
document.head.innerHTML = `
106+
<style ${SC_ATTR} ${SC_ATTR_VERSION}="${SC_VERSION}">
107+
{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
108+
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
109+
}
110+
.rule {}/*!sc*/
111+
${SC_ATTR}.g1[id="idA"]{content:""}/*!sc*/
112+
</style>
113+
`;
114+
115+
const sheet = new StyleSheet({ isServer: true });
116+
rehydrateSheet(sheet);
117+
});
97118
});

packages/styled-components/src/test/__snapshots__/ssr.test.js.snap

+30-30
Original file line numberDiff line numberDiff line change
@@ -3,58 +3,58 @@
33
exports[`ssr should add a nonce to the stylesheet if webpack nonce is detected in the global scope 1`] = `"<h1 class=\\"sc-b c\\">Hello SSR!</h1>"`;
44
55
exports[`ssr should add a nonce to the stylesheet if webpack nonce is detected in the global scope 2`] = `
6-
"<style nonce=\\"foo\\" data-styled=\\"true\\" data-styled-version=\\"JEST_MOCK_VERSION\\">.c{color:red;}
7-
data-styled.g1[id=\\"sc-b\\"]{content:\\"c,\\"}
8-
body{background:papayawhip;}
9-
data-styled.g2[id=\\"sc-global-a1\\"]{content:\\"sc-global-a1,\\"}
6+
"<style nonce=\\"foo\\" data-styled=\\"true\\" data-styled-version=\\"JEST_MOCK_VERSION\\">.c{color:red;}/*!sc*/
7+
data-styled.g1[id=\\"sc-b\\"]{content:\\"c,\\"}/*!sc*/
8+
body{background:papayawhip;}/*!sc*/
9+
data-styled.g2[id=\\"sc-global-a1\\"]{content:\\"sc-global-a1,\\"}/*!sc*/
1010
</style>"
1111
`;
1212
1313
exports[`ssr should extract both global and local CSS 1`] = `"<h1 class=\\"sc-b c\\">Hello SSR!</h1>"`;
1414
1515
exports[`ssr should extract both global and local CSS 2`] = `
16-
"<style data-styled=\\"true\\" data-styled-version=\\"JEST_MOCK_VERSION\\">.c{color:red;}
17-
data-styled.g1[id=\\"sc-b\\"]{content:\\"c,\\"}
18-
body{background:papayawhip;}
19-
data-styled.g2[id=\\"sc-global-a1\\"]{content:\\"sc-global-a1,\\"}
16+
"<style data-styled=\\"true\\" data-styled-version=\\"JEST_MOCK_VERSION\\">.c{color:red;}/*!sc*/
17+
data-styled.g1[id=\\"sc-b\\"]{content:\\"c,\\"}/*!sc*/
18+
body{background:papayawhip;}/*!sc*/
19+
data-styled.g2[id=\\"sc-global-a1\\"]{content:\\"sc-global-a1,\\"}/*!sc*/
2020
</style>"
2121
`;
2222
2323
exports[`ssr should extract the CSS in a simple case 1`] = `"<h1 class=\\"sc-a b\\">Hello SSR!</h1>"`;
2424
2525
exports[`ssr should extract the CSS in a simple case 2`] = `
26-
"<style data-styled=\\"true\\" data-styled-version=\\"JEST_MOCK_VERSION\\">.b{color:red;}
27-
data-styled.g1[id=\\"sc-a\\"]{content:\\"b,\\"}
26+
"<style data-styled=\\"true\\" data-styled-version=\\"JEST_MOCK_VERSION\\">.b{color:red;}/*!sc*/
27+
data-styled.g1[id=\\"sc-a\\"]{content:\\"b,\\"}/*!sc*/
2828
</style>"
2929
`;
3030
3131
exports[`ssr should handle errors while streaming 1`] = `[Error: React.Children.only expected to receive a single React element child.]`;
3232
3333
exports[`ssr should interleave styles with rendered HTML when utilitizing streaming 1`] = `
34-
"<style data-styled=\\"true\\" data-styled-version=\\"JEST_MOCK_VERSION\\">.c{color:red;}
35-
data-styled.g1[id=\\"sc-b\\"]{content:\\"c,\\"}
36-
body{background:papayawhip;}
37-
data-styled.g2[id=\\"sc-global-a1\\"]{content:\\"sc-global-a1,\\"}
34+
"<style data-styled=\\"true\\" data-styled-version=\\"JEST_MOCK_VERSION\\">.c{color:red;}/*!sc*/
35+
data-styled.g1[id=\\"sc-b\\"]{content:\\"c,\\"}/*!sc*/
36+
body{background:papayawhip;}/*!sc*/
37+
data-styled.g2[id=\\"sc-global-a1\\"]{content:\\"sc-global-a1,\\"}/*!sc*/
3838
</style><h1 class=\\"sc-b c\\">Hello SSR!</h1>"
3939
`;
4040
4141
exports[`ssr should render CSS in the order the components were defined, not rendered 1`] = `"<div><h2 class=\\"TWO a\\"></h2><h1 class=\\"ONE b\\"></h1></div>"`;
4242
4343
exports[`ssr should render CSS in the order the components were defined, not rendered 2`] = `
44-
"<style data-styled=\\"true\\" data-styled-version=\\"JEST_MOCK_VERSION\\">.b{color:red;}
45-
data-styled.g1[id=\\"ONE\\"]{content:\\"b,\\"}
46-
.a{color:blue;}
47-
data-styled.g2[id=\\"TWO\\"]{content:\\"a,\\"}
44+
"<style data-styled=\\"true\\" data-styled-version=\\"JEST_MOCK_VERSION\\">.b{color:red;}/*!sc*/
45+
data-styled.g1[id=\\"ONE\\"]{content:\\"b,\\"}/*!sc*/
46+
.a{color:blue;}/*!sc*/
47+
data-styled.g2[id=\\"TWO\\"]{content:\\"a,\\"}/*!sc*/
4848
</style>"
4949
`;
5050
5151
exports[`ssr should return a generated React style element 1`] = `
5252
Object {
5353
"dangerouslySetInnerHTML": Object {
54-
"__html": ".c{color:red;}
55-
data-styled.g1[id=\\"sc-b\\"]{content:\\"c,\\"}
56-
body{background:papayawhip;}
57-
data-styled.g2[id=\\"sc-global-a1\\"]{content:\\"sc-global-a1,\\"}
54+
"__html": ".c{color:red;}/*!sc*/
55+
data-styled.g1[id=\\"sc-b\\"]{content:\\"c,\\"}/*!sc*/
56+
body{background:papayawhip;}/*!sc*/
57+
data-styled.g2[id=\\"sc-global-a1\\"]{content:\\"sc-global-a1,\\"}/*!sc*/
5858
",
5959
},
6060
"data-styled": "",
@@ -70,10 +70,10 @@ exports[`ssr should throw if getStyleElement is called after interleaveWithNodeS
7070
`;
7171
7272
exports[`ssr should throw if getStyleElement is called after streaming is complete 1`] = `
73-
"<style data-styled=\\"true\\" data-styled-version=\\"JEST_MOCK_VERSION\\">.c{color:red;}
74-
data-styled.g1[id=\\"sc-b\\"]{content:\\"c,\\"}
75-
body{background:papayawhip;}
76-
data-styled.g2[id=\\"sc-global-a1\\"]{content:\\"sc-global-a1,\\"}
73+
"<style data-styled=\\"true\\" data-styled-version=\\"JEST_MOCK_VERSION\\">.c{color:red;}/*!sc*/
74+
data-styled.g1[id=\\"sc-b\\"]{content:\\"c,\\"}/*!sc*/
75+
body{background:papayawhip;}/*!sc*/
76+
data-styled.g2[id=\\"sc-global-a1\\"]{content:\\"sc-global-a1,\\"}/*!sc*/
7777
</style><h1 class=\\"sc-b c\\">Hello SSR!</h1>"
7878
`;
7979
@@ -92,10 +92,10 @@ exports[`ssr should throw if getStyleTags is called after interleaveWithNodeStre
9292
`;
9393
9494
exports[`ssr should throw if getStyleTags is called after streaming is complete 1`] = `
95-
"<style data-styled=\\"true\\" data-styled-version=\\"JEST_MOCK_VERSION\\">.c{color:red;}
96-
data-styled.g1[id=\\"sc-b\\"]{content:\\"c,\\"}
97-
body{background:papayawhip;}
98-
data-styled.g2[id=\\"sc-global-a1\\"]{content:\\"sc-global-a1,\\"}
95+
"<style data-styled=\\"true\\" data-styled-version=\\"JEST_MOCK_VERSION\\">.c{color:red;}/*!sc*/
96+
data-styled.g1[id=\\"sc-b\\"]{content:\\"c,\\"}/*!sc*/
97+
body{background:papayawhip;}/*!sc*/
98+
data-styled.g2[id=\\"sc-global-a1\\"]{content:\\"sc-global-a1,\\"}/*!sc*/
9999
</style><h1 class=\\"sc-b c\\">Hello SSR!</h1>"
100100
`;
101101

0 commit comments

Comments
 (0)