Skip to content

Commit aec36a6

Browse files
authored
Add proper styled-components optimisation (#3)
Add proper styled-components optimisation
2 parents a85f77a + 82cd764 commit aec36a6

File tree

11 files changed

+298
-70
lines changed

11 files changed

+298
-70
lines changed

.flowconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
[untyped]
66
.*/node_modules/react-is/.*
7+
.*/node_modules/styled-components/.*
78

89
[libs]
910
types/

src/__tests__/suspense.test.js

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('renderPrepass', () => {
1818

1919
const Outer = () => {
2020
const start = Date.now()
21-
while (Date.now() - start < 21) {}
21+
while (Date.now() - start < 40) {}
2222
return <Inner />
2323
}
2424

@@ -37,6 +37,78 @@ describe('renderPrepass', () => {
3737
})
3838
})
3939

40+
it('preserves the correct legacy context values across yields', () => {
41+
let called = false
42+
const Inner = (_, context) => {
43+
expect(context.test).toBe(123)
44+
called = true
45+
return null
46+
}
47+
48+
const Wait = props => {
49+
const start = Date.now()
50+
while (Date.now() - start < 21) {}
51+
return props.children
52+
}
53+
54+
class Outer extends Component {
55+
getChildContext() {
56+
return { test: 123 }
57+
}
58+
59+
render() {
60+
return (
61+
<Wait>
62+
<Wait>
63+
<Inner />
64+
</Wait>
65+
</Wait>
66+
)
67+
}
68+
}
69+
70+
Inner.contextTypes = { test: null }
71+
Outer.childContextTypes = { test: null }
72+
73+
const render$ = renderPrepass(<Outer />)
74+
expect(called).toBe(false)
75+
return render$.then(() => {
76+
expect(called).toBe(true)
77+
})
78+
})
79+
80+
it('preserves the correct context values across yields', () => {
81+
const Context = createContext(null)
82+
83+
let called = false
84+
const Inner = () => {
85+
const value = useContext(Context)
86+
expect(value).toBe(123)
87+
called = true
88+
return null
89+
}
90+
91+
const Wait = () => {
92+
const start = Date.now()
93+
while (Date.now() - start < 21) {}
94+
return <Inner />
95+
}
96+
97+
const Outer = () => {
98+
return (
99+
<Context.Provider value={123}>
100+
<Wait />
101+
</Context.Provider>
102+
)
103+
}
104+
105+
const render$ = renderPrepass(<Outer />)
106+
expect(called).toBe(false)
107+
return render$.then(() => {
108+
expect(called).toBe(true)
109+
})
110+
})
111+
40112
it('does not yields when work is below the threshold', () => {
41113
const Inner = jest.fn(() => null)
42114
const Outer = () => <Inner />

src/__tests__/visitor.test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ const {
3131
ReactCurrentDispatcher
3232
} = (React: any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
3333

34+
const {
35+
StyleSheet
36+
} = require('styled-components').__DO_NOT_USE_OR_YOU_WILL_BE_HAUNTED_BY_SPOOKY_GHOSTS
37+
3438
let prevDispatcher = null
3539

3640
beforeEach(() => {
@@ -48,6 +52,10 @@ afterEach(() => {
4852
const Noop = () => null
4953

5054
describe('visitElement', () => {
55+
beforeEach(() => {
56+
StyleSheet.reset(true)
57+
})
58+
5159
it('walks Fragments', () => {
5260
const element = (
5361
<Fragment>
@@ -115,14 +123,32 @@ describe('visitElement', () => {
115123
[],
116124
() => {}
117125
)
126+
118127
expect(children.length).toBe(1)
119128
expect(children[0].type).toBe(Noop)
129+
expect(StyleSheet.master.tags.length).toBe(1)
130+
131+
const tag = StyleSheet.master.tags[0]
132+
expect(tag.css().trim()).toBe('')
133+
134+
expect(Object.keys(StyleSheet.master.deferred)).toEqual([
135+
expect.any(String)
136+
])
120137
})
121138

122139
it('walks StyledComponent wrapper elements', () => {
123140
const Comp = styled(Noop)``
124141
const children = visitElement(<Comp />, [], () => {})
142+
125143
expect(children.length).toBe(1)
144+
expect(StyleSheet.master.tags.length).toBe(1)
145+
146+
const tag = StyleSheet.master.tags[0]
147+
expect(tag.css().trim()).toBe('')
148+
149+
expect(Object.keys(StyleSheet.master.deferred)).toEqual([
150+
expect.any(String)
151+
])
126152
})
127153

128154
it('walks Providers and Consumers', () => {

src/index.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,16 @@ const updateWithFrame = (
5656
// Update the component after we've suspended to rerender it,
5757
// at which point we'll actually get its children
5858
if (frame.kind === 'frame.class') {
59-
children = getChildrenArray(updateClassComponent(queue, frame))
59+
children = updateClassComponent(queue, frame)
6060
} else if (frame.kind === 'frame.hooks') {
61-
children = getChildrenArray(updateFunctionComponent(queue, frame))
61+
children = updateFunctionComponent(queue, frame)
6262
} else if (frame.kind === 'frame.lazy') {
63-
children = getChildrenArray(updateLazyComponent(queue, frame))
63+
children = updateLazyComponent(queue, frame)
6464
}
6565

6666
// Now continue walking the previously suspended component's
6767
// children (which might also suspend)
68-
visitChildren(children, queue, visitor)
68+
visitChildren(getChildrenArray(children), queue, visitor)
6969
ReactCurrentDispatcher.current = prevDispatcher
7070
})
7171
}
@@ -80,11 +80,11 @@ const flushFrames = (queue: Frame[], visitor: Visitor): Promise<void> => {
8080
)
8181
}
8282

83-
const defaultVisitor = () => {}
83+
const defaultVisitor = () => undefined
8484

8585
const renderPrepass = (element: Node, visitor?: Visitor): Promise<void> => {
8686
const queue: Frame[] = []
87-
let fn = visitor !== undefined ? visitor : defaultVisitor
87+
const fn = visitor !== undefined ? visitor : defaultVisitor
8888

8989
// Context state is kept globally and is modified in-place.
9090
// Before we start walking the element tree we need to reset

src/render/classComponent.js

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,17 +151,15 @@ export const mount = (
151151
props: DefaultProps,
152152
queue: Frame[],
153153
visitor: Visitor,
154-
element?: UserElement
154+
element: UserElement
155155
) => {
156156
setCurrentIdentity(null)
157-
const instance = createInstance(type, props)
158157

159-
if (element !== undefined) {
160-
const p = visitor(element, instance)
161-
if (typeof p === 'object' && p !== null && typeof p.then === 'function') {
162-
queue.push(makeFrame(type, instance, p))
163-
return null
164-
}
158+
const instance = createInstance(type, props)
159+
const promise = visitor(element, instance)
160+
if (promise) {
161+
queue.push(makeFrame(type, instance, promise))
162+
return null
165163
}
166164

167165
return render(type, instance, queue)

src/render/functionComponent.js

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,6 @@ const render = (
6161

6262
queue.push(makeFrame(type, props, error))
6363
return null
64-
} finally {
65-
setCurrentIdentity(null)
6664
}
6765
}
6866

@@ -72,17 +70,15 @@ export const mount = (
7270
props: DefaultProps,
7371
queue: Frame[],
7472
visitor: Visitor,
75-
element?: UserElement
73+
element: UserElement
7674
): Node => {
7775
setFirstHook(null)
7876
setCurrentIdentity(makeIdentity())
7977

80-
if (element !== undefined) {
81-
const p = visitor(element)
82-
if (typeof p === 'object' && p !== null && typeof p.then === 'function') {
83-
queue.push(makeFrame(type, props, p))
84-
return null
85-
}
78+
const promise = visitor(element)
79+
if (promise) {
80+
queue.push(makeFrame(type, props, promise))
81+
return null
8682
}
8783

8884
return render(type, props, queue)

src/render/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,8 @@ export {
2222
mount as mountClassComponent,
2323
update as updateClassComponent
2424
} from './classComponent'
25+
26+
export {
27+
isStyledElement,
28+
mount as mountStyledComponent
29+
} from './styledComponent'

src/render/styledComponent.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// @flow
2+
3+
import { createElement, type ComponentType, type Node } from 'react'
4+
5+
import { getChildrenArray, computeProps } from '../element'
6+
import { readContextValue } from '../internals'
7+
import { mount as mountFunctionComponent } from './functionComponent'
8+
9+
import type {
10+
AbstractElement,
11+
DefaultProps,
12+
ForwardRefElement,
13+
Frame,
14+
ComponentStatics
15+
} from '../types'
16+
17+
let styledComponents: any
18+
try {
19+
styledComponents = require('styled-components')
20+
21+
if (
22+
styledComponents.__DO_NOT_USE_OR_YOU_WILL_BE_HAUNTED_BY_SPOOKY_GHOSTS ===
23+
undefined ||
24+
styledComponents.ThemeContext === undefined
25+
) {
26+
styledComponents = undefined
27+
}
28+
} catch (_error) {}
29+
30+
type AttrsFn = (context: mixed) => DefaultProps
31+
type Attr = void | AttrsFn | { [propName: string]: ?AttrsFn }
32+
33+
type StyledComponentStatics = {
34+
styledComponentId: string,
35+
attrs: Attr | Attr[],
36+
target: ComponentType<DefaultProps> & ComponentStatics,
37+
defaultProps?: Object
38+
}
39+
40+
/** Determines a StyledComponent's theme taking defaults into account */
41+
const computeTheme = (props: Object, defaultProps: Object): Object => {
42+
const defaultTheme = defaultProps ? defaultProps.theme : undefined
43+
const isDefaultTheme = defaultTheme ? props.theme === defaultTheme : false
44+
45+
if (props.theme && !isDefaultTheme) {
46+
return props.theme
47+
} else {
48+
const contextTheme = readContextValue(styledComponents.ThemeContext)
49+
return contextTheme || defaultTheme
50+
}
51+
}
52+
53+
/** Computes a StyledComponent's props with attributes */
54+
const computeAttrsProps = (input: Attr[], props: any, theme: any): any => {
55+
const executionContext = { ...props, theme }
56+
57+
const attrs = input.reduce((acc, attr) => {
58+
if (typeof attr === 'function') {
59+
return Object.assign(acc, attr(executionContext))
60+
} else if (typeof attr !== 'object' || attr === null) {
61+
return acc
62+
}
63+
64+
for (const key in attr) {
65+
const attrProp = attr[key]
66+
if (typeof attrProp === 'function') {
67+
acc[key] = attrProp(executionContext)
68+
} else if (attr.hasOwnProperty(key)) {
69+
acc[key] = attrProp
70+
}
71+
}
72+
73+
return acc
74+
}, {})
75+
76+
const newProps = (Object.assign(attrs, props): any)
77+
newProps.className = props.className || ''
78+
newProps.style = props.style
79+
? Object.assign({}, attrs.style, props.style)
80+
: attrs.style
81+
return newProps
82+
}
83+
84+
/** Checks whether a ForwardRefElement is a StyledComponent element */
85+
export const isStyledElement = (element: ForwardRefElement): boolean %checks =>
86+
styledComponents !== undefined &&
87+
typeof element.type.target !== undefined &&
88+
typeof element.type.styledComponentId === 'string'
89+
90+
/** This is an optimised faux mounting strategy for StyledComponents.
91+
It is only enabled when styled-components is installed and the component
92+
can safely be skipped */
93+
export const mount = (element: ForwardRefElement): AbstractElement[] => {
94+
// Imitate styled-components' attrs props without computing styles
95+
const type = ((element.type: any): StyledComponentStatics)
96+
const attrs: Attr[] = Array.isArray(type.attrs) ? type.attrs : [type.attrs]
97+
const computedProps = computeProps(element.props, type.defaultProps)
98+
const theme = computeTheme(element.props, type)
99+
const props = computeAttrsProps(attrs, computedProps, theme)
100+
const as = props.as || type.target
101+
const children = computedProps.children || null
102+
103+
// StyledComponents rendering DOM elements can safely be skipped like normal DOM elements
104+
if (typeof as === 'string') {
105+
return getChildrenArray(children)
106+
} else {
107+
delete props.as
108+
return [(createElement((as: any), props, children): any)]
109+
}
110+
}

src/types/element.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export type ForwardRefElement = {
104104
type: {
105105
render: ComponentType<DefaultProps> & ComponentStatics,
106106
$$typeof: typeof REACT_FORWARD_REF_TYPE,
107+
defaultProps?: Object,
107108
// styled-components specific properties
108109
styledComponentId?: string,
109110
target?: ComponentType<mixed> | string

src/types/frames.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ export type HooksFrame = BaseFrame & {
3939
export type YieldFrame = BaseFrame & {
4040
kind: 'frame.yield',
4141
children: AbstractElement[][],
42-
index: number[],
4342
map: Array<void | ContextMap>,
4443
store: Array<void | ContextEntry>
4544
}

0 commit comments

Comments
 (0)