Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: significantly speed up styled-components in dev mode #7440

Merged
merged 1 commit into from
Aug 30, 2024

Conversation

stipsan
Copy link
Member

@stipsan stipsan commented Aug 29, 2024

TL;DR – 1 LOC makes sanity dev render 10 times faster 🤯

Description

This PR enables styled-components "speedy mode" in sanity dev by explicitly setting the ‎SC_DISABLE_SPEEDY flag to false. By default, styled-components have different modes for inserting CSS rules, choosing which mode to use based on whether process.env.NODE_ENV is set to production or not. When NODE_ENV is production, it uses the "speedy mode," which significantly boosts performance. Otherwise, it falls back to a slower mode.

I discovered this performance difference while working on an unrelated benchmark in the concurrent-styled-components repository. I noticed that styled-components had drastically different performance results depending on whether I ran next dev or next build && next start. After adding ‎SC_DISABLE_SPEEDY: 'false' in my next.config.ts, the performance in development mode dramatically improved, almost matching production levels (it does match if reactStrictMode is false).

How are CSS rules inserted when NODE_ENV is development?

styled-components is a mature library that has been around for a long time. Initially, CSS-in-JS libraries inserted styles by adding text nodes to a <style> tag:

document
  .querySelector('style[data-styled="active"]')
  .appendChild(
    document.createTextNode(
      'body { transform: scale(0.5) }',
    ),
  )

When using this method, you can open the <style> tag in DevTools and inspect the text nodes:

Pasted Graphic

To confirm this, run the following snippet to see a bunch of text:

document.querySelector('style[data-styled="active"]')
  .innerText

The downside is that every time a text node is added to the <style>, the browser has to parse the entire innerText. In large applications like Sanity Studio, this can start off at almost half a megabyte, and as more CSS is loaded, it only grows larger, taking even longer to parse.

What is "speedy mode"?

"Speedy mode" was introduced in v3.1.0. It uses the CSSOM API CSSStyleSheet.insertRule instead of text nodes:

document
  .querySelector('style[data-styled="active"]')
  .sheet.insertRule('body { transform: scale(0.5) }')

With "speedy mode" enabled, the <style> tag appears empty in DevTools:

Pasted Graphic 1

To verify "speedy mode," inspect the cssRules:

document.querySelector('style[data-styled="active"]').sheet
  .cssRules

However, cssRules alone isn't a sufficient indicator of "speedy mode" because it's also available when using text nodes. You need to check that innerText is empty while cssRules has entries:

const node = document.querySelector(
  'style[data-styled="active"]',
)
const isSpeedySc =
  node.innerText.length === 0 &&
  node.sheet.cssRules.length > 0

Why isn't "speedy mode" always enabled?

  • "Speedy mode" was introduced in v3.1.0 and set to be enabled only when NODE_ENV is not development because, while browser support for CSSStyleSheet.insertRule was good (available since v1 versions of Safari, Chrome, Firefox, and IE v9), it was poorly supported in testing environments like jsdom, such as in Jest. For example, jest-styled-components broke in v3.1.0 and was patched in v3.1.3.

  • Poor DevTools support was another reason it wasn't enabled by default in development. Chrome added support for editing rules in "speedy mode" in version 81, released in April 2020, and Firefox also supports it now. With Speedy disabled, developers could edit rules directly from generated CSS-in-JS in DevTools:
    editable safari

  • Safari, however, still does not allow editing rules with CSSStyleSheet.insertRule in DevTools. It only allows inspecting them:
    not editable safari
    Given Safari already lacks a React DevTools extension, the argument against enabling "speedy mode" in development for Safari users becomes weaker. Developers who need to edit styles can use Chrome or Firefox, which they typically already use as their main development browsers.

What's the purpose of the SC_DISABLE_SPEEDY flag?

In v4.1.0, the SC_DISABLE_SPEEDY flag was added to disable "speedy mode" in production environments due to issues such as the Yandex crawler not supporting CSSOM styles. The idea was that in production, you typically want "speedy mode" enabled for better performance, but in certain edge cases, you can disable it.

Over time, more flags were added to support popular bundler setups, as shown in the constants file:

  • process.env.REACT_APP_SC_DISABLE_SPEEDY
  • process.env.SC_DISABLE_SPEEDY

It is counterintuitive because while the flag’s name suggests disabling "speedy mode," we use it to force-enable "speedy mode" in development where it is off by default. This double-negative logic can be confusing, but it provides a way to achieve consistent behavior between development and production.

Despite recent improvements in browser DevTools, not all environments support editing dynamically generated and injected CSS rules. Chrome and Firefox have full editing capabilities, while Safari remains read-only for such rules. This limitation is one of the reasons styled-components maintainers originally chose to disable "speedy mode" by default in development while enabling it in production, where such editing capabilities are less critical. However, given the current landscape of browser support, enabling "speedy mode" in development aligns with modern expectations and usage.

What to review

  • Run sanity dev and verify that "speedy mode" is enabled:

    const node = document.querySelector('style[data-styled="active"]')
    console.log(node.innerText.length === 0 && node.sheet.cssRules.length > 0 ? 'Speedy is enabled 🎉' : 'Speedy is disabled 😞')
  • When clicking around in the Studio, cold start performance should be significantly and noticeably improved. Clicking on a tool that lazy-loads, like Vision, should reach first render faster. Opening popovers, dialogs, new screens, and UI should all be faster. Re-render performance on components that respond to input and render changed CSS (e.g., hovering inputs that change border color, hovering a tooltip, typing in an input that changes to invalid status, typing into a search field) should also be much faster.

Testing

The performance profiling on Vision Tool shows that before applying the ‎SC_DISABLE_SPEEDY flag, the VisionGui component's render was significantly faster than 30 other components. After applying the flag, VisionGui's render time remained the same (2.9ms), but all other styled components in that render pass became significantly faster, making VisionGui the slowest component. This change results in a substantial performance boost, especially for views with many styled components, as seen in the attached profiles.

  • Before:
    Pasted Graphic 4

  • After:
    Pasted Graphic 5

You can load the profiling JSON files into React DevTools using the "Load profile" button for further analysis:

They were produced by running pnpm dev and clicking "Reload and start profiling" on http://localhost:3333/test/vision

Notes for release

This change enables styled-components "speedy mode" in sanity dev, improving developer mode performance. This change impacts only development builds (sanity dev), and production builds (sanity build or sanity start) remain unaffected since "speedy mode" has always been enabled in production.

Developers embedding Sanity Studio in other frameworks like Next.js or Remix need to adjust their own build tooling to declare the SC_DISABLE_SPEEDY flag to achieve the same performance benefits. For example, Next.js users can add this snippet to their next.config.{js,mjs,ts} file:

export default {
  env: {
    SC_DISABLE_SPEEDY: 'false' // makes styled-components as fast in dev mode as it is in production mode
  }
}

For Safari users, while they will still be able to inspect and see CSS rules coming from styled-components, these will now be read-only in the DevTools inspector. The performance benefits from enabling "speedy mode" in development mode are significant for Safari users and outweigh the convenience of being able to edit these rules directly in the inspector. With Hot Module Reload, developers can still quickly make changes to their source code, and the new styles will apply immediately.

Enabling "speedy mode" offers a faster development experience across the board. Developers using Safari for testing purposes can still verify the functionality of the studio, and for more detailed CSS editing, they can switch to Chrome, Firefox, or other Chromium-based browsers like Arc or Brave.

It's still possible to restore the old behaviour of disabling speedy in dev mode in userland with a custom sanity.cli.ts override:

import {defineCliConfig} from 'sanity/cli'
import {type UserConfig} from 'vite'

export default defineCliConfig({
  vite(viteConfig: UserConfig): UserConfig {
    return {
      ...viteConfig,
      define: {
        ...viteConfig.define,
        // `sanity dev` enables speedy in both development and production, this line restores the default `styled-components` behaviour of only enabling it in production
        'process.env.SC_DISABLE_SPEEDY': JSON.stringify(process.env.NODE_ENV !== 'production'),
      },
    }
  },
})

@stipsan stipsan requested a review from a team as a code owner August 29, 2024 17:25
@stipsan stipsan requested review from ricokahler and removed request for a team August 29, 2024 17:25
Copy link

vercel bot commented Aug 29, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
page-building-studio ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 29, 2024 5:25pm
performance-studio ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 29, 2024 5:25pm
test-compiled-studio ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 29, 2024 5:25pm
test-next-studio ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 29, 2024 5:25pm
test-studio ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 29, 2024 5:25pm
1 Skipped Deployment
Name Status Preview Comments Updated (UTC)
studio-workshop ⬜️ Ignored (Inspect) Aug 29, 2024 5:25pm

Copy link
Contributor

No changes to documentation

@stipsan stipsan enabled auto-merge August 29, 2024 17:33
@sanity-io sanity-io deleted a comment from github-actions bot Aug 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants