Static extraction is HARD
Firstly, the set-up of static extraction is more complicated - there needs to be a build process, and the static extractor needs to fit into that build process.
More importantly, static extraction will require some form of static evaluation. Take the following code for example:
In order to statically generate the css styles, the extractor needs to look
at at least one other file called “helpers”. In this case, it may also need to
understand what “helpers” is pointing to - is it to some top level file called
“helpers”, or a module? Moreover, “helpers” may itself import
getColor may also be defined in many interesting ways.
A perfect extractor will need to somehow evaluate all of the code and generate the class name as well as the CSS code.
At the same time, we should only extract the minimal amount of code necessary, both due to performance concerns and the general desire to avoid side effects during a build process.
This is really hard to do right. Facebook prepack may offer a glimpse of hope, but it’s still very early in the development stage and the development seems to have stalled as of March 2021.
For now, most build-time CSS-in-JS libraries impose some kind of restrictions to make things easier, and most non-build-time CSS-in-JS libraries avoid building any static extraction at all to stay flexible (see arguments from styled-component and emotion ).
Nevertheless, let’s give it a try and build a simple static extractor for our little library.
Building a simple static extractor
In our last post we have built a dynamic CSS-in-JS library powered by CSS variables.
We will create a script that generate CSS code and replaces the call to
some static class names.
We will impose some restrictions here. In particular:
- All expressions in the function parameter of
styledmust be static.
- This means that all static values needs to be inline constants.
- We will replace all function calls with
styleddoesn’t need to be imported, but developers also can’t redefined
styledto be something else
Then we will build a simple script that reads through only a single js file, extracts these definitions, replaces them with a definition with some constant class name and generates a corresponding stylesheet.
Since we are dealing with a single JS file at a time, our implementation can be encapsulated in the following function:
That’s pretty easy. We specify
sourceType: "module", because we want to support
jsx is also necessary to support the JSX
At this point, it may be useful to print out the AST to get a sense of what it looks like.
Notice that the AST consists of ‘nodes’, each has a
type attribute that describes what
type of node it is. Each node can nest other nodes. For example, the
node has a
declarations list, where some elements can be
VariableDeclarators, that specify
const x =), and the
init part of the declarator (e.g. the expression
to call a function).
Next, we want to traverse the AST, look for our
styled() call, generate some CSS code
based on that, and replace the
styled() call with something that only reference the
class name. To do so, we use the
traverse function from
babel. This function follows
the visitor pattern
We will call
traverse with the AST with a ‘visitor’, and the traverse function walks
through the AST and calls the proper functions in the visitor when it encounters
specific nodes. In this case, we only care about the ‘call expression’, since we
want to know when
styled is called.
checkArgument call - remember when we said we want to impose
some restriction? This is where we check against the first restriction -
all expressions must be static.
Statically evaluate each
Now that we know that the call is valid, let’s statically evaluate it. Our restriction has made things much simpler - we can pretty much evaluate the object as it is, with functions represented by some ‘tokens’.
We will need to pull out our
again. Notice that in
nestedDeclarationToRuleStrings we don’t ever try to
evaluate the functions, so we can update the implementation and replace
functions with our AST nodes instead. We annotate the changes with comments below:
Running this function should generate a list of CSS rules and a map from the CSS variable to the AST nodes representing each function in the original code.
nestedDeclarationToRuleStrings function that we have just built,
coupled with our counter-base class name function and a simple string to collect
css code, we were able to statically generate the class name and the css rules
We then plug the statically evaluated class name into to the code string, which
is the same the implementation for
styled, parse it into an AST and replace
the node representing the
styled call with it.
First we need to fill in the
style attribute in our generated React code.
We will use
@babel/generator to generate code from AST - think of it
as the inverse of
variablesValueMapping maps a variable name, which is a string, to
a node representing a function. We will first convert this node
to some code as a string, then insert the string to our generated React
code. We will do that for every key-value pair.
This is what it looks like:
Complete source code
While we simplified our extractor to make it almost not useful, our code generation is probably on the more complicated side. Most tools will try to generate only the class name and the list of variables to avoid being too specific to a framework.
Also, depending on where we specify the dynamic logics, we may not need CSS variables at all. Facebook’s StyleX is such an example. The API allows for only static styles with ‘variants’, and only generates class names. This makes static codegen simpler and removes the potential overhead of CSS variables.