Skip to content

Commit d305c2a

Browse files
committed
Add proper styled-components rendering optimisation
This implements specific rendering logic that skips StyledComponents entirely, if a new enough version of styled-component is installed and the optimisation can be applied. It does this by computing "attrs" manually and skipping the styled-components logic entirely by rendering the target directly.
1 parent 96105c5 commit d305c2a

File tree

3 files changed

+116
-14
lines changed

3 files changed

+116
-14
lines changed

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

src/visitor.js

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import { typeOf, shouldConstruct, getChildrenArray } from './element'
66
import {
77
mountFunctionComponent,
88
mountClassComponent,
9-
mountLazyComponent
9+
mountLazyComponent,
10+
mountStyledComponent,
11+
isStyledElement
1012
} from './render'
1113

1214
import type {
@@ -62,6 +64,9 @@ import {
6264
// the event loop is not interrupted for too long
6365
const YIELD_AFTER_MS = process.env.NODE_ENV !== 'production' ? 20 : 5
6466

67+
// A no-op function for styled-components' ComponentStyle class
68+
const NOOP_GEN_CLASSNAME = () => ''
69+
6570
const render = (
6671
type: ComponentType<DefaultProps> & ComponentStatics,
6772
props: DefaultProps,
@@ -132,21 +137,19 @@ export const visitElement = (
132137

133138
case REACT_FORWARD_REF_TYPE: {
134139
const refElement = ((element: any): ForwardRefElement)
135-
if (
136-
typeof refElement.type.styledComponentId === 'string' &&
137-
typeof refElement.type.target !== 'function'
138-
) {
139-
// This is an optimization that's specific to styled-components
140-
// We can safely skip them if they're not wrapping a component
141-
return getChildrenArray(refElement.props.children)
140+
141+
// If we find a StyledComponent, we trigger a specific optimisation
142+
// that allows quick rendering of them without computing styles
143+
let child = null
144+
if (isStyledElement(refElement)) {
145+
child = mountStyledComponent(refElement, queue, visitor)
142146
} else {
143-
const {
144-
props,
145-
type: { render }
146-
} = refElement
147-
const child = mountFunctionComponent(render, props, queue, visitor)
148-
return getChildrenArray(child)
147+
const { render } = refElement.type
148+
const props = refElement.props
149+
child = mountFunctionComponent(render, props, queue, visitor)
149150
}
151+
152+
return getChildrenArray(child)
150153
}
151154

152155
case REACT_ELEMENT_TYPE: {

0 commit comments

Comments
 (0)