-
Notifications
You must be signed in to change notification settings - Fork 427
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: testing multiple experimental perf boosting strategies
- Loading branch information
Showing
38 changed files
with
12,696 additions
and
353 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 GitHub Actions / lint
|
||
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?.() | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,3 +6,4 @@ presentation.js | |
router.js | ||
structure.js | ||
/migrate/* | ||
/web-workers/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,3 +30,6 @@ | |
/migrate.js | ||
/_internal.js | ||
/cli.js | ||
|
||
/web-workers/**/*.map | ||
/web-workers/**/*.esm.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.