Skip to content

Commit

Permalink
feat: testing multiple experimental perf boosting strategies
Browse files Browse the repository at this point in the history
  • Loading branch information
stipsan committed Jul 9, 2024
1 parent 24c8c96 commit 423e9d5
Show file tree
Hide file tree
Showing 38 changed files with 12,696 additions and 353 deletions.
270 changes: 270 additions & 0 deletions dev/test-studio/components/debugStyledComponents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import {
// useCallback,
useEffect,
useInsertionEffect,
// useRef,
useState,
// useSyncExternalStore,
} from 'react'
import {definePlugin, type LayoutProps} from 'sanity'
import {__PRIVATE__, StyleSheetManager} from 'styled-components'

const IS_BROWSER = typeof window !== 'undefined' && 'HTMLElement' in window

export const debugStyledComponents = definePlugin({
name: 'debug-styled-components',
studio: {
components: {
layout: DebugLayout,
},
},
})

function DebugLayout(props: LayoutProps) {
const {renderDefault} = props
const [, setTick] = useState(1)
// const [onStoreChange, setOnStoreChange] = useState(() => () => {})
// const onBufferRef = useRef(onStoreChange)
// useEffect(() => {
// onBufferRef.current = onStoreChange
// }, [onStoreChange])
const [blazingSheet] = useState(
() =>
new BlazingStyleSheet({
// Schedule state updates when the buffer is queued
onBuffer: () => {
// console.log('onBuffer')
setTick((prev) => prev + 1)
},
// onBuffer: () => onBufferRef.current(),
// Reconstruct default options
// https://github.com/styled-components/styled-components/blob/770d1fa2bc1a4bfe3eea1b14a0357671ba9407a4/packages/styled-components/src/sheet/Sheet.ts#L23-L26
isServer: !IS_BROWSER,
useCSSOMInjection: true,
}),
)
// const shouldFlush = useSyncExternalStore(
// useCallback((_onStoreChange) => {
// setOnStoreChange(() => _onStoreChange)
// return () => setOnStoreChange(() => () => {})
// }, []),
// () => blazingSheet.shouldFlush(),
// () => true,
// )
const [enabled, setEnabled] = useState(true)
const [flush, setFlush] = useState(true)
const [namespace, setNamespace] = useState<string | undefined>()
const [disableCSSOMInjection, setDisableCSSOMInjection] = useState<boolean | undefined>()
const [enableVendorPrefixes, setEnableVendorPrefixes] = useState<boolean | undefined>()

useEffect(() => {
// @ts-expect-error -- debug global
window.cody = {
setNamespace,
setDisableCSSOMInjection,
setEnableVendorPrefixes,
setEnabled,
toggle: () => setFlush((prev) => !prev),
}
return () => {
// @ts-expect-error -- debug global
delete window.cody
}
}, [])

// useEffect(() => {
// console.log({
// blazingSheet,
// namespace,
// disableCSSOMInjection,
// enableVendorPrefixes,
// enabled,
// // shouldFlush,
// })
// }, [blazingSheet, disableCSSOMInjection, enableVendorPrefixes, enabled, namespace])

// Pause event emitter during render:
// https://github.com/final-form/react-final-form/issues/751#issuecomment-689431448
blazingSheet.pauseEvents()

// Update CSSOM
useInsertionEffect(() => {
if (flush) {
blazingSheet.flush()
}
})

// Check if CSSOM should update
// eslint-disable-next-line react-hooks/exhaustive-deps -- @TODO rewrite to use useState to buffer up changes that should flush

Check warning on line 98 in dev/test-studio/components/debugStyledComponents.tsx

View workflow job for this annotation

GitHub Actions / lint

React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior
useEffect(() => {
if (flush) {
if (blazingSheet.shouldFlush()) {
// console.log('Flush in side-effect!')
setTick((prev) => prev + 1)
}
blazingSheet.resumeEvents()
}
})

if (!enabled) {
return renderDefault(props)
}

return (
<StyleSheetManager sheet={blazingSheet}>
<StyleSheetManager
namespace={namespace}
disableCSSOMInjection={disableCSSOMInjection}
enableVendorPrefixes={enableVendorPrefixes}
>
{renderDefault(props)}
</StyleSheetManager>
</StyleSheetManager>
)
}

// @TODO refactor to wrap around mainSheet with a proxy that queues up insertRule and deleteRule calls
const {StyleSheet} = __PRIVATE__

const EMPTY_OBJECT = Object.freeze({}) as Readonly<{[key: string]: any}>

class BlazingStyleSheet extends StyleSheet {
#buffer: (
| {type: 'insertRules'; payload: [id: string, name: string, rules: string | string[]]}
| {type: 'clearRules'; payload: [id: string]}
)[] = []
#flushing = false
#onBuffer: any
#paused = true

constructor(
options: ConstructorParameters<typeof StyleSheet>[0] & {onBuffer?: any} = EMPTY_OBJECT,
globalStyles: ConstructorParameters<typeof StyleSheet>[1] = {},
names?: ConstructorParameters<typeof StyleSheet>[2],
) {
super(options, globalStyles, names)

if (options.onBuffer) {
this.#onBuffer = options.onBuffer
}
}

/**
* Overriding this method is necessary, as it has a hardcoded call with the `StyleSheet` constructor
*/
override reconstructWithOptions(
options: Parameters<InstanceType<typeof StyleSheet>['reconstructWithOptions']>[0],
withNames = true,
) {
return new BlazingStyleSheet(
{onBuffer: this.#onBuffer, ...this.options, ...options},
this.gs,
(withNames && this.names) || undefined,
)
}
/**
* Overriding `getTag`, original implementation: https://github.com/styled-components/styled-components/blob/770d1fa2bc1a4bfe3eea1b14a0357671ba9407a4/packages/styled-components/src/sheet/Sheet.ts#L76-L79
*
* There are two main performance bottlenecks in styled-components:
* 1. Regular styled components (the result of \`const StyledComponent = styled.div\`\`\`) updates CSS during render:
* https://github.com/styled-components/styled-components/blob/770d1fa2bc1a4bfe3eea1b14a0357671ba9407a4/packages/styled-components/src/models/StyledComponent.ts#L64-L68
* 2. Global styled components (the result of `const GlobalStyle = createGlobalStyle``) updates CSS using \`useLayoutEffect\`:
* https://github.com/styled-components/styled-components/blob/770d1fa2bc1a4bfe3eea1b14a0357671ba9407a4/packages/styled-components/src/constructors/createGlobalStyle.ts#L52-L57
*
* An attempt to moving to `useInsertionEffect` were made in 2022, but little activity since then:
* https://github.com/styled-components/styled-components/pull/3821
*
* This custom version of `StyleSheet` allows us to intercept either:
* a) writes to CSSOM: https://github.com/styled-components/styled-components/blob/770d1fa2bc1a4bfe3eea1b14a0357671ba9407a4/packages/styled-components/src/sheet/Tag.ts#L34
* b) writes to the DOM that triggers CSSOM updates: https://github.com/styled-components/styled-components/blob/770d1fa2bc1a4bfe3eea1b14a0357671ba9407a4/packages/styled-components/src/sheet/Tag.ts#L73-L75
* Option b) is only used if disableCSSOMInjection is set to true on a parent StyleSheetManager component.
*
* By wrapping the group tag, and its internal tag models, we can intercept and buffer writes to the CSSOM,
* and flush them in a `useInsertionEffect`, allowing React to optimize them and orchestrate better.
*/
override getTag() {
if (this.tag) {
return this.tag
}
const groupedTag = super.getTag()
const {tag} = groupedTag
const proxyTag = new Proxy(tag, {
get: (target, prop, receiver) => {
if (prop === 'insertRule' || prop === 'deleteRule' || prop === 'getRule') {
// console.log('Tag.get()', prop, {target, receiver}, this.#buffer)
}
return Reflect.get(target, prop, receiver)
},
})
groupedTag.tag = proxyTag
// console.log('getTag() is called', {tag, groupedTag}, this, document?.querySelector('style'))
return groupedTag
}
/**
* Flush all `insertRules` and `clearRules` from the buffer,
* this should happen during a `useInsertionEffect` or similar as updating CSSOM is intensive.
*/
flush() {
try {
this.#flushing = true
while (this.#buffer.length > 0) {
const {type, payload} = this.#buffer.shift()!
switch (type) {
case 'insertRules':
this.insertRules(...payload)
break
case 'clearRules':
this.clearRules(...payload)
break
default:
throw new TypeError(`Unknown buffer type: ${type}`, {cause: {type, payload}})
}
}
} catch (err) {
console.error('Something crashed during flushing', err)
} finally {
this.#flushing = false
}
}
shouldFlush() {
if (this.#flushing) {
throw new TypeError('Cannot flush while flushing')
}
return this.#buffer.length > 0
}
/**
* Handle React constraint of not being allowed to call setState during render of another component (`styled` components call `insertStyles` during render, so it cannot trigger a state setter)
*/
pauseEvents() {
// console.count('pauseEvents')
this.#paused = true
}
resumeEvents() {
// console.count('resumeEvents')
this.#paused = false
}
override insertRules(id: string, name: string, rules: string | string[]): void {
if (this.#flushing) {
// console.count(`Flushing insertRules(${id}, ${name})`)
super.insertRules(id, name, rules)
} else {
// console.count(`Queueing insertRules(${id}, ${name})`)
this.#buffer.push({type: 'insertRules', payload: [id, name, rules]})
if (!this.#paused) {
this.#onBuffer?.()
}
}
}
override clearRules(id: string): void {
if (this.#flushing) {
// console.count(`Flushing clearRules(${id})`)
super.clearRules(id)
} else {
// console.count(`Queueing clearRules(${id})`)
this.#buffer.push({type: 'clearRules', payload: [id]})
if (!this.#paused) {
this.#onBuffer?.()
}
}
}
}
2 changes: 2 additions & 0 deletions dev/test-studio/sanity.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {imageHotspotArrayPlugin} from 'sanity-plugin-hotspot-array'
import {muxInput} from 'sanity-plugin-mux-input'

import {imageAssetSource} from './assetSources'
import {debugStyledComponents} from './components/debugStyledComponents'
import {
Annotation,
Block,
Expand Down Expand Up @@ -102,6 +103,7 @@ const sharedSettings = definePlugin({
badges: (prev, context) => (context.schemaType === 'author' ? [CustomBadge, ...prev] : prev),
},
plugins: [
debugStyledComponents(),
structureTool({
icon: BookIcon,
structure,
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@
},
"overrides": {
"@typescript-eslint/eslint-plugin": "$@typescript-eslint/eslint-plugin",
"@typescript-eslint/parser": "$@typescript-eslint/parser"
"@typescript-eslint/parser": "$@typescript-eslint/parser",
"@sanity/ui": "2.6.4-canary.0"
}
},
"isSanityMonorepo": true
Expand Down
12 changes: 11 additions & 1 deletion packages/@repo/package.bundle/src/package.bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {version} from '../package.json'
export const defaultConfig: UserConfig = {
appType: 'custom',
define: {
'__DEV__': 'false',
'process.env.PKG_VERSION': JSON.stringify(version),
'process.env.NODE_ENV': '"production"',
'process.env': {},
Expand All @@ -19,7 +20,13 @@ export const defaultConfig: UserConfig = {
formats: ['es'],
},
rollupOptions: {
external: ['react', /^react-dom/, 'react/jsx-runtime', 'styled-components'],
external: [
'react',
/^react-dom/,
'react/jsx-runtime',
'styled-components',
'./checkoutPairWorker.ts',
],
output: {
exports: 'named',
dir: 'dist',
Expand All @@ -30,4 +37,7 @@ export const defaultConfig: UserConfig = {
},
},
},
worker: {
format: 'es',
},
}
3 changes: 3 additions & 0 deletions packages/@repo/package.config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@
"types": "./src/package.config.ts",
"scripts": {
"check:types": "tsc --noEmit --skipLibCheck src/package.config.ts"
},
"dependencies": {
"@web/rollup-plugin-import-meta-assets": "2.2.1"
}
}
14 changes: 14 additions & 0 deletions packages/@repo/package.config/src/package.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {defineConfig} from '@sanity/pkg-utils'
// @ts-expect-error -- missing types
import {importMetaAssets} from '@web/rollup-plugin-import-meta-assets'

export default defineConfig({
define: {
Expand Down Expand Up @@ -29,6 +31,18 @@ export default defineConfig({
legacyExports: true,
rollup: {
optimizeLodash: true,
plugins: ([t1, t2, t3, t4, t5, t6, ...plugins]) => [
t1,
t2,
t3,
t4,
t5,
t6,
importMetaAssets({
include: ['**/checkoutPair.mjs', '**/checkoutPair.ts'],
}),
...plugins,
],
},
tsconfig: 'tsconfig.lib.json',
strictOptions: {
Expand Down
1 change: 1 addition & 0 deletions packages/sanity/.eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ presentation.js
router.js
structure.js
/migrate/*
/web-workers/*
3 changes: 3 additions & 0 deletions packages/sanity/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@
/migrate.js
/_internal.js
/cli.js

/web-workers/**/*.map
/web-workers/**/*.esm.js
1 change: 1 addition & 0 deletions packages/sanity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
"scripts": {
"build": "pkg-utils build --strict --check --clean",
"build:bundle": "vite build --config package.bundle.ts",
"build:web-workers": "vite build --config package.worker.ts",
"check:types": "tsc --project tsconfig.lib.json",
"clean": "rimraf _internal.js _singletons.js cli.js desk.js migrate.js presentation.js router.js structure.js lib",
"coverage": "jest --coverage",
Expand Down
Loading

0 comments on commit 423e9d5

Please sign in to comment.