Potential 5x perf improvement for stylesheet updates by switching to `CSSStyleSheet.replaceSync`
See original GitHub issueThe problem
Currently, hot module reloading pages that use Emotion with React is about 8x slower than using CSSStyleSheet.replaceSync
to update styles.
Here are two Chrome profiles where the same CSS is updated on disk 1024 times, sleeping for 32ms between each update. In the first case, it’s a <Global>
React component, and in the second case, it’s a <link>
tag being hot reloaded
with-emotion-react.json.gz - this one is using Emotion and the React Component is being re-imported each time. Note the difference in time spent on Rendering between the two profiles.
with-replaceSync.json.gz - this one is using CSSStyleSheet.replaceSync
This is the CSS:
:root {
--timestamp: "16336741341477";
--interval: "32";
--progress-bar: 56.889%;
--spinner-1-muted: rgb(179, 6, 202);
--spinner-1-primary: rgb(224, 8, 253);
--spinner-2-muted: rgb(22, 188, 124);
--spinner-2-primary: rgb(27, 235, 155);
--spinner-3-muted: rgb(89, 72, 0);
--spinner-3-primary: rgb(111, 90, 0);
--spinner-4-muted: rgb(18, 84, 202);
--spinner-4-primary: rgb(23, 105, 253);
--spinner-rotate: 304deg;
}
This is the React component using Emotion:
import { Global } from "@emotion/react";
export function CSSInJSStyles() {
return (
<Global
styles={`
:root {
--timestamp: "16336721342556";
--interval: "32";
--progress-bar: 56.889%;
--spinner-1-muted: rgb(179, 6, 202);
--spinner-1-primary: rgb(224, 8, 253);
--spinner-2-muted: rgb(22, 188, 124);
--spinner-2-primary: rgb(27, 235, 155);
--spinner-3-muted: rgb(89, 72, 0);
--spinner-3-primary: rgb(111, 90, 0);
--spinner-4-muted: rgb(18, 84, 202);
--spinner-4-primary: rgb(23, 105, 253);
--spinner-rotate: 304deg;
} `}
/>
);
}
Proposed solution
Detect if CSSStyleSheet.replaceSync
is supported in the current browser and use that to update the existing stylesheet (rather than creating a new one). This would work for both development and production (in production for dynamic styles).
Drawbacks:
- This API is only available in Chromium browsers. Multiple “backends” for updating styles introduces complexity
@import
is not supported withCSSStyleSheet.replaceSync
Alternative solutions
Additional context
replaceSync
has some cost, but it’s not so bad:
Versus:
Incase opening the profiles are a pain, here are screenshots.
Emotion:
replaceSync:
Issue Analytics
- State:
- Created 2 years ago
- Reactions:12
- Comments:5 (3 by maintainers)
Top GitHub Comments
Thank you for the issue - TIL about constructable stylesheets (I knew there was such a thing for Shadow DOM but never occurred to me that I could use it for the document itself).
Some additional problems that I’m seeing with:
document.adoptedStyleSheets
seems to be really cumbersome - before each manipulation, we’d have to find our style sheet in that and create the appropriate new version of that in an immutable way. Not that big of a problem implementation-wise but it’s a little bit quirky that we can’t just manipulate our stuff without caring about what other scripts do on the page 😢I wonder if maybe there are some perf gains to be had by keeping
style
elements and only swapping the rules contained in them.Note that this whole thing only matters for Global styles since those are the only ones that are “replaceable”. I wonder if there are really any real-world perf gains here or if this is just more of a theoretical problem when looking at this with the perspective of a stress test.
btw FWIW on the original issue about manipulating adoptedStylesheets: the implementation agreed upon across browsers (and shipped in chrome 99) is no longer a FrozenArray but rather an ObservableArray so immutability is not an issue anymore.