lightningcss icon indicating copy to clipboard operation
lightningcss copied to clipboard

Further optimize `@layer` minification opportunities

Open adamwathan opened this issue 1 year ago • 5 comments

Cascade layers support a few things that Lightning CSS doesn't currently take into consideration:

  • Nested layers both by nesting directly as well as using dot notation to insert into a nested layer
  • Anonymous layers, which are treated as unique and can't be combined
  • Predefined layer order using @layer without a block

That means input like this:

@layer one, two, three;

@layer three.utilities {
  .bg-black {
    background: #000;
  }
}

@layer two {
  body {
    background: red;
  }
  @layer base {
    body {
      font-size: 10px;
    }
  }
}

@layer {
  .class-1 {
    color: blue;
  }
}

@layer three {
  @layer utilities {
    .mt-10 {
      margin-top: 10px;
    }
  }
  html {
    color: white;
  }
}

@layer two.base {
  h1 {
    font-size: 24px;
  }
}

@layer two {
  h2 {
    font-size: 20px;
  }
}

@layer one {
  p {
    font-size: 16px;
  }
}

@layer {
  .class-2 {
    color: red;
  }
}

...can be minified to this sort of shape:

@layer one {
  p {
    font-size: 16px;
  }
}

@layer two {
  body {
    background: red;
  }
  h2 {
    font-size: 20px;
  }
  @layer base {
    body {
      font-size: 10px;
    }
    h1 {
      font-size: 24px;
    }
  }
}

@layer three {
  @layer utilities {
    .bg-black {
      background: #000;
    }
    .mt-10 {
      margin-top: 10px;
    }
  }
  html {
    color: white;
  }
}

@layer {
  .class-1 {
    color: blue;
  }
}

@layer {
  .class-2 {
    color: red;
  }
}

This is unfortunately complicated, hah 😄 It would be pretty amazing to be able to handle this sort of thing at the minification layer as I have to imagine it makes the browser's job easier.

I ask selfishly because I'm anticipating a future for Tailwind CSS where we leverage cascade layers to avoid a bunch of the complex sorting logic that exists in the framework right now, and instead generate code that looks like this:

@layer defaults, base, components, utilities, variants;

@layer base {
  html { /* ... */ }
  /* ... */
}

@layer components {
  .container { /* ... */ }
}

.shadow-sm {
  @layer defaults {
    --tw-ring-offset-shadow: 0 0 #0000;
    --tw-ring-shadow: 0 0 #0000;
    --tw-shadow: 0 0 #0000;
    --tw-shadow-colored: 0 0 #0000
  }
  @layer utilities {
    --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
    --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
    box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)
  }
}

.blur {
  @layer defaults {
    --tw-blur:  ;
    --tw-brightness:  ;
    --tw-contrast:  ;
    --tw-grayscale:  ;
    --tw-hue-rotate:  ;
    --tw-invert:  ;
    --tw-saturate:  ;
    --tw-sepia:  ;
    --tw-drop-shadow:  
  }
  @layer utilities {
    --tw-blur: blur(8px);
    filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)
  }
}

.hover\:scale-125 {
  @layer defaults {
    --tw-translate-x: 0;
    --tw-translate-y: 0;
    --tw-rotate: 0;
    --tw-skew-x: 0;
    --tw-skew-y: 0;
    --tw-scale-x: 1;
    --tw-scale-y: 1
  }
  @layer variants {
    &:hover {
      --tw-scale-x: 1.25;
      --tw-scale-y: 1.25;
      transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))
    }
  }
}

...and delegate all of the work for processing nesting and minification to Lightning CSS (or our own separate production-ifying layer if needed, not trying to make demands here of course!) to get output more like this:

@layer defaults {
  .shadow-sm {
    --tw-ring-offset-shadow: 0 0 #0000;
    --tw-ring-shadow: 0 0 #0000;
    --tw-shadow: 0 0 #0000;
    --tw-shadow-colored: 0 0 #0000;
  }
  .blur {
    --tw-blur: ;
    --tw-brightness: ;
    --tw-contrast: ;
    --tw-grayscale: ;
    --tw-hue-rotate: ;
    --tw-invert: ;
    --tw-saturate: ;
    --tw-sepia: ;
    --tw-drop-shadow: ;
  }
  .hover\:scale-125 {
    --tw-translate-x: 0;
    --tw-translate-y: 0;
    --tw-rotate: 0;
    --tw-skew-x: 0;
    --tw-skew-y: 0;
    --tw-scale-x: 1;
    --tw-scale-y: 1;
  }
}

@layer base {
  html { /* ... */ }
  /* ... */
}

@layer components {
  .container { /* ... */ }
}

@layer utilities {
  .shadow-sm {
    --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
    --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
    box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000),
      var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
  }
  .blur {
    --tw-blur: blur(8px);
    filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast)
      var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert)
      var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
  }
}

@layer variants {
  .hover\:scale-125:hover {
    --tw-scale-x: 1.25;
    --tw-scale-y: 1.25;
    transform: translate(var(--tw-translate-x), var(--tw-translate-y))
      rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y))
      scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
  }
}

Thought it was worth raising just to bring some awareness at least to all of these extra cascade layer features that might need to be considered.

This is a really fantastic post that covers all of this stuff in quite a bit of depth:

https://css-tricks.com/css-cascade-layers/

adamwathan avatar Feb 11 '23 19:02 adamwathan

Yeah what I implemented in #414 only covers layers defined in the same css rule list, not nested ones. We could potentially flatten some of these. However there are some cases where it isn't possible like layer blocks inside media rules or nested in style rules where it wouldn't be safe.

devongovett avatar Feb 11 '23 20:02 devongovett

@devongovett i'm not sure whether to create a separate issue for this or not (lmk) but it would also be super handy if it could respect browserslist.

i've set Chrome version real low here and you can see the @layer rules still exist. i expected it to do the same grouping but unwrap them. perhaps i am misunderstanding how browserslist is intended to be used here tho?

jjenzz avatar Dec 11 '23 13:12 jjenzz

it's not safe for us to compile layers away completely because there might be other css on the page that uses them, or has its own layers (and unlayered rules have higher precedence). perhaps there could be an opt-in way of doing it, but that would go along with other unsafe optimizations that have been discussed in the past.

devongovett avatar Dec 11 '23 21:12 devongovett

that makes sense, and an opt-in wld be fab. i haven't followed the discussions around unsafe optimisations tho, what's the context there? shall i create a sep issue?

jjenzz avatar Dec 12 '23 08:12 jjenzz

aedf6b675705ad385b78bd35b72858b9b68875e8 implements more @layer optimizations, mainly inlining layer blocks into pre-declared layer statements. So something like this:

@layer foo, bar, baz;

@layer bar {
  .bar { color: red }
}

@layer baz {
  .baz { color: red }
}

@layer foo {
  .foo { color: red }
}

minifies to:

@layer foo {
  .foo { color: red }
}

@layer bar {
  .bar { color: red }
}

@layer baz {
  .baz { color: red }
}

the pre-declarations are removed and the layer blocks are placed in the correct order.

Still doesn't do anything with nested layers though. The merging only happens on a single level at a time. But if you set up your rules to be generated this way, it should be optimal.

devongovett avatar Dec 28 '23 20:12 devongovett