Styled-components: Managing highly dynamic styles

Created on 24 Oct 2016  ยท  42Comments  ยท  Source: styled-components/styled-components

Example component:

const Comp = styled.div`
     /*lots of styles here*/
     animation-delay: ${ props => props.index * 0.1 };
`;

With this component rendered over a collection of items, we'll generate a new style tag with an associated class for each one.

Is there a recommended way of working with these kinds of styles so as not to thrash the head with a new style tag everytime render is called on Comp?

  • One solution could be to move the /* lots of styles here */ into another component that doesn't include the dynamic properties that cause huge amounts of CSS to be rendered into the head. This way the generated style tag only contains the animation-delay property. Still a lot of style tags being injected into the head though and since the injection only happens at render time, this could cause some trashing.
  • Another option is to put the animationDelay into the style prop of the component as we compose the components and not let styled-components handle this.

How would you do this? Is there a recommended approach?

I saw there might be some plans to extract static styles which, as a solution, is similar to our first option above.

discussion

Most helpful comment

I'll finally close this discussion since a lot of things have changed since the initial kickoff by @JamieDixon. (thanks!)

  1. We've added a warning when you change styles too often that logs to the console and suggests using inline styles for highly dynamic styles to avoid this kind of confusion.
  2. We now have the .attrs selector, making dynamic styles that come in via props possible with inline styles. You could change the bg prop of this component a lot of times without running into performance issues. (I'd be interested to see the color picker from above reworked this way though!)

    const HighlyDynamicComponent = styled.div.attrs({
    style: props => ({
      background: props.bg,
    })
    })`
    color: blue;
    `
    
  3. More future-wise we're focusing on performance at the moment, @geelen has found a way to batch style injection that'll come out with the next patch release which will speed up initial and rerenders by orders of magnitude. We'll finally be in a similar spot to all other CSS in JS libraries, which means many more dynamic use cases will now be possible too!

That should clear up most, if not all concerns around highly dynamic styles and styled-components. If you have further issues, suggestions or comments please feel free to open a new issue with your ideas!

All 42 comments

On the other hand, it could be that we're trying to do things with styled-components that it's just not designed for.

Another option would be to use something like Velocity to handle our animations and styled-components for the rest.

Have you noticed any visible performance impact on the animation with the current way? If so, could you provide a reproduction in a webpack bin?

Hi @mxstbr,

No performance impact but duplication of the same style again and again which increases the bundle size. Following @JamieDixon example if we have a list of Comp components the style created will be the following:

.[generated class name for 1st instance of comp] {
    /*lots of styles here*/
    animation-delay: 0s;
}
.[generated class name for 2nd instance of comp] {
    /*lots of styles here*/
    animation-delay: 0.1s;
}
.[generated class name for 3rd instance of comp] {
    /*lots of styles here*/
    animation-delay: 0.2s;
}
/* and so on */

As you see the /*lots of style here*/, although it is the same style, appears n times the number of items in the list.

I'm wondering if #59 could actually fix this issue.

Comparison of animation done directly with style prop and with styled-components. http://www.webpackbin.com/VJ3-tBukf In this example animation with props is about 2 times slower on first run.

I came across an example today where styled-components couldn't nicely handle the requirement I had.

This is a colour picker that I'm currently building and the colour of the centre is equal to the chosen colour on the scale. Here's two gifs showing a smooth colour transition when using the style prop and a staggered colour transition with styled-components.

I'm not sure what the answer is to this but wanted to provide it as an example of the aforementioned issue.

style=
smooth

styled-components
stutter

where

background: ${props => `rgb(${props.rgb})`};

I'm not sure what the answer is to this but wanted to provide it as an example of the aforementioned issue.

Sorry, I'm not 100% sure what I should be looking at here! Is the staggering due to styled-components?

Yeah. The stagger is caused because styled-components needs to generate a new <style> tag in the head and switch out the class on the element every time the mouse moves.

I suspect this is just a mis-use of styled-components.

Oh god, that's no good. I'll dig into this soon!

Took a look at this, it _is_ working the way I expected, and yeah it's not ideal for cases like this. First up, the demo: https://styled-components-134.surge.sh/

I'm using the following component (source here):

const Box = styled.div`
  margin: 2rem auto;
  width: 360px;
  height: 360px;
  background: hsl(
    ${props => props.hue}, 
    ${props => 50 + props.luminosity / 2}%, 
    ${props => props.luminosity}%
  );
`

And it seems... about as fast as I expected. It _is_ generating a new CSS classname for each colour value but they're all being injected into their own <style> tag:

image

Now, SC _will_ use multiple <style> tags as the number of components grows for performance reasons (big <style> tags do slow down), but each component's styles are co-located because otherwise some ๐Ÿ‘ป spooky ๐Ÿ‘ป stuff happens (see #31, #1). That's also the reason we re-generate _all_ the CSS for the component rather than a "static" chunk and a "dynamic" chunk. So there's not any great bug we can squash to make this perform better, sadly.

The solution is, of course, to use inline styles for anything that's going to change _really_ often. For one, these don't require parsing & serialising CSS in JS, but also they don't _invalidate_ anything the browser has already understood about the CSS. They're basically the "right tool for the job"

We could, however, expose inline styling through an API, something like this:

const Box = styled.div`
  margin: 2rem auto;
  width: 360px;
  height: 360px;
  :inline {
    background: hsl(
      ${props => props.hue}, 
      ${props => 50 + props.luminosity / 2}%, 
      ${props => props.luminosity}%
    );
  }
`

Or something to that effect. Anyway, for now, just use the style attribute like normal. It'll be passed through and you'll be a-ok ๐Ÿ‘

I think this case should be avoided completely. Ideally user would be allowed to declare styled components only in the top level scope of the module, same like import statement. Otherwise this is going to be misused for stuff like animations which require super high performance, and won't go well with this rendering.

Ideally user would be allowed to declare styled components only in the top level scope of the module, same like import statement.

How does that solve the problem, this component still has this issue even when declared at the top level scope:

import styled from 'styled-components';

// Top level
const Box = styled.div`
  margin: 2rem auto;
  width: 360px;
  height: 360px;
  background: hsl(
    ${props => props.hue}, 
    ${props => 50 + props.luminosity / 2}%, 
    ${props => props.luminosity}%
  );
`

Oh I thought the issue comes from creating the styled component within render, but its only part of the problem. In this example its the functions which can accept props...

What is the current rendering behavior, when a styled component becomes new props and returns a different style, what do you do?

Inject a new style fragment, since it might only apply to a single instance of the component, not generally!

Which means that on every render that requires a different styling, you insert a new CSSRule into the style element. If a user does a lot of such changes or even a javascript-controlled animation - it will generate a shit load of css)))

Yeah exactly, which is why this issue is a thing! ๐Ÿ˜‰

I really like the idea by @geelen to provide some way to control inline CSS with styled-components.
Maybe something like this:

const Box = styled.div`
  margin: 2rem auto;
  width: 360px;
  height: 360px;
  ${styled.inline`
    background: hsl(
      ${props => props.hue}, 
      ${props => 50 + props.luminosity / 2}%, 
      ${props => props.luminosity}%
    );
  `}
`

This looks much cleaner to me than introducing some new syntax like :inline {...} but I don't know exactly how styled-components works and if this is possible.

The styled.inline syntax won't work with the introduction of #166, but I get where the general idea is coming from.

The question is if we can do this automatically. Why do people need to annotate if styles should be inline? Can't we just inline all the dynamic styles that are not in a media query or some sort of nesting?

The question is if we can do this automatically.

The question is then whether user has enough control and its not too much magic. For me an explicit statement for inline rendering is a better choice. Also for SSR this would have potentially an impact - bigger documents.

Yeah specificity of inline stuff concerns me too. I think automagically mixing-and-matching inline and stylesheet rules is a face-melting bug waiting to happen:

styled.a`
  color: red;
  &:hover {
    color: darkred;
  }
`

Suddenly this would render <a class='foo' style='color: red;'> and .foo:hover { color: darkred; } and your :hover rule doesn't work. Let's not do that.


Basically, nothing about the above _needs_ to be inline, it's just the behaviour of keeping the existing styles around that's the problem (because the total amount of CSS grows)

const Box = styled.div`
  margin: 2rem auto;
  width: 360px;
  height: 360px;
  ${ephemeral`
    background: hsl(
      ${props => props.hue}, 
      ${props => 50 + props.luminosity / 2}%, 
      ${props => props.luminosity}%
    );
  `}
`

This could basically inject two classes: .hash1234 and .hash1234_ephemeral, and then changes to the background property keep reusing .hash1234_ephemeral. It has the nice property that nesting and everything would still be supported and specificity and source-order is preserved, it becomes a simple opt-in performance tweak for frequently-changing properties.

What do you all think?

Alternatively, we could do:

const Box = styled.div`
  margin: 2rem auto;
  width: 360px;
  height: 360px;
  background: hsl(
    ${props => props.hue}, 
    ${props => 50 + props.luminosity / 2}%, 
    ${props => props.luminosity}%
  );
`
Box.overwriteStyles = true

That way you optimise on a per-component level. I think I prefer the ephemeral helper though, personally...

Well, just think how we do it usually, with CSS modules, or regular CSS โ€“ we just add style property to the component. So, what you can do, just separate dynamic part and apply it to your component:

const Box = styled.div`
  margin: 2rem auto;
  width: 360px;
  height: 360px;
`;

...
render() {
   const background = `${hue}, ${50 + props.luminosity / 2}, ${luminosity}`;
   return (
      ... <Box style={{ background }}>
              {content}
         </Box>
  );

We don't really do animations with values updates through classes โ€“ they are always classes.

@Bloomca I think the issue here is that styled-components allow that and doesn't discourage this behavior i.e for the user of the lib it might be unclear what will happen when the interpolation happens.

I'm wondering how JSS solves this. @kof?

What about issuing some warnings in dev mode saying "Don't do real-time values interpolation here?" but this won't solve the issue.

Another idea is to restrict style fragments generation to only statically resolvable props and doing inline runtime calculation for the rest? Using information from propTypes or flow annotation to resolve all possible combintations statically ahead of time to pre-generate stylesheets. This could also help solve #59 and #214.

@okonet I've suggested warnings implementation in #268

I think @Bloomca's approach above is the best option for dynamic styles, this should be added to the docs. I can make a pr if it sounds like a good idea.

Interesting thread. I hadn't realized at first that using the pattern described in "adapting based on props" would duplicate the entire style, I had assumed that it would somehow factor out the parts that change into their own classes.

The fact that this will duplicate styles should probably be mentioned in the docs? It seems like an important thing to consider when deciding whether to use that pattern or not.

I always considered the ability to assign css depending on the prop was more of a one time deal, and if i need animation i would use inline styles.

Is there a reason why dynamic properties would not simply always be inlined by styled-components ?

@jide That's because their are many edge cases which you have to solve then. For example media queries can't be inlined but they can be dynamic with styled-components. And this will introduce many specificity bugs because inline styles have a higher specificity than normal css classes.

Hmmm ok I see.

Maybe styled-components should export a different module for using that behavior explicitly ?

import { dynamic as styled } from 'styled-components'

On a side note, that's totally something css variables could solve :) When I think of it, we could imagine that the current behaviour would be the default, and if browser supports css variables, instead of injecting a style node in header, we would manipulate variables. But that would mean a little work.

Just a thought on putting all dynamic styles inline. It's obviously a breaking change, since it'll alter the specificity. Also, a lot of people seem to be using the theming aspect of styled-components, which would cause everything to be inlined.

But we could provide it as an option. If we did, we could extract all the non-dynamic CSS into a static CSS file via a babel-plugin.

Other than that, css-variables and the style prop seem to be the way to get it to work as of current.

@jacobp100 The problem with inlining dynamic styles is that you can write dynamic styles in :hover or @media and you can't inline those.

It would definitely have to be an opt-in thing. You could generate some css custom properties and inject those into the inline styles to get it to work in :hover etc. I suppose this is v2 and beyond though!

I'll finally close this discussion since a lot of things have changed since the initial kickoff by @JamieDixon. (thanks!)

  1. We've added a warning when you change styles too often that logs to the console and suggests using inline styles for highly dynamic styles to avoid this kind of confusion.
  2. We now have the .attrs selector, making dynamic styles that come in via props possible with inline styles. You could change the bg prop of this component a lot of times without running into performance issues. (I'd be interested to see the color picker from above reworked this way though!)

    const HighlyDynamicComponent = styled.div.attrs({
    style: props => ({
      background: props.bg,
    })
    })`
    color: blue;
    `
    
  3. More future-wise we're focusing on performance at the moment, @geelen has found a way to batch style injection that'll come out with the next patch release which will speed up initial and rerenders by orders of magnitude. We'll finally be in a similar spot to all other CSS in JS libraries, which means many more dynamic use cases will now be possible too!

That should clear up most, if not all concerns around highly dynamic styles and styled-components. If you have further issues, suggestions or comments please feel free to open a new issue with your ideas!

๐Ÿ‘ thanks for all you've done @mxstbr. I might have a shot at the colour picker this weekend!

That'd be awesome, let me know when it's done!

@mxstbr Better late than never!

It works beautifully. Thanks for the great work everyone. SC has made such a difference to our project.

sep-10-2017 11-52-05

@mxstbr Can you please guide me to the doc related to attrs?
I don't get the syntax needed for string expansion:

Here is my div:

const BG = styled.div.attrs(
    ( { x, y } ) => ( { 
        bgcolor: `rgba(${ x * 3.6 },${ y * 3.6 },0,1)`
} ) )`
  background-color:${ props => props.bgcolor };
`

Edit:
Ok I got the idea behind attrs here is the working version:

``jsx const BG = styled.div.attrs({ style: props => ({ backgroundColor:rgba(${ props.x * 3.6 },${ props.y * 3.6 },0,1)}) })
pointer-events:none;
`

Forget it...

I just understood the use of attrs (mix of props and html attributes) !!! It's useful for many other stuff aswell, glad to discover that one.

It should be more obvious in the docs and it needs more use cases examples (I can already think of many) IMHO!
Thanks

Yeah. The stagger is caused because styled-components needs to generate a new <style> tag in the head and switch out the class on the element every time the mouse moves.

I suspect this is just a mis-use of styled-components.

I ran into the same problem.

here's a video (although there isn't a noticeable slowdown in that demo, but it does get slow over time): https://www.youtube.com/watch?v=D1pgPdU3D8M

@mxstbr Maybe we should add a note to documentation somewhere on styled components page, that this .attrs() method should be used instead of inlining dynamically-and-often-changed variables into the css? (Current documentation mentions other reasons when attrs should be used or not used, but not the performance reason.)

P.S. Is there a "Best performance practices" article somewhere on styled-components page?

โ˜๏ธ Any section for styled-components best practices? I too, was unaware of the attrs perf hack.

  1. More future-wise we're focusing on performance at the moment, @geelen has found a way to batch style injection that'll come out with the next patch release which will speed up initial and rerenders by orders of magnitude. We'll finally be in a similar spot to all other CSS in JS libraries, which means many more dynamic use cases will now be possible too!

@mxstbr Can you perhaps comment if the issue(s) in this thread are a concern with v5? For styles that change a lot, should one still use the attrs solution your proposed?

Thanks!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Nemsae picture Nemsae  ยท  65Comments

exogen picture exogen  ยท  60Comments

mxstbr picture mxstbr  ยท  67Comments

rtymchyk picture rtymchyk  ยท  42Comments

brad-decker picture brad-decker  ยท  66Comments