Repository: microsoft/griffel
Pull request #268 (2022-11-01 17:25:38)

fix: use style buckets in CSS extraction
This PR changes the approach for sorting CSS rules in a produced CSS asset by `@griffel/webpack-extraction-plugin`.

### Before

We used `sortCSSRules` to parse CSS rules and sort them into buckets in the similar order as rules in `resolveStyleRules()`. However, it was not **the same way**.

### After

The loader used for CSS extraction now emits comments to CSS to identify chunks and meta information (media queries case).

```css
/** @griffel:css-start [d] **/
.fe3e8s9 {
  color: red;
}
/** @griffel:css-end **/
/** @griffel:css-start [m] [{"m":"screen and (max-width: 100px)"}] **/
@media screen and (max-width: 100px) {
  .fr5o61b {
    color: red;
  }
}
/** @griffel:css-end **/
/** @griffel:css-start [m] [{"m":"screen and (max-width: 100px)"}] **/
@media screen and (max-width: 100px) {
  .f1j0ers2 {
    display: grid;
  }
}
/** @griffel:css-end **/
```

This solves the sorting problem: now we can use the same approach. It also allows us clearly identify our CSS rules in output and unblocks other approaches for the extraction plugin.

github-actions[bot]: ## 📊 Bundle size report

<details>
<summary>Unchanged fixtures</summary>

| Package & Exports                                                                                                            |        Size (minified/GZIP) |
| ---------------------------------------------------------------------------------------------------------------------------- | --------------------------: |
| <samp>core</samp> <br /> <abbr title='bundle-size/makeResetStyles-runtime.fixture.js'>makeResetStyles (runtime)</abbr>       | `16.832 kB`<br />`6.245 kB` |
| <samp>core</samp> <br /> <abbr title='bundle-size/makeStyles.fixture.js'>makeStyles + mergeClasses (build time)</abbr>       |     `1.842 kB`<br />`877 B` |
| <samp>core</samp> <br /> <abbr title='bundle-size/makeStylesRuntime.fixture.js'>makeStyles + mergeClasses (runtime)</abbr>   | `20.789 kB`<br />`7.724 kB` |
| <samp>react</samp> <br /> <abbr title='bundle-size/__css.fixture.js'>\_\_css + mergeClasses (build time)</abbr>              |     `1.879 kB`<br />`874 B` |
| <samp>react</samp> <br /> <abbr title='bundle-size/__styles.fixture.js'>\_\_styles + mergeClasses (build time)</abbr>        |  `3.581 kB`<br />`1.614 kB` |
| <samp>react</samp> <br /> <abbr title='bundle-size/makeResetStyles-runtime.fixture.js'>makeResetStyles (runtime)</abbr>      | `18.595 kB`<br />`6.981 kB` |
| <samp>react</samp> <br /> <abbr title='bundle-size/makeStaticStyles-runtime.fixture.js'>makeStaticStyles (runtime)</abbr>    |  `9.443 kB`<br />`4.068 kB` |
| <samp>react</samp> <br /> <abbr title='bundle-size/makeStyles-runtime.fixture.js'>makeStyles + mergeClasses (runtime)</abbr> | `22.554 kB`<br />`8.436 kB` |

</details>
<sub>🤖 This report was generated against <a href='https://github.com/microsoft/griffel/commit/3bcf4bceb36e19e62e8af5edf6dafbc842487405'>3bcf4bceb36e19e62e8af5edf6dafbc842487405</a></sub>

<!-- Sticky Pull Request Comment -->

Patch:
diff --git a/change/@griffel-core-da60ef94-dd69-4128-9f8f-53fceb3ec3c3.json b/change/@griffel-core-da60ef94-dd69-4128-9f8f-53fceb3ec3c3.json
new file mode 100644
index 0000000..eb2ccb9
--- /dev/null
+++ b/change/@griffel-core-da60ef94-dd69-4128-9f8f-53fceb3ec3c3.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "chore: export \"CSSBucketEntry\" type for internal usage",
+  "packageName": "@griffel/core",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@griffel-webpack-extraction-plugin-b995688a-9700-4878-8832-49047eed3bb3.json b/change/@griffel-webpack-extraction-plugin-b995688a-9700-4878-8832-49047eed3bb3.json
new file mode 100644
index 0000000..a6a1e7e
--- /dev/null
+++ b/change/@griffel-webpack-extraction-plugin-b995688a-9700-4878-8832-49047eed3bb3.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "fix: use style buckets in CSS extraction",
+  "packageName": "@griffel/webpack-extraction-plugin",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 4075283..10cdbc6 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -80,6 +80,7 @@ export type {
   // Internal types
   CSSClasses,
   CSSClassesMapBySlot,
+  CSSBucketEntry,
   CSSRulesByBucket,
   StyleBucketName,
   // Util
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index ff9260f..68b3bbd 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -316,6 +316,7 @@ export type CSSRulesByBucket = {
   m?: CSSBucketEntry[];
 };
 
+/** @internal */
 export type CSSBucketEntry = string | [string, Record<string, unknown>];
 
 export type StylesBySlots<Slots extends string | number> = Record<Slots, GriffelStyle>;
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/assets-multiple/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/assets-multiple/output.ts
index 004b02f..51d1b76 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/assets-multiple/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/assets-multiple/output.ts
@@ -5,4 +5,4 @@ export const useStyles = __css({
   },
 });
 
-import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=.fp00rh9%7Bbackground-image%3Aurl(..%2F__fixtures__%2Fwebpack%2Fassets-multiple%2Fblank.jpg)%2Curl(..%2F__fixtures__%2Fwebpack%2Fassets-multiple%2Fempty.jpg)%3B%7D';
+import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=%2F**%20%40griffel%3Acss-start%20%5Bd%5D%20**%2F%0A.fp00rh9%7Bbackground-image%3Aurl(..%2F__fixtures__%2Fwebpack%2Fassets-multiple%2Fblank.jpg)%2Curl(..%2F__fixtures__%2Fwebpack%2Fassets-multiple%2Fempty.jpg)%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/assets/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/assets/output.ts
index 49c5175..df00930 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/assets/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/assets/output.ts
@@ -5,4 +5,4 @@ export const useStyles = __css({
   },
 });
 
-import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=.fnwsaxv%7Bbackground-image%3Aurl(..%2F__fixtures__%2Fwebpack%2Fassets%2Fblank.jpg)%3B%7D';
+import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=%2F**%20%40griffel%3Acss-start%20%5Bd%5D%20**%2F%0A.fnwsaxv%7Bbackground-image%3Aurl(..%2F__fixtures__%2Fwebpack%2Fassets%2Fblank.jpg)%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/basic-rules/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/basic-rules/output.ts
index 3a5eb05..56ad088 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/basic-rules/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/basic-rules/output.ts
@@ -10,4 +10,4 @@ export const styles = __css({
   },
 });
 
-import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=.fe3e8s9%7Bcolor%3Ared%3B%7D%0A.fycuoez%7Bpadding-left%3A4px%3B%7D%0A.f8wuabp%7Bpadding-right%3A4px%3B%7D%0A.fcnqdeg%7Bbackground-color%3Agreen%3B%7D';
+import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=%2F**%20%40griffel%3Acss-start%20%5Bd%5D%20**%2F%0A.fe3e8s9%7Bcolor%3Ared%3B%7D.fycuoez%7Bpadding-left%3A4px%3B%7D.f8wuabp%7Bpadding-right%3A4px%3B%7D.fcnqdeg%7Bbackground-color%3Agreen%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/config-name/fs.json b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-name/fs.json
index 7a3911c..4086f0a 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/config-name/fs.json
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-name/fs.json
@@ -1,1 +1,1 @@
-["bundle.js", "griffel.e44646c584b8724e9528.css"]
+["bundle.js", "griffel.3f25ad2d330c015ca6fa.css"]
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/config-name/output.css b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-name/output.css
index 6948fb7..c4122ec 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/config-name/output.css
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-name/output.css
@@ -1,4 +1,4 @@
-/** griffel.e44646c584b8724e9528.css **/
+/** griffel.3f25ad2d330c015ca6fa.css **/
 .fe3e8s9 {
   color: red;
 }
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/mixed/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/mixed/output.ts
index 79dc01f..b670831 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/mixed/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/mixed/output.ts
@@ -6,4 +6,4 @@ export const useClasses = __css({
 });
 export const useClassName = __resetCSS('rjefjbm', 'r7z97ji');
 
-import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=.fe3e8s9%7Bcolor%3Ared%3B%7D%0A.rjefjbm%7Bcolor%3Ared%3Bpadding-left%3A4px%3B%7D%0A.r7z97ji%7Bcolor%3Ared%3Bpadding-right%3A4px%3B%7D';
+import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=%2F**%20%40griffel%3Acss-start%20%5Bd%5D%20**%2F%0A.fe3e8s9%7Bcolor%3Ared%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A%2F**%20%40griffel%3Acss-start%20%5Br%5D%20**%2F%0A.rjefjbm%7Bcolor%3Ared%3Bpadding-left%3A4px%3B%7D.r7z97ji%7Bcolor%3Ared%3Bpadding-right%3A4px%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.css b/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.css
index 8325d4d..a4f3fe2 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.css
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.css
@@ -1,7 +1,7 @@
 /** griffel.css **/
-.fe3e8s9 {
-  color: red;
-}
 .fcnqdeg {
   background-color: green;
 }
+.fe3e8s9 {
+  color: red;
+}
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.ts
index 42aeb0d..5450176 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/multiple/output.ts
@@ -10,4 +10,4 @@ export const stylesB = __css({
   },
 });
 
-import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=.fe3e8s9%7Bcolor%3Ared%3B%7D%0A.fcnqdeg%7Bbackground-color%3Agreen%3B%7D';
+import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=%2F**%20%40griffel%3Acss-start%20%5Bd%5D%20**%2F%0A.fcnqdeg%7Bbackground-color%3Agreen%3B%7D.fe3e8s9%7Bcolor%3Ared%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-assets/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-assets/output.ts
index d8a01b1..7283d9e 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-assets/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/reset-assets/output.ts
@@ -1,4 +1,4 @@
 import { __resetStyles, __resetCSS } from '@griffel/react';
 export const useClassName = __resetCSS('ra9m047', null);
 
-import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=.ra9m047%7Bbackground-image%3Aurl(..%2F__fixtures__%2Fwebpack%2Freset-assets%2Fblank.jpg)%3B%7D';
+import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=%2F**%20%40griffel%3Acss-start%20%5Br%5D%20**%2F%0A.ra9m047%7Bbackground-image%3Aurl(..%2F__fixtures__%2Fwebpack%2Freset-assets%2Fblank.jpg)%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A';
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/reset/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/reset/output.ts
index dc0f79a..9380ff8 100644
--- a/packages/webpack-extraction-plugin/__fixtures__/webpack/reset/output.ts
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/reset/output.ts
@@ -1,4 +1,4 @@
 import { __resetStyles, __resetCSS } from '@griffel/react';
 export const useClassName = __resetCSS('rjefjbm', 'r7z97ji');
 
-import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=.rjefjbm%7Bcolor%3Ared%3Bpadding-left%3A4px%3B%7D%0A.r7z97ji%7Bcolor%3Ared%3Bpadding-right%3A4px%3B%7D';
+import 'griffel.css!=!../../../virtual-loader/index.js!../../../virtual-loader/griffel.css?style=%2F**%20%40griffel%3Acss-start%20%5Br%5D%20**%2F%0A.rjefjbm%7Bcolor%3Ared%3Bpadding-left%3A4px%3B%7D.r7z97ji%7Bcolor%3Ared%3Bpadding-right%3A4px%3B%7D%0A%2F**%20%40griffel%3Acss-end%20**%2F%0A';
diff --git a/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.ts b/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.ts
index a9de0f3..081d6f9 100644
--- a/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.ts
+++ b/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.ts
@@ -3,6 +3,7 @@ import { Compilation } from 'webpack';
 import type { Compiler, sources } from 'webpack';
 
 import { sortCSSRules } from './sortCSSRules';
+import { parseCSSRules } from './parseCSSRules';
 
 export type GriffelCSSExtractionPluginOptions = {
   compareMediaQueries?: GriffelRenderer['compareMediaQueries'];
@@ -62,10 +63,10 @@ export class GriffelCSSExtractionPlugin {
 
           const [assetName, assetSource] = griffelAsset;
 
-          const unsortedCSSRules = getAssetSourceContents(assetSource);
-          const sortedCSSRules = sortCSSRules(unsortedCSSRules, this.compareMediaQueries);
+          const { cssRulesByBucket } = parseCSSRules(getAssetSourceContents(assetSource));
+          const cssRules = sortCSSRules([cssRulesByBucket], this.compareMediaQueries);
 
-          compilation.updateAsset(assetName, new compiler.webpack.sources.RawSource(sortedCSSRules));
+          compilation.updateAsset(assetName, new compiler.webpack.sources.RawSource(cssRules));
         },
       );
     });
diff --git a/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts
index 5a9ab9c..a2a8240 100644
--- a/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts
+++ b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts
@@ -1,6 +1,6 @@
 import { NodePath, PluginObj, PluginPass, types as t } from '@babel/core';
 import { declare } from '@babel/helper-plugin-utils';
-import { CSSRulesByBucket, normalizeCSSBucketEntry } from '@griffel/core';
+import { CSSRulesByBucket } from '@griffel/core';
 import * as path from 'path';
 
 type StripRuntimeBabelPluginOptions = {
@@ -11,11 +11,11 @@ type StripRuntimeBabelPluginOptions = {
 type FunctionKinds = '__styles' | '__resetStyles';
 
 type StripRuntimeBabelPluginState = PluginPass & {
-  cssRules?: string[];
+  cssRulesByBucket?: CSSRulesByBucket;
 };
 
 export type StripRuntimeBabelPluginMetadata = {
-  cssRules: string[];
+  cssRulesByBucket: CSSRulesByBucket;
 };
 
 export function transformUrl(filename: string, resourceDirectory: string, assetPath: string) {
@@ -39,10 +39,10 @@ export const babelPluginStripGriffelRuntime = declare<
   return {
     name: '@griffel/webpack-extraction-plugin/babel',
     pre() {
-      this.cssRules = [];
+      this.cssRulesByBucket = {};
     },
     post() {
-      (this.file.metadata as unknown as StripRuntimeBabelPluginMetadata).cssRules = this.cssRules!;
+      (this.file.metadata as unknown as StripRuntimeBabelPluginMetadata).cssRulesByBucket = this.cssRulesByBucket!;
     },
 
     visitor: {
@@ -215,19 +215,15 @@ export const babelPluginStripGriffelRuntime = declare<
               if (functionKind === '__styles') {
                 const cssRulesByBucket = evaluationResult.value as CSSRulesByBucket;
 
-                Object.values(cssRulesByBucket).forEach(cssBucketEntries => {
-                  const cssRules = cssBucketEntries.map(cssBucketEntry => {
-                    const [cssRule] = normalizeCSSBucketEntry(cssBucketEntry);
-
-                    return cssRule;
-                  });
-
-                  state.cssRules!.push(...cssRules);
+                Object.entries(cssRulesByBucket).forEach(([cssBucketName, cssBucketEntries]) => {
+                  state.cssRulesByBucket![cssBucketName as keyof CSSRulesByBucket] = cssBucketEntries.concat(
+                    state.cssRulesByBucket![cssBucketName as keyof CSSRulesByBucket] || [],
+                  );
                 });
               } else if (functionKind === '__resetStyles') {
-                const cssRules = evaluationResult.value as string[];
+                const cssRules = evaluationResult.value as NonNullable<CSSRulesByBucket['r']>;
 
-                state.cssRules!.push(...cssRules);
+                state.cssRulesByBucket!.r = cssRules.concat(state.cssRulesByBucket!.r || []);
               }
 
               argumentPath.remove();
diff --git a/packages/webpack-extraction-plugin/src/parseCSSRules.ts b/packages/webpack-extraction-plugin/src/parseCSSRules.ts
new file mode 100644
index 0000000..fa07e96
--- /dev/null
+++ b/packages/webpack-extraction-plugin/src/parseCSSRules.ts
@@ -0,0 +1,50 @@
+import { styleBucketOrdering } from '@griffel/core';
+import { COMMENT, compile, MEDIA, serialize, stringify } from 'stylis';
+
+import type { CSSBucketEntry, CSSRulesByBucket, StyleBucketName } from '@griffel/core';
+
+export function parseCSSRules(css: string) {
+  const cssRulesByBucket = styleBucketOrdering.reduce<CSSRulesByBucket>((acc, styleBucketName) => {
+    acc[styleBucketName] = [];
+
+    return acc;
+  }, {}) as Required<CSSRulesByBucket>;
+  const elements = compile(css);
+  const unrelatedElements: ReturnType<typeof compile> = [];
+
+  let cssBucketName: StyleBucketName | null = null;
+  let cssMeta: Record<string, unknown> | null = null;
+
+  for (const element of elements) {
+    if (element.type === COMMENT) {
+      if (element.value.indexOf('/** @griffel:css-start') === 0) {
+        cssBucketName = element.value.charAt(24) as StyleBucketName;
+
+        if (cssBucketName === 'm') {
+          cssMeta = JSON.parse(element.value.slice(28, -5));
+        }
+
+        continue;
+      }
+
+      if (element.value.indexOf('/** @griffel:css-end') === 0) {
+        cssBucketName = null;
+        cssMeta = null;
+
+        continue;
+      }
+    }
+
+    if (cssBucketName) {
+      const cssRule = serialize([element], stringify);
+      const bucketEntry: CSSBucketEntry = element.type === MEDIA ? [cssRule, cssMeta!] : cssRule;
+
+      cssRulesByBucket[cssBucketName].push(bucketEntry);
+      continue;
+    }
+
+    unrelatedElements.push(element);
+  }
+
+  return { cssRulesByBucket, remainingCSS: serialize(unrelatedElements, stringify) };
+}
diff --git a/packages/webpack-extraction-plugin/src/sortCSSRules.ts b/packages/webpack-extraction-plugin/src/sortCSSRules.ts
index cd11a6d..a990f0d 100644
--- a/packages/webpack-extraction-plugin/src/sortCSSRules.ts
+++ b/packages/webpack-extraction-plugin/src/sortCSSRules.ts
@@ -1,99 +1,58 @@
-import {
-  RESET_HASH_PREFIX,
-  getStyleBucketName,
-  GriffelRenderer,
-  StyleBucketName,
-  styleBucketOrdering,
-} from '@griffel/core';
-import { COMMENT, compile, Element, KEYFRAMES, MEDIA, RULESET, serialize, stringify, SUPPORTS, tokenize } from 'stylis';
+import { styleBucketOrdering } from '@griffel/core';
+import type { GriffelRenderer, CSSBucketEntry, CSSRulesByBucket } from '@griffel/core';
 
-function isResetClassName(className: string) {
-  return className[0] === '.' && className[1] === RESET_HASH_PREFIX;
+function getCSSRuleFromBucketEntry(entry: CSSBucketEntry): string {
+  return Array.isArray(entry) ? entry[0] : entry;
 }
 
-export function getSelectorFromElement(element: Element) {
-  return tokenize(element.value).slice(1).join('');
+function getCSSMetaFromBucketEntry(entry: CSSBucketEntry): Record<string, unknown> {
+  return Array.isArray(entry) ? entry[1] : {};
 }
 
-export function getElementMetadata(element: Element): string {
-  if (element.type === MEDIA) {
-    return element.value.replace(/^@media/, '').trim();
-  }
-
-  return '';
-}
-
-export function getElementReference(element: Element, suffix = ''): string {
-  if (element.type === RULESET || element.type === KEYFRAMES) {
-    return element.value + suffix;
-  }
-
-  if (Array.isArray(element.children)) {
-    return element.value + '[' + element.children.map(child => getElementReference(child, suffix)).join(',') + ']';
-  }
-
-  function removeRootProperty(element: Element): unknown {
-    return {
-      ...element,
-      children: Array.isArray(element.children)
-        ? element.children.map(child => removeRootProperty(child))
-        : element.children,
-      root: undefined,
-      parent: undefined,
-    };
-  }
-
-  throw new Error(
-    [
-      'getElementReference(): An unhandled case, please report if it happens and provide debug information about an element:',
-      JSON.stringify(removeRootProperty(element), null, 2),
-    ].join('\n'),
-  );
-}
-
-export function getStyleBucketNameFromElement(element: Element): StyleBucketName {
-  if (element.type === KEYFRAMES) {
-    return 'k';
-  }
-
-  if (isResetClassName(element.value)) {
-    return 'r';
-  }
-
-  return getStyleBucketName(
-    [getSelectorFromElement(element)],
-    element.type === '@layer' ? element.value : '',
-    element.type === MEDIA ? element.value : '',
-    element.type === SUPPORTS ? element.value : '',
-  );
-}
-
-export function sortCSSRules(css: string, compareMediaQueries: GriffelRenderer['compareMediaQueries']): string {
-  const childElements = compile(css)
-    // Remove top level comments as it is unclear how to sort them
-    .filter(element => element.type !== COMMENT)
-    .map(element => ({
-      ...element,
-      bucketName: getStyleBucketNameFromElement(element),
-      metadata: getElementMetadata(element),
-      reference: getElementReference(element),
-    }));
-  const uniqueElements = childElements.reduce<Record<string, typeof childElements[0]>>((acc, element) => {
-    acc[element.reference] = element;
-
-    return acc;
-  }, {});
-  const sortedElements = Object.values(uniqueElements).sort((elementA, elementB) => {
-    if (elementA.bucketName === 'm' && elementB.bucketName === 'm') {
-      return compareMediaQueries(elementA.metadata, elementB.metadata);
-    }
-
-    if (elementA.bucketName === elementB.bucketName) {
-      return 0;
-    }
-
-    return styleBucketOrdering.indexOf(elementA.bucketName) - styleBucketOrdering.indexOf(elementB.bucketName);
-  });
-
-  return serialize(sortedElements, stringify);
+export function sortCSSRules(
+  setOfCSSRules: CSSRulesByBucket[],
+  compareMediaQueries: GriffelRenderer['compareMediaQueries'],
+) {
+  return styleBucketOrdering
+    .map(styleBucketName => {
+      return {
+        styleBucketName,
+        cssBucketEntries:
+          // We deduplicate CSS rules there by using them as keys in an object:
+          // - create an array with pairs [key, value]
+          // - use Object.fromEntries() to create an object that contains unique values
+          Object.values(
+            Object.fromEntries(
+              setOfCSSRules.flatMap(cssRulesByBucket => {
+                if (Array.isArray(cssRulesByBucket[styleBucketName])) {
+                  return cssRulesByBucket[styleBucketName]!.map(bucketEntry => [
+                    getCSSRuleFromBucketEntry(bucketEntry),
+                    bucketEntry,
+                  ]);
+                }
+
+                return [];
+              }),
+            ),
+          ),
+      };
+    })
+    .reduce((acc, { styleBucketName, cssBucketEntries }) => {
+      if (styleBucketName === 'm') {
+        return (
+          acc +
+          cssBucketEntries
+            .sort((entryA, entryB) => {
+              return compareMediaQueries(
+                getCSSMetaFromBucketEntry(entryA)['m'] as string,
+                getCSSMetaFromBucketEntry(entryB)['m'] as string,
+              );
+            })
+            .map(entry => entry[0])
+            .join('')
+        );
+      }
+
+      return acc + cssBucketEntries.join('');
+    }, '');
 }
diff --git a/packages/webpack-extraction-plugin/src/transformSync.ts b/packages/webpack-extraction-plugin/src/transformSync.ts
index f2183e3..7daf4f7 100644
--- a/packages/webpack-extraction-plugin/src/transformSync.ts
+++ b/packages/webpack-extraction-plugin/src/transformSync.ts
@@ -1,4 +1,6 @@
 import * as Babel from '@babel/core';
+import type { CSSRulesByBucket } from '@griffel/core';
+
 import { babelPluginStripGriffelRuntime, StripRuntimeBabelPluginMetadata } from './babelPluginStripGriffelRuntime';
 
 export type TransformOptions = {
@@ -11,7 +13,7 @@ export type TransformOptions = {
 
 export type TransformResult = {
   code: string;
-  cssRules: string[] | undefined;
+  cssRulesByBucket: CSSRulesByBucket | undefined;
   sourceMap: NonNullable<Babel.BabelFileResult['map']> | undefined;
 };
 
@@ -52,7 +54,7 @@ export function transformSync(sourceCode: string, options: TransformOptions): Tr
 
   return {
     code: babelFileResult.code as string,
-    cssRules: (babelFileResult.metadata as unknown as StripRuntimeBabelPluginMetadata).cssRules,
+    cssRulesByBucket: (babelFileResult.metadata as unknown as StripRuntimeBabelPluginMetadata).cssRulesByBucket,
     sourceMap: babelFileResult.map === null ? undefined : babelFileResult.map,
   };
 }
diff --git a/packages/webpack-extraction-plugin/src/webpackLoader.ts b/packages/webpack-extraction-plugin/src/webpackLoader.ts
index 7ee4918..73600ef 100644
--- a/packages/webpack-extraction-plugin/src/webpackLoader.ts
+++ b/packages/webpack-extraction-plugin/src/webpackLoader.ts
@@ -1,3 +1,4 @@
+import { normalizeCSSBucketEntry } from '@griffel/core';
 import { getOptions } from 'loader-utils';
 import * as path from 'path';
 import { validate } from 'schema-utils';
@@ -19,10 +20,6 @@ function toURIComponent(rule: string): string {
   return encodeURIComponent(rule).replace(/!/g, '%21');
 }
 
-function shouldTransformSourceCode(sourceCode: string): boolean {
-  return sourceCode.indexOf('__styles') !== -1 || sourceCode.indexOf('__resetStyles') !== -1;
-}
-
 /**
  * Webpack can also pass sourcemaps as a string, Babel accepts only objects.
  * See https://github.com/babel/babel-loader/pull/889.
@@ -57,7 +54,7 @@ function webpackLoader(
   });
 
   // Early return to handle cases when __styles() calls are not present, allows skipping expensive invocation of Babel
-  if (!shouldTransformSourceCode(sourceCode)) {
+  if (sourceCode.indexOf('__styles') === -1 && sourceCode.indexOf('__resetStyles') === -1) {
     this.callback(null, sourceCode, inputSourceMap);
     return;
   }
@@ -78,11 +75,39 @@ function webpackLoader(
   }
 
   if (result) {
-    if (result.cssRules) {
+    if (result.cssRulesByBucket) {
+      const css = Object.entries(result.cssRulesByBucket).reduce((acc, [cssBucketName, cssBucketRules]) => {
+        if (cssBucketName === 'm') {
+          return (
+            acc +
+            cssBucketRules
+              .map(entry => {
+                return [
+                  `/** @griffel:css-start [${cssBucketName}] [${JSON.stringify(entry[1])}] **/`,
+                  normalizeCSSBucketEntry(entry)[0],
+                  `/** @griffel:css-end **/`,
+                  '',
+                ].join('\n');
+              })
+              .join('')
+          );
+        }
+
+        return (
+          acc +
+          [
+            `/** @griffel:css-start [${cssBucketName}] **/`,
+            cssBucketRules.flatMap(entry => normalizeCSSBucketEntry(entry)).join(''),
+            `/** @griffel:css-end **/`,
+            '',
+          ].join('\n')
+        );
+      }, '');
+
       const request = `import ${JSON.stringify(
         this.utils.contextify(
           this.context || this.rootContext,
-          `griffel.css!=!${virtualLoaderPath}!${resourcePath}?style=${toURIComponent(result.cssRules.join('\n'))}`,
+          `griffel.css!=!${virtualLoaderPath}!${resourcePath}?style=${toURIComponent(css)}`,
         ),
       )};`;
 
diff --git a/yarn.lock b/yarn.lock
index 97190c0..48e5b2d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11515,9 +11515,9 @@ __metadata:
   linkType: hard
 
 "deep-object-diff@npm:^1.1.0":
-  version: 1.1.7
-  resolution: "deep-object-diff@npm:1.1.7"
-  checksum: 543fb1ae87b138ad260691e6949e72bf7dc144825084b7ad1886bb725d2ace1c19ed1ef1280f1116243e86bf2c6b942f45c670958b1468f644613f28c5dc97ea
+  version: 1.1.9
+  resolution: "deep-object-diff@npm:1.1.9"
+  checksum: ecd42455e4773f653595d28070295e7aaa8402db5f8ab21d0bec115a7cb4de5e207a5665514767da5f025c96597f1d3a0a4888aeb4dd49e03c996871a3aa05ef
   languageName: node
   linkType: hard
 


Test patch:
diff --git a/packages/webpack-extraction-plugin/src/parseCSSRules.test.ts b/packages/webpack-extraction-plugin/src/parseCSSRules.test.ts
new file mode 100644
index 0000000..aa1ec1e
--- /dev/null
+++ b/packages/webpack-extraction-plugin/src/parseCSSRules.test.ts
@@ -0,0 +1,91 @@
+import { parseCSSRules } from './parseCSSRules';
+import { CSSRulesByBucket } from '@griffel/core';
+
+function removeEmptyBuckets(cssRulesByBucket: CSSRulesByBucket) {
+  return Object.fromEntries(Object.entries(cssRulesByBucket).filter(([, bucketEntries]) => bucketEntries.length > 0));
+}
+
+describe('parseCSSRules', () => {
+  it('handles regular rules', () => {
+    const css = `
+    /** @griffel:css-start [d] **/
+    .fe3e8s9 { color: red; }
+    /** @griffel:css-end **/
+    /** @griffel:css-start [h] **/
+    .faf35ka:hover { color: red; }
+    /** @griffel:css-end **/
+  `;
+    const { cssRulesByBucket, remainingCSS } = parseCSSRules(css);
+
+    expect(removeEmptyBuckets(cssRulesByBucket)).toMatchInlineSnapshot(`
+      Object {
+        "d": Array [
+          ".fe3e8s9{color:red;}",
+        ],
+        "h": Array [
+          ".faf35ka:hover{color:red;}",
+        ],
+      }
+    `);
+    expect(remainingCSS).toBe('');
+  });
+
+  it('handles rules with meta', () => {
+    const css = `
+    /** @griffel:css-start [d] **/
+    .fe3e8s9 { color: red; }
+    /** @griffel:css-end **/
+    /** @griffel:css-start [m] [{"m":"screen and (max-width: 100px)"}] **/
+    @media screen and (max-width: 100px) { .fr5o61b{ color:red; } }
+    /** @griffel:css-end **/
+    /** @griffel:css-start [m] [{"m":"screen and (max-width: 100px)"}] **/
+    @media screen and (max-width: 100px) { .f1j0ers2 { display: grid; } }
+    /** @griffel:css-end **/
+  `;
+    const { cssRulesByBucket, remainingCSS } = parseCSSRules(css);
+
+    expect(removeEmptyBuckets(cssRulesByBucket)).toMatchInlineSnapshot(`
+      Object {
+        "d": Array [
+          ".fe3e8s9{color:red;}",
+        ],
+        "m": Array [
+          Array [
+            "@media screen and (max-width: 100px){.fr5o61b{color:red;}}",
+            Object {
+              "m": "screen and (max-width: 100px)",
+            },
+          ],
+          Array [
+            "@media screen and (max-width: 100px){.f1j0ers2{display:grid;}}",
+            Object {
+              "m": "screen and (max-width: 100px)",
+            },
+          ],
+        ],
+      }
+    `);
+    expect(remainingCSS).toBe('');
+  });
+
+  it('keeps third parties CSS', () => {
+    const css = `
+    /** @griffel:css-start [d] **/
+    .fe3e8s9 { color: red; }
+    /** @griffel:css-end **/
+    .foo { color: red }
+    /* some comment */
+    .bar { color: green }
+  `;
+    const { cssRulesByBucket, remainingCSS } = parseCSSRules(css);
+
+    expect(removeEmptyBuckets(cssRulesByBucket)).toMatchInlineSnapshot(`
+      Object {
+        "d": Array [
+          ".fe3e8s9{color:red;}",
+        ],
+      }
+    `);
+    expect(remainingCSS).toBe('.foo{color:red;}.bar{color:green;}');
+  });
+});
diff --git a/packages/webpack-extraction-plugin/src/sortCSSRules.test.ts b/packages/webpack-extraction-plugin/src/sortCSSRules.test.ts
index 2760007..5c4c100 100644
--- a/packages/webpack-extraction-plugin/src/sortCSSRules.test.ts
+++ b/packages/webpack-extraction-plugin/src/sortCSSRules.test.ts
@@ -1,8 +1,7 @@
-import { GriffelRenderer } from '@griffel/core';
+import { CSSRulesByBucket, GriffelRenderer } from '@griffel/core';
 import * as prettier from 'prettier';
-import { compile } from 'stylis';
 
-import { getElementReference, getElementMetadata, sortCSSRules } from './sortCSSRules';
+import { sortCSSRules } from './sortCSSRules';
 
 export const cssSerializer: jest.SnapshotSerializerPlugin = {
   test(value) {
@@ -20,47 +19,18 @@ export const cssSerializer: jest.SnapshotSerializerPlugin = {
 
 expect.addSnapshotSerializer(cssSerializer);
 
-describe('getElementReference', () => {
-  it.each`
-    css                                                                                              | reference
-    ${'.foo { color: red; }'}                                                                        | ${'.foo'}
-    ${'.foo:hover { color: red; }'}                                                                  | ${'.foo:hover'}
-    ${'@media (max-width: 2px) { .foo { color: blue; } }'}                                           | ${'@media (max-width: 2px)[.foo]'}
-    ${'@keyframes foo { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }'}        | ${'@keyframes foo'}
-    ${'@media screen and (max-width: 992px) { .a { text-align: left; } .b { text-align: right; } }'} | ${'@media screen and (max-width: 992px)[.a,.b]'}
-    ${'@media (max-width: 2px) { @supports (display: grid) { .a { color: blue; } } }'}               | ${'@media (max-width: 2px)[@supports (display: grid)[.a]]'}
-  `('returns "$reference" for "$css"', ({ css, reference }: { css: string; reference: string }) => {
-    const element = compile(css)[0];
-
-    expect(getElementReference(element)).toBe(reference);
-  });
-});
-
-describe('getElementMetadata', () => {
-  it.each`
-    css                                                    | metadata
-    ${'.foo { color: red; }'}                              | ${''}
-    ${'.foo:hover { color: red; }'}                        | ${''}
-    ${'@media (max-width: 2px) { .foo { color: blue; } }'} | ${'(max-width: 2px)'}
-  `('returns "$reference" for "$css"', ({ css, metadata }: { css: string; metadata: string }) => {
-    const element = compile(css)[0];
-
-    expect(getElementMetadata(element)).toBe(metadata);
-  });
-});
-
 describe('sortCSSRules', () => {
   it('removes duplicate rules', () => {
-    const css = `
-      .baz { color: orange; }
-      .foo { color: red; }
-      .baz { color: orange; }
-
-      @media (max-width: 2px) { .foo { color: blue; } }
-      @media (max-width: 2px) { .yellow { color: blue; } }
-    `;
-
-    expect(sortCSSRules(css, () => 0)).toMatchInlineSnapshot(`
+    const setA: CSSRulesByBucket = {
+      d: ['.baz { color: orange; }', '.foo { color: red; }'],
+      m: [['@media (max-width: 2px) { .foo { color: blue; } }', { m: '(max-width: 2px)' }]],
+    };
+    const setB: CSSRulesByBucket = {
+      d: ['.baz { color: orange; }'],
+      m: [['@media (max-width: 2px) { .yellow { color: blue; } }', { m: '(max-width: 2px)' }]],
+    };
+
+    expect(sortCSSRules([setA, setB], () => 0)).toMatchInlineSnapshot(`
       .baz {
         color: orange;
       }
@@ -81,14 +51,16 @@ describe('sortCSSRules', () => {
   });
 
   it('sorts rules by buckets order', () => {
-    const css = `
-      .foo:focus { color: pink; }
-      .baz { color: orange; }
-      .foo:hover { color: yellow; }
-      .foo { color: red; }
-    `;
-
-    expect(sortCSSRules(css, () => 0)).toMatchInlineSnapshot(`
+    const setA: CSSRulesByBucket = {
+      d: ['.baz { color: orange; }'],
+      f: ['.foo:focus { color: pink; }'],
+    };
+    const setB: CSSRulesByBucket = {
+      d: ['.foo { color: red; }'],
+      h: ['.foo:hover { color: yellow; }'],
+    };
+
+    expect(sortCSSRules([setA, setB], () => 0)).toMatchInlineSnapshot(`
       .baz {
         color: orange;
       }
@@ -105,18 +77,22 @@ describe('sortCSSRules', () => {
   });
 
   it('sorts media queries', () => {
-    const css = `
-      @media (max-width: 2px) { .foo { color: blue; } }
-      @media (max-width: 1px) { .foo { color: red; } }
-      @media (max-width: 3px) { .foo { color: red; } }
-      .foo { color: green; }
-    `;
+    const setA: CSSRulesByBucket = {
+      m: [
+        ['@media (max-width: 2px) { .foo { color: blue; } }', { m: '(max-width: 2px)' }],
+        ['@media (max-width: 3px) { .foo { color: red; } }', { m: '(max-width: 3px)' }],
+      ],
+    };
+    const setB: CSSRulesByBucket = {
+      d: ['.foo { color: green; }'],
+      m: [['@media (max-width: 1px) { .foo { color: red; } }', { m: '(max-width: 1px)' }]],
+    };
 
     const mediaQueryOrder = ['(max-width: 1px)', '(max-width: 2px)', '(max-width: 3px)', '(max-width: 4px)'];
     const compareMediaQueries: GriffelRenderer['compareMediaQueries'] = (a: string, b: string) =>
       mediaQueryOrder.indexOf(a) - mediaQueryOrder.indexOf(b);
 
-    expect(sortCSSRules(css, compareMediaQueries)).toMatchInlineSnapshot(`
+    expect(sortCSSRules([setA, setB], compareMediaQueries)).toMatchInlineSnapshot(`
       .foo {
         color: green;
       }
@@ -137,17 +113,4 @@ describe('sortCSSRules', () => {
       }
     `);
   });
-
-  it('removes comments from output', () => {
-    const css = `
-      /* This is a comment in CSS */
-      .baz { color: orange; }
-    `;
-
-    expect(sortCSSRules(css, () => 0)).toMatchInlineSnapshot(`
-      .baz {
-        color: orange;
-      }
-    `);
-  });
 });