question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Potential 5x perf improvement for stylesheet updates by switching to `CSSStyleSheet.replaceSync`

See original GitHub issue

The 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.

CleanShot 2021-10-07 at 23 35 22@2x

with-replaceSync.json.gz - this one is using CSSStyleSheet.replaceSync

CleanShot 2021-10-07 at 23 35 11@2x

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 with CSSStyleSheet.replaceSync

Alternative solutions

Additional context

replaceSync has some cost, but it’s not so bad:

CleanShot 2021-10-07 at 23 58 24@2x

Versus:

CleanShot 2021-10-07 at 23 58 53@2x

Incase opening the profiles are a pain, here are screenshots.

Emotion: CleanShot 2021-10-08 at 00 09 54@2x

replaceSync: CleanShot 2021-10-08 at 00 08 16@2x

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:12
  • Comments:5 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
Andaristcommented, Oct 8, 2021

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:

  • it’s hard to control the injection order - they seem to be “prepended” to everything else (any cascading rule can override them). This is probably a good thing and not something that I disagree with on a conceptual level - our global styles are also always “prepended” to our styles (IIRC). The problem is that our global styles are always “grouped” with our “scoped” styles - if people only would use Emotion-based styles then using this would not be a problem at all. However, people tend to use Emotion in combination with other styles and this would have an impact on which styles are applied in the end when dealing with conflicting rules or rules with the same specificity
  • manipulating 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.

0reactions
e111077commented, May 26, 2022

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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

CSSStyleSheet.replaceSync() - Web APIs | MDN
The replaceSync() method of the CSSStyleSheet interface synchronously replaces the content of the stylesheet with the content passed into it ...
Read more >
Why Would Anyone Use Constructible Stylesheets, Anyways?
GitHub logo Potential 5x perf improvement for stylesheet updates by switching to `CSSStyleSheet.replaceSync` #2501 ... Currently, hot module ...
Read more >
Constructable Stylesheets - web.dev
Updates to a shared CSSStyleSheet are applied to all roots into which it has been adopted, and adopting a stylesheet is fast and...
Read more >
CSS Object Model (CSSOM) - W3C
The purposes of the present document are (1) to improve on that prior work ... Does not change during the lifetime of the...
Read more >
On Styling Web Components. Doing it the right way - webf
CSS Object Model is different than DOM and it plays well with Shadow DOM. Encapsulation is the primary reason why you might want...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Hashnode Post

No results found