CSS Pipeline @ Housing.com

Dhruvil Patel
Engineering @ Housing/Proptiger/Makaan
5 min readMar 16, 2023

Part 5: Reducing runtime overhead of linaria/atomic’s cx function

This article is fifth in a series of articles related to CSS Pipeline at Housing.com. We recommend reading the previous articles before proceeding further. Part 1, Part 2, Part 3, Part 4

This article is highly opinionated, the solutions presented here might not be suited to everyone’s needs.

This article is a followup to the problem mentioned in Further things we are exploring [Part B] in one of our previous articles

Let’s begin by reiterating the problem again.

Problem Statement

Consider the following case:

const style1 = css`
font-size: 12px;
color: red;
font-size: 13px;
`
// sample output: const style1 = 'atm_fs_0012px atm_cl_000red atm_fs_0013px'


const style2 = css`
color: green;
font-size: 14px;
`
// sample output: const style2 = 'atm_cl_0green atm_fs_0014px'


const style3 = css`
color: blue;
`
// sample output: const style3 = 'atm_cl_00blue'


const style4 = css`
font-size: 15px;
`
// sample output: const style4 = 'atm_fs_0015px'


export default ({addStyle4, overrideStyle}) => (
<>
<div className={cx(style1, style2, style3)}>
<span className={cx(style2, style3, addStyle4 && style4, overrideStyle)}></span>
</div>
</>
)

In this case the following happen:

  1. the styles applied on div will always be ‘atm_fs_0014px atm_cl_00blue’ . Therefore, can’t we modify cx(style1, style2, style3) to ‘atm_fs_0014px atm_cl_00blue’ at build time itself. It will save the runtime call to cx function as well [which can be costly if the page layout is huge and re-renders frequently and there are many such cx calls which could otherwise be resolved at build time]
  2. But the same isn’t true for the span since style4 and overrideStyle are dependent on runtime conditions. Same build time optimisations therefore, can’t happen on span. But still it should be possible to transform cx(style2, style3, addStyle4 && style4, overrideStyle) to cx('atm_fs_0014px atm_cl_00blue', addStyle4 && 'atm_fs_0015px', overrideStyle)

This nice little optimisation, can save us some runtime computation at the expense of some additional build time processing.

Solution

We created a babel-plugin, which finds all calls made to the cx function. Identifies all such calls which can be replaced at build time. The logic for that goes like so:

  1. All arguments send to cx function are of type StringLiteral or Identifier
  2. If an argument is of type Identifier, then that Identifier must either be imported from another style file or declared locally.
  3. If both these conditions are met, we take the values corresponding to these StringLiterals & Identifiers run them through cx function and inline the value.

An example to understand how this works:

//style.js
import "style.linaria.css"
export const style1 = 'atm_fs_df543'


//style2.js
import "style2.linaria.css"
export const style2 = 'atm_cl_red'



// input
import {style1} from './style'
import {style2} from './style2'
const style3 = 'atm_fs_df3rf'
export default ({style4, someRuntimeCondition}) => {
return (
<div className={cx(style1, style2)}>
<div className={cx(style1, 'some-className')}>hello</div>
<div className={cx(style1, style3)}>lorem </div>
<div className={cx(style1, style4)}>ipsum</div>
<div className={cx(style1, someRuntimeCondition && style3)}>ipsum</div>
</div>
)
}




// output
import {style1} from './style'
import {style2} from './style2'
const style3 = 'atm_fs_df3rf'
export default ({style4, someRuntimeCondition}) => {
return (
<div className='atm_fs_df543 atm_cl_red'>
<div className='atm_fs_df543 some-className'>hello</div>
<div className='atm_fs_df3rf'>lorem </div>
<div className={cx(style1, style4)}>ipsum</div>
<div className={cx(style1, someRuntimeCondition && style3)}>ipsum</div>
</div>
)
}

Notice how the first 3 calls made to cx function have been replaced with strings. This was possible because those 3 calls satisfied the rules mentioned above.

The last 2 calls however don’t satisfy those rules. Fourth call uses ‘style4’ the value of which can’t be resolved at build time, since it’s a prop that this component receives and would only be accessible at build time.

The Fifth call has a runtime conditional expression, which can’t be resolved at build time

How to do this?

The challenge here is how do we get the value of style1, style2, which are imports from another file.

We were able to accomplish this with the help of a webpack-loader and a babel-plugin. This is how the solution works:

  1. We find all the cx function nodes which satisfy the above conditions using a babel plugin and store them
  2. We also use the same babel plugin to identify all external dependencies. In our case style1 from style.js and style2 from style2.js.
  3. We use a webpack-loader to load these external dependencies using webpack’s loadModule function. This function returns the code as a string. We parse these loaded modules with another babel-plugin and find the values of style1 and style2.
  4. We use all of this to replace the cx nodes collected in step 1 and replace them with final string outputs

Doing this however, created another problem. Let’s look at it now:

Problem 2: Tree shaking

Consider the following code

//style.js
import "style.linaria.css"
export const style1 = 'atm_fs_df543'
export const style2 = 'atm_cl_red'



// input
import {style1, style2} from './style'
export default () => {
return (
<div className={cx(style1, style2)}>
hello
</div>
)
}

In this case we have 2 imports style1, style2 from ./style.js file. Both of these imports are used in a cx call, which can be resolved at build time.

Once our optimisation for evaluating cx calls a build time kick in the output code will look somthing like this:

// output
import {style1, style2} from './style'
export default () => {
return (
<div className='atm_fs_df543 atm_cl_red'>
hello
</div>
)
}

We correctly evaluated the cx at build time. But this created a problem. Now the imports style1, style2 are unused. Hence, webpack [if configured correctly for tree shaking] will remove this useless import for ‘./style’ in production mode. Once that happens, import “style.linaria.css” will also be gone. Hence, even though classNames will be properly applied on your elements, the corresponding css declarations won’t load.

This is an edge case, which we solved by creating a useless side-effect so that webpack is unable to tree shake the import. It’s a hack which made our solution full proof.

You can checkout the code for corresponding webpack-loader and babel-plugins.

Thanks a lot for reading. Please do share your valuable feedback & keep following this space for more such content.

This is a fifth in the series of articles on our CSS Pipeline.

Check out the other articles of this series: Part 1, Part 2, Part 3, Part 4

Once again shout out to the ones who helped achieve this: sachin agrawal, Naman Saini, Nikhil Verma, Sachin Tyagi

--

--