Skip to content

Conversation

@nmn
Copy link
Collaborator

@nmn nmn commented Jul 14, 2025

What changed / motivation ?

Today all StyleX transformation happens in-place. This usually works fine, but there are edge cases where this breaks down a bit.

  • Using stylex.create within Typescript namespaces doesn't work
  • Dynamic style function recreate the object for static classNames on every call, de-opting caching.

To solve these issues, this PR refactors and add AST utilities for hoisting expressions to the top level of a file and provide an identifier reference to it instead.

As of right now this is only used for two use-cases:

  1. stylex.create can now be defined within blocks such as functions. The compiler will define the actual object at the top level of the file, and use a stable reference where it is defined.
  2. Dynamic styles are tuples of static classNames objects and dynamic inline styles. The static classNames are now hoisted out so they can maintain a static reference.

Tests have been added and updated to show these changes.

@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jul 14, 2025
@github-actions
Copy link

github-actions bot commented Jul 14, 2025

workflow: benchmarks/perf

Comparison of performance test results, measured in operations per second. Larger is better.

benchmarks@0.14.3 compare
node ./compare.js /tmp/tmp.MMJjp7T3J9 /tmp/tmp.Q1kLFyU48H

Results Base Patch Ratio
babel-plugin: stylex.create
· basic create 645 639 0.99 -
· complex create 190 191 1.01 +
babel-plugin: stylex.createTheme
· basic themes 461 458 0.99 -
· complex themes 43 44 1.02 +
@github-actions
Copy link

github-actions bot commented Jul 14, 2025

workflow: benchmarks/size

Comparison of minified (terser) and compressed (brotli) size results, measured in bytes. Smaller is better.

benchmarks@0.14.3 compare
node ./compare.js /tmp/tmp.q8FIBPVefg /tmp/tmp.cHiLxCmKjH

Results Base Patch Ratio
@stylexjs/stylex/lib/cjs/stylex.js
· compressed 1,190 1,190 1.00
· minified 3,541 3,541 1.00
@stylexjs/stylex/lib/cjs/inject.js
· compressed 1,223 1,223 1.00
· minified 3,216 3,216 1.00
benchmarks/size/.build/bundle.js
· compressed 496,574 496,582 1.00 +
· minified 4,846,926 4,847,174 1.00 +
benchmarks/size/.build/stylex.css
· compressed 99,908 99,908 1.00
· minified 747,226 747,226 1.00
return true;
}

export function getProgramStatement(path: NodePath<>): NodePath<> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we need to check programPath.isStatement() here too. but please double check

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. Everything is in a program and if not we want to return the top level anyway. Otherwise, things will break.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't we call insertBefore later on the node returned by getProgramStatement? if that ever returns a non-Statement then Babel will throw

this doesn't affect this PR specifically because we only call it from CallExpression paths but since this is a general purpose util it could break things in the future

Copy link
Collaborator Author

@nmn nmn Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A program can never have a non statement child anyway afaik. Is there an edge-case where that's not true?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

babel workings... yes correct

Copy link
Contributor

@necolas necolas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please could we break this down into different PRs? It would be good if we could fix the deopt for dynamic styles separately from a big change that might no longer require create calls at the top level. We should also talk about whether that should be limited to nesting within TS vs being allowed for arbitrary functions or blocks

const { code, metadata } = transform(`
import * as stylex from '@stylexjs/stylex';
function fooBar() {
const styles = stylex.create({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably have a discussion about the pros and cons of allowing create within arbitrary functions. It might set incorrect expectations for users that create can be used dynamically

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should’ve marked this PR as a draft as it’s mostly a POC right now and the actual design needs discussion.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. I've made changes now. The important thing here is that there is no change to the actual API. The ESLint rule should still error when using stylex.create not at the top level.

However, I've added these tests for two reasons:

  1. Flow doesn't support namespaces and our tests aren't set up for TS syntax
  2. To make the transformation more reliable when used with various bundlers and transformations. It's not uncommon for various bundlers to wrap each module in a self-executing function or something like that. Today StyleX tends to break if things aren't just right. Making it work in more scenarios should help make the babel plugin more reliable.
@mellyeliu mellyeliu merged commit 0851f40 into main Aug 1, 2025
10 checks passed
@mellyeliu mellyeliu deleted the feat/ast-hoisting branch August 1, 2025 16:45
mellyeliu added a commit that referenced this pull request Aug 9, 2025
@mellyeliu mellyeliu changed the title [feat] Hoist stylex.create definitions and nested objects within functions Aug 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

5 participants