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 554d579 commit cc03d53
Show file tree
Hide file tree
Showing 40 changed files with 12,915 additions and 464 deletions.
279 changes: 279 additions & 0 deletions dev/test-studio/components/debugStyledComponents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
/* eslint-disable no-console */
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
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>
)
}

/** CSSStyleSheet-like Tag abstraction for CSS rules */
interface Tag {
insertRule(index: number, rule: string): boolean
deleteRule(index: number): void
getRule(index: number): string
length: number
}

// @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 @@ -104,6 +105,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',
},
}
4 changes: 4 additions & 0 deletions packages/@repo/package.config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,9 @@
"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",
"rollup-plugin-web-worker-loader": "1.6.1"
}
}
40 changes: 40 additions & 0 deletions packages/@repo/package.config/src/package.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import {importMetaAssets} from '@web/rollup-plugin-import-meta-assets'

Check failure on line 1 in packages/@repo/package.config/src/package.config.ts

View workflow job for this annotation

GitHub Actions / typeCheck

Could not find a declaration file for module '@web/rollup-plugin-import-meta-assets'. '/home/runner/work/sanity/sanity/node_modules/.pnpm/@web[email protected][email protected]/node_modules/@web/rollup-plugin-import-meta-assets/index.mjs' implicitly has an 'any' type.
import {defineConfig} from '@sanity/pkg-utils'
// import webWorkerLoader from 'rollup-plugin-web-worker-loader'
// import OMT from "@surma/rollup-plugin-off-main-thread";

export default defineConfig({
define: {
Expand Down Expand Up @@ -29,6 +32,43 @@ 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,
],
/*
plugins: ([replace, alias, nodeResolve, commonjs, json, ...plugins]) => {
console.log({replace, alias, nodeResolve, commonjs, json})
return [
importMetaAssets({
// */
//include: ['src//*.ts', '**/*.js'],
// exclude: ['**/worker.ts', '**/features/*.ts']
/*
}),
replace,
alias,
nodeResolve,
commonjs,
json,
// webWorkerLoader({
// 'targetPlatform': 'browser',
// 'extensions': ['.ts', '.tsx', '.js', '.jsx'],
// // regex that matches paths that ends in ?worker`
// 'web-worker': /(\?worker)$/,
// }),
...plugins,
]
},
// */
},
tsconfig: 'tsconfig.lib.json',
strictOptions: {
Expand Down
Loading

0 comments on commit cc03d53

Please sign in to comment.