Search code examples
javascriptcssperformanceweb-componentlit-element

LitElement: best practise (or best performance) when creating many custom style rules vs a couple of dynamic rules inside a tiny web component?


So I'm working on a very small web component to feature as part of a much larger design system.

I'm a bit new to use of Web Components, but I know this particular web-component could be used many, many times in a single layout.

This web component controls how much vertical space to put around any child components that are passed into it.

The anatomy of the component is pretty simple:

import { LitElement, html, css, unsafeCSS, property, customElement } from 'lit-element';

export const spacingAmounts = {
  'x-small': css`4px`,
  'small': css`8px`,
  'medium': css`12px`,
  'large': css`16px`,
  'x-large': css`20px`,
  '2x-large': css`30px`,
  '3x-large': css`40px`,
  '4x-large': css`60px`,
  '5x-large': css`90px`,
  '6x-large': css`120px`,
};

const createSpacingStyleRules = (direction: 'top' | 'bottom') => {
  return Object.keys(spacingAmounts).map(s => {
    const amount = spacingAmounts[s];
    return css`
      :host([${unsafeCSS(direction)}="${unsafeCSS(s)}"]) {
        margin-${unsafeCSS(direction)}: ${unsafeCSS(amount)};
      }
    `;
  });
};

@customElement('gu-vertical-space')
export class GuVerticalSpace extends LitElement {
  @property() top: string;
  @property() bottom: string;

  static get styles() {
    const styles = [
      css`
        :host {
          display: block;
        }
      `,
      // ----------------------------------------------------------
      // @TODO:
      // test if it's better performance wise to either:
      //
      // 1 -  generate a verbose list of static styles for
      //      each instance of gu-vertical-space
      //  or
      // 2 -  generate a tiny amount of styles on the fly, 
      //      based on property inputs...
      // ----------------------------------------------------------
      // ...createSpacingStyleRules('top'),
      // ...createSpacingStyleRules('bottom'),
    ];
    return styles;
  }

  render() {
    const styles = [];
    if (this.top) {
      styles.push(css`
        :host([top="${unsafeCSS(this.top)}"]) {
          margin-top: ${spacingAmounts[this.top]}
        }
      `);
    }
    if (this.bottom) {
      styles.push(css`
        :host([bottom="${unsafeCSS(this.bottom)}"]) {
          margin-bottom: ${spacingAmounts[this.bottom]}
        }
      `);
    }
    return html`
      <style>${styles}</style>
      <slot></slot>
    `;
  }
}


There are 2 approaches here for mapping pre-defined margin amounts to either the top or bottom of the web component's host.

The currently active approach is simply to dynamically generate a <style> block inside the render function, which contains any margin amounts, as decided by the "top" or "bottom" input properties.

The other approach that I'm contemplating (is commented out atm though) - is to staticly generate a massive list of style rules - thus avoiding the need to generate any dynamic style stuff inside the render() function, which could be an anti-pattern - if i'm understanding the lit-element docs correctly.

Perhaps there's a more elegant approach that I'm missing? I'm leaning toward the current approach, just because it feels simpler to understand - but curious as to what others think!


Solution

  • As suggested by Alan, a much simpler solution to this issue is to use css-variables.

    Basically, just map the input margin amounts as a css-variable at the connect() life-cycle event (or render if you imagine the props will ever change after the initial render) - and call it a day!

    static get styles() {
      return css`
        :host {
          display: block;
          margin-top: var(--margin-top);
          margin-bottom: var(--margin-bottom);
        }
      `;
    }
    
    connectedCallback() {
      super.connectedCallback();
      this.style.setProperty(
        '--margin-top',
        this.top
          ? spacingAmounts[this.top]
          : 'unset'
      );
      this.style.setProperty(
        '--margin-bottom',
        this.bottom
          ? spacingAmounts[this.bottom]
          : 'unset'
      );
    }