Skip to content

Commit

Permalink
perf(system): improve perf x4
Browse files Browse the repository at this point in the history
Before:

@xstyled/system x 168,611 ops/sec ±0.71% (86 runs sampled)
styled-system x 595,936 ops/sec ±1.10% (89 runs sampled)
Fastest is styled-system

After:

@xstyled/system x 707,654 ops/sec ±0.69% (88 runs sampled)
styled-system x 589,760 ops/sec ±1.10% (88 runs sampled)
Fastest is @xstyled/system
  • Loading branch information
gregberge committed Jul 7, 2019
1 parent b6324f2 commit 996b773
Show file tree
Hide file tree
Showing 11 changed files with 592 additions and 294 deletions.
16 changes: 8 additions & 8 deletions benchmarks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@
"start": "webpack-dev-server"
},
"devDependencies": {
"@babel/core": "^7.4.5",
"@babel/preset-env": "^7.4.5",
"@babel/core": "^7.5.0",
"@babel/preset-env": "^7.5.0",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.6",
"benchmark": "^2.1.4",
"html-webpack-plugin": "^3.2.0",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"webpack": "^4.32.2",
"webpack-cli": "^3.3.2",
"webpack-dev-server": "^3.5.1"
"webpack": "^4.35.2",
"webpack-cli": "^3.3.5",
"webpack-dev-server": "^3.7.2"
},
"dependencies": {
"@xstyled/styled-components": "^1.0.3",
"styled-components": "^4.2.1",
"styled-system": "^5.0.2"
"@xstyled/styled-components": "^1.5.2",
"styled-components": "^4.3.2",
"styled-system": "^5.0.12"
}
}
4 changes: 3 additions & 1 deletion benchmarks/system.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const Benchmark = require('benchmark')
const xsys = require('@xstyled/system')
const sys = require('styled-system')

// Benchmark.options.maxTime = 0.2

const xsysSystem = xsys.compose(
xsys.fontSize,
xsys.space,
Expand All @@ -13,7 +15,7 @@ const sysSystem = sys.compose(
sys.space,
)

const suite = new Benchmark.Suite()
const suite = new Benchmark.Suite('systems')

const xSysValue = {
theme: {},
Expand Down
641 changes: 424 additions & 217 deletions benchmarks/yarn.lock

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions packages/system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
"access": "public"
},
"dependencies": {
"@babel/runtime": "^7.4.4",
"deepmerge": "^3.2.0"
"@babel/runtime": "^7.4.4"
}
}
6 changes: 4 additions & 2 deletions packages/system/src/media.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ export function getBreakpoints(props) {
return DEFAULT_BREAKPOINTS
}

export const mediaMinWidth = value => `@media (min-width: ${value})`
export const mediaMaxWidth = value => `@media (max-width: ${value})`
export const mediaMinWidth = value =>
value ? `@media (min-width: ${value})` : null
export const mediaMaxWidth = value =>
value ? `@media (max-width: ${value})` : null
export const mediaBetweenWidth = (min, max) =>
`@media (min-width: ${min}) and (max-width: ${max})`

Expand Down
151 changes: 101 additions & 50 deletions packages/system/src/style.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,77 @@
/* eslint-disable no-continue, no-underscore-dangle, no-restricted-syntax, guard-for-in, no-multi-assign */
import { getBreakpoints, getBreakpointMin, mediaMinWidth } from './media'
import {
is,
num,
string,
obj,
merge,
getThemeValue,
warn,
identity,
merge,
assign,
} from './util'

const transformOptions = {}
const cacheSupported =
typeof Map !== 'undefined' && typeof WeakMap !== 'undefined'
const caches = cacheSupported ? new WeakMap() : null
function getThemeCache(theme) {
if (caches.has(theme)) return caches.get(theme)
const cache = {}
caches.set(theme, cache)
return cache
}

const noopCache = {
has: () => false,
set: () => {},
get: () => {},
}

function getCacheNamespace(theme, namespace) {
if (!cacheSupported || !theme) return noopCache
const cache = getThemeCache(theme)
cache[namespace] = cache[namespace] || new Map()
return cache[namespace]
}

export const themeGetter = ({ transform, key, defaultVariants }) => value => {
return props => {
let themeGetterId = 0
export const themeGetter = ({ transform, key, defaultVariants }) => {
const id = themeGetterId++
return value => props => {
if (!string(value) && !num(value)) return value
const cache = getCacheNamespace(props.theme, `__themeGetter${id}`)
if (cache.has(value)) return cache.get(value)
let variants = is(key) ? getThemeValue(props, key) : null
variants = is(variants) ? variants : defaultVariants
const themeValue = is(variants)
? getThemeValue(props, value, variants)
: null
const computedValue = is(themeValue) ? themeValue : value
if (!transform) return computedValue
transformOptions.rawValue = value
transformOptions.variants = variants
return transform(computedValue, transformOptions)
if (!transform) {
cache.set(value, computedValue)
return computedValue
}
const transformedValue = transform(computedValue, {
rawValue: value,
variants,
})
cache.set(value, transformedValue)
return transformedValue
}
}

function styleFromValue(cssProperties, value, props, themeGet) {
function styleFromValue(cssProperties, value, props, themeGet, cache) {
if (obj(value)) return null
if (cache.has(value)) return cache.get(value)
const computedValue = themeGet(value)(props)
if (string(computedValue) || num(computedValue)) {
const style = {}
for (let i = 0; i < cssProperties.length; i++) {
style[cssProperties[i]] = computedValue
}
return style
if (!string(computedValue) && !num(computedValue)) return null
const style = {}
for (const key in cssProperties) {
style[cssProperties[key]] = computedValue
}
return null
cache.set(value, style)
return style
}

export function createStyleGenerator(getStyle, props, generators) {
Expand All @@ -45,51 +80,68 @@ export function createStyleGenerator(getStyle, props, generators) {
getStyle,
generators,
}

return getStyle
}

export function reduceBreakpoints(props, values, getStyle = identity) {
function getMedias(props) {
const breakpoints = getBreakpoints(props)
const keys = Object.keys(values)
let allStyle = {}
for (let i = 0; i < keys.length; i++) {
const breakpoint = keys[i]
const style = getStyle(values[breakpoint])
const medias = {}
for (const breakpoint in breakpoints) {
medias[breakpoint] = mediaMinWidth(
getBreakpointMin(breakpoints, breakpoint),
)
}
return medias
}

if (style !== null) {
const breakpointValue = getBreakpointMin(breakpoints, breakpoint)
function getCachedMedias(props, cache) {
if (cache.has('_medias')) {
return cache.get('_medias')
}
const medias = getMedias(props)
cache.set('_medias', medias)
return medias
}

if (breakpointValue === null) {
allStyle = merge(allStyle, style)
} else {
allStyle = merge(allStyle, {
[mediaMinWidth(breakpointValue)]: style,
})
}
export function reduceBreakpoints(props, values, getStyle = identity, cache) {
const medias = cache ? getCachedMedias(props, cache) : getMedias(props)
let styles = {}
for (const breakpoint in values) {
const style = getStyle(values[breakpoint])
if (style === null) continue
const media = medias[breakpoint]
if (media === null) {
styles = merge(styles, style)
} else {
styles[media] = styles[media] ? assign(styles[media], style) : style
}
}
return allStyle
return styles
}

function getStyleFactory(prop, cssProperties, themeGet) {
return function getStyle(props) {
const value = props[prop]
if (!is(value)) return null

const style = styleFromValue(cssProperties, value, props, themeGet)

if (style !== null) {
return style
}
const cache = getCacheNamespace(props.theme, prop)

if (obj(value)) {
return reduceBreakpoints(props, value, breakpointValue =>
styleFromValue(cssProperties, breakpointValue, props, themeGet),
return reduceBreakpoints(
props,
value,
breakpointValue =>
styleFromValue(
cssProperties,
breakpointValue,
props,
themeGet,
cache,
),
cache,
)
}

return null
return styleFromValue(cssProperties, value, props, themeGet, cache)
}
}

Expand Down Expand Up @@ -123,17 +175,15 @@ export function compose(...generators) {
const generatorsByProp = indexGeneratorsByProp(flatGenerators)

function getStyle(props) {
const propKeys = Object.keys(props)
const propCount = propKeys.length
let allStyle = {}
for (let i = 0; i < propCount; i++) {
const propKey = propKeys[i]
const generator = generatorsByProp[propKey]
const styles = {}
for (const key in props) {
const generator = generatorsByProp[key]
if (generator) {
allStyle = merge(allStyle, generator.meta.getStyle(props))
const style = generator.meta.getStyle(props)
merge(styles, style)
}
}
return allStyle
return styles
}

const props = flatGenerators.reduce(
Expand Down Expand Up @@ -162,6 +212,7 @@ export function style({
),
)
}

themeGet = themeGet || themeGetter({ key, transform })
const getStyle = getStyleFactory(prop, cssProperties, themeGet)
return createStyleGenerator(getStyle, [prop])
Expand Down
25 changes: 19 additions & 6 deletions packages/system/src/util.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable no-console */
import deepmerge from 'deepmerge' // < 1kb payload overhead when lodash/merge is > 3kb.

const DEV = process.env.NODE_ENV !== 'production'

Expand All @@ -24,13 +23,27 @@ export const get = (from, path) => {
return result
}

export const merge = (acc, item) => {
if (!is(item)) {
return acc
export const assign = (a, b) => {
if (!is(b)) return a
// eslint-disable-next-line no-restricted-syntax, guard-for-in
for (const key in b) {
a[key] = b[key]
}
return a
}

// No need to clone deep, it's way faster.
return deepmerge(acc, item, { clone: false })
export const merge = (a, b) => {
if (!is(b)) return a
// eslint-disable-next-line no-restricted-syntax
for (const key in b) {
// eslint-disable-next-line no-continue
if (obj(a[key])) {
a[key] = merge(assign({}, a[key]), b[key])
} else {
a[key] = b[key]
}
}
return a
}

export const warn = (condition, message) => {
Expand Down
26 changes: 22 additions & 4 deletions packages/system/src/util.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
negative,
get,
merge,
assign,
cascade,
getThemeValue,
} from './util'
Expand Down Expand Up @@ -98,15 +99,32 @@ describe('util', () => {
})
})

describe('#merge', () => {
it('merges an item into another', () => {
describe('#assign', () => {
it('assigns an item into another', () => {
const a = { x: 1 }
const b = { y: 2 }
const result = merge(a, b)
const result = assign(a, b)
expect(result).toEqual({ x: 1, y: 2 })
expect(result).toBe(a)
})

it('supports null as second attribute', () => {
const a = { x: 1 }
const result = assign(a, null)
expect(result).toBe(a)
})
})

describe('#merge', () => {
it('merges an item into another', () => {
const a = { x: 1, z: { a: 1 } }
const b = { y: 2, z: { b: 2 } }
const result = merge(a, b)
expect(result).toEqual({ x: 1, y: 2, z: { a: 1, b: 2 } })
expect(result).toBe(a)
})

it('returns the first one if the second is not defined', () => {
it('supports null as second attribute', () => {
const a = { x: 1 }
const result = merge(a, null)
expect(result).toBe(a)
Expand Down
4 changes: 2 additions & 2 deletions packages/system/src/variant.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getThemeValue, merge, warn, is } from './util'
import { getThemeValue, merge, warn, is, assign } from './util'

export const variant = ({
key = null,
Expand All @@ -7,7 +7,7 @@ export const variant = ({
prop = 'variant',
}) => props => {
const themeVariants = is(key) ? getThemeValue(props, key) : null
const computedVariants = merge(variants, themeVariants)
const computedVariants = merge(assign({}, variants), themeVariants)
const value = props[prop] !== undefined ? props[prop] : defaultValue
const result = getThemeValue(props, value, computedVariants)
warn(is(result), `variant "${value}" not found`)
Expand Down

0 comments on commit 996b773

Please sign in to comment.