Repository: microsoft/griffel
Pull request #135 (2022-06-14 14:39:15)

feat: add Babel plugin for CSS extraction
This PR implements the Babel plugin that will be used for CSS extraction.

This plugin replaces `__styles` with `__css` function and removes CSS rules from output:

```tsx
import { __styles } from '@griffel/react';

export const stylesA = __styles(
  {
    root: {
      sj55zd: 'fe3e8s9',
    },
  },
  {
    d: ['.fe3e8s9{color:red;}'],
  },
);

export const stylesB = __styles(
  {
    root: {
      De3pzq: 'fcnqdeg',
    },
  },
  {
    d: ['.fcnqdeg{background-color:green;}'],
  },
);

//
// ⬇️⬇️⬇️
//

import { __styles, __css } from '@griffel/react';

export const stylesA = __css({
  root: {
    sj55zd: 'fe3e8s9',
  },
});

export const stylesB = __css({
  root: {
    De3pzq: 'fcnqdeg',
  },
});
```

CSS rules are returned as part of `meta` informantion.



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

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

<!-- Sticky Pull Request Comment -->

Patch:
diff --git a/apps/benchmark/README.md b/apps/benchmark/README.md
new file mode 100644
index 0000000..5fe1a72
--- /dev/null
+++ b/apps/benchmark/README.md
@@ -0,0 +1,17 @@
+# Performance benchmark
+
+This app runs a performance benchmark of different css styling implementations courtesy of [react-native-web](https://github.com/necolas/react-native-web/tree/master/packages/benchmarks).
+
+## Usage
+
+To start benchmarking run these commands from the root of the repo:
+
+```shell
+# Builds the app and watches for changes
+yarn nx run @griffel/benchmark:watch
+```
+
+```shell
+# Serves the benchmark app to use in the browser
+yarn nx run @griffel/benchmark:serve
+```
diff --git a/apps/benchmark/build.js b/apps/benchmark/build.js
new file mode 100644
index 0000000..13d2ab7
--- /dev/null
+++ b/apps/benchmark/build.js
@@ -0,0 +1,40 @@
+const esbuild = require('esbuild');
+const ImportGlobPlugin = require('esbuild-plugin-import-glob').default;
+const fs = require('fs');
+const path = require('path');
+const yargs = require('yargs');
+
+const argv = yargs(process.argv)
+  .options({
+    watch: { type: 'boolean', default: false },
+  })
+  .parseSync();
+
+const outDir = path.resolve(__dirname, '../../dist/apps/benchmark/');
+
+esbuild
+  .build({
+    entryPoints: ['./src/index.tsx'],
+    outfile: path.join(outDir, 'bundle.js'),
+    minify: true,
+    bundle: true,
+    plugins: [ImportGlobPlugin()],
+    ...(argv.watch && {
+      watch: {
+        onRebuild(error, result) {
+          if (error) {
+            console.error('watch build failed:', error);
+          } else {
+            console.log('watch build succeeded:', result);
+          }
+        },
+      },
+    }),
+  })
+  .then(() => {
+    if (argv.watch) {
+      console.log('watching');
+    }
+    fs.copyFileSync(path.resolve(__dirname, './index.html'), path.join(outDir, 'index.html'));
+  })
+  .catch(() => process.exit(1));
diff --git a/apps/benchmark/index.html b/apps/benchmark/index.html
new file mode 100644
index 0000000..ae1dd71
--- /dev/null
+++ b/apps/benchmark/index.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>Performance tests</title>
+    <meta name="viewport" content="width=device-width" />
+    <link rel="stylesheet" type="text/css" href="./bundle.css" />
+    <style>
+      html,
+      body {
+        height: 100%;
+        width: 100%;
+        overflow: hidden;
+      }
+      #root {
+        height: 100%;
+        overflow: hidden;
+      }
+    </style>
+  </head>
+  <body>
+    <div id="root"></div>
+    <script src="./bundle.js"></script>
+  </body>
+</html>
diff --git a/apps/benchmark/package.json b/apps/benchmark/package.json
new file mode 100644
index 0000000..84888b7
--- /dev/null
+++ b/apps/benchmark/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "@griffel/benchmark",
+  "description": "Performance benchmarking app for Griffel",
+  "private": true,
+  "dependencies": {}
+}
diff --git a/apps/benchmark/project.json b/apps/benchmark/project.json
new file mode 100644
index 0000000..09983c9
--- /dev/null
+++ b/apps/benchmark/project.json
@@ -0,0 +1,39 @@
+{
+  "root": "apps/benchmark",
+  "sourceRoot": "apps/benchmark/src",
+  "projectType": "application",
+  "targets": {
+    "build": {
+      "executor": "@nrwl/workspace:run-commands",
+      "options": {
+        "cwd": "apps/benchmark",
+        "commands": [{ "command": "node ./build.js" }],
+        "outputPath": ["dist/apps/benchmark"]
+      }
+    },
+    "watch": {
+      "executor": "@nrwl/workspace:run-commands",
+      "options": {
+        "cwd": "apps/benchmark",
+        "commands": [{ "command": "node ./build.js --watch" }],
+        "outputPath": ["dist/apps/benchmark"]
+      }
+    },
+    "serve": {
+      "executor": "@nrwl/workspace:run-commands",
+      "options": {
+        "cwd": "apps/benchmark",
+        "commands": [{ "command": "npx http-serve ../../dist/apps/benchmark" }]
+      }
+    },
+    "type-check": {
+      "executor": "@nrwl/workspace:run-commands",
+      "options": {
+        "cwd": "apps/benchmark",
+        "commands": [{ "command": "tsc -b --pretty" }],
+        "outputPath": []
+      }
+    }
+  },
+  "tags": []
+}
diff --git a/apps/benchmark/src/App.css b/apps/benchmark/src/App.css
new file mode 100644
index 0000000..d67dae5
--- /dev/null
+++ b/apps/benchmark/src/App.css
@@ -0,0 +1,73 @@
+.app-root {
+  display: flex;
+  height: 100%;
+}
+
+.benchmark-container {
+  flex-grow: 1;
+  flex-shrink: 1;
+  flex-basis: 0%;
+}
+
+.benchmark-actions {
+  display: flex;
+}
+
+.benchmark-action {
+  min-height: 40px;
+  font-size: 20px;
+  cursor: pointer;
+  flex-grow: 1;
+}
+
+.results-list {
+  margin-top: 10px;
+  border-top: 1px solid black;
+  padding: 10px 0;
+}
+
+.render-container {
+  background-color: black;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-grow: 1;
+  flex-shrink: 1;
+  flex-basis: 0%;
+}
+
+.report-card {
+  margin: 10px 0;
+  padding: 4px 10px;
+  border-bottom: 1px solid gray;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.report-card .main {
+  font-size: 20px;
+  font-weight: 700;
+}
+
+.report-card .column {
+ display: flex;
+ flex-direction: column;
+}
+
+.select {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.select select {
+  cursor: pointer;
+  background-color: transparent;
+  padding: 0 1em;
+  border-radius: 4px;
+  box-shadow: none;
+  box-sizing: border-box;
+  font-size: 20px;
+}
+
diff --git a/apps/benchmark/src/App.tsx b/apps/benchmark/src/App.tsx
new file mode 100644
index 0000000..6eb05d6
--- /dev/null
+++ b/apps/benchmark/src/App.tsx
@@ -0,0 +1,155 @@
+import React from 'react';
+import implementations from './implementations';
+import { ReportCard, Select } from './components';
+import tests from './tests';
+import { Benchmark } from './Benchmark';
+import type { BenchDisplayResults, BenchmarkName, BenchResultsType, LibraryName } from './types';
+
+type AppAction =
+  | { type: 'SELECT_BENCHMARK'; payload: BenchmarkName }
+  | { type: 'SELECT_LIBRARY'; payload: { library: LibraryName; version?: string } }
+  | { type: 'BENCHMARK_COMPLETED'; payload: BenchResultsType }
+  | { type: 'CLEAR_RESULTS' }
+  | { type: 'START_BENCHMARK' };
+
+interface AppState {
+  benchmark: BenchmarkName;
+  library: LibraryName;
+  version?: string;
+  results: BenchDisplayResults[];
+  inProgress: boolean;
+}
+
+const appInitialState: AppState = {
+  benchmark: 'Mount deep tree',
+  library: Object.keys(implementations)[0],
+  results: [],
+  inProgress: false,
+};
+
+function appReducer(state: AppState, action: AppAction): AppState {
+  switch (action.type) {
+    case 'SELECT_LIBRARY':
+      return {
+        ...state,
+        library: action.payload.library,
+        version: action.payload.version,
+      };
+    case 'SELECT_BENCHMARK':
+      return {
+        ...state,
+        benchmark: action.payload,
+      };
+    case 'CLEAR_RESULTS':
+      return {
+        ...state,
+        results: [],
+      };
+    case 'BENCHMARK_COMPLETED':
+      const resultsWithCompletedBenchmark = [...state.results].slice(0, -1);
+      resultsWithCompletedBenchmark.push({
+        analysis: { ...action.payload },
+        library: state.library,
+        benchmark: state.benchmark,
+      });
+      return {
+        ...state,
+        inProgress: false,
+        results: resultsWithCompletedBenchmark,
+      };
+
+    case 'START_BENCHMARK':
+      const resultsWithNewBenchmark = [...state.results];
+      resultsWithNewBenchmark.push({
+        version: state.version,
+        library: state.library,
+        benchmark: state.benchmark,
+      });
+      return { ...state, inProgress: true, results: resultsWithNewBenchmark };
+  }
+}
+
+export const App: React.FC = () => {
+  const [state, dispatch] = React.useReducer(appReducer, appInitialState);
+  const { library, benchmark, results, inProgress } = state;
+  const imperativeRef = React.useRef({ start: () => null });
+  React.useEffect(() => {
+    if (inProgress) {
+      imperativeRef.current.start();
+    }
+  }, [inProgress]);
+
+  const benchmarkToRun = tests[benchmark][library];
+  const {
+    components: { Provider },
+  } = implementations[library];
+
+  return (
+    <div className="app-root">
+      <div className="benchmark-container">
+        <div className="benchmark-actions">
+          <button
+            className="benchmark-action"
+            disabled={inProgress}
+            onClick={() => {
+              dispatch({ type: 'START_BENCHMARK' });
+            }}
+          >
+            {inProgress ? 'Running...' : 'Run'}
+          </button>
+          <button className="benchmark-action" onClick={() => dispatch({ type: 'CLEAR_RESULTS' })}>
+            Clear results
+          </button>
+        </div>
+        <Select
+          label="Library"
+          value={library}
+          onChange={e =>
+            dispatch({
+              type: 'SELECT_LIBRARY',
+              payload: { library: e.target.value, version: implementations[e.target.value].version },
+            })
+          }
+        >
+          {Object.keys(implementations).map(implementation => (
+            <option value={implementation} key={implementation}>
+              {implementation}
+            </option>
+          ))}
+        </Select>
+        <Select
+          label="Benchmark"
+          value={benchmark}
+          onChange={e => dispatch({ type: 'SELECT_BENCHMARK', payload: e.target.value as BenchmarkName })}
+        >
+          {Object.keys(tests).map(test => (
+            <option value={test} key={test}>
+              {test}
+            </option>
+          ))}
+        </Select>
+        <div className="results-list">
+          {results.map(result => (
+            <ReportCard {...result} version={result.version} />
+          ))}
+        </div>
+      </div>
+      <div className="render-container">
+        <Provider>
+          {inProgress ? (
+            <Benchmark
+              forceLayout
+              // TODO benchmark code uses legacy refs that can't be typed - refactor it to be functional
+              // @ts-expect-error
+              ref={imperativeRef}
+              {...benchmarkToRun}
+              onComplete={res => dispatch({ type: 'BENCHMARK_COMPLETED', payload: res })}
+            />
+          ) : (
+            <benchmarkToRun.Component {...benchmarkToRun.getComponentProps({ cycle: 10 })} />
+          )}
+        </Provider>
+      </div>
+    </div>
+  );
+};
diff --git a/apps/benchmark/src/Benchmark/index.tsx b/apps/benchmark/src/Benchmark/index.tsx
new file mode 100644
index 0000000..04f91a2
--- /dev/null
+++ b/apps/benchmark/src/Benchmark/index.tsx
@@ -0,0 +1,249 @@
+/**
+ * Core benchmarking code is not original and
+ * taken from https://github.com/necolas/react-native-web/tree/master/packages/benchmarks
+ */
+
+import React, { Component } from 'react';
+import { getMean, getMedian, getStdDev } from './utils/math';
+import * as Timing from './utils/timing';
+
+import type {
+  BenchResultsType,
+  FullSampleTimingType,
+  SampleTimingType,
+  BenchmarkType,
+  BenchmarkParameters,
+} from '../types';
+
+export const BenchmarkTypes = {
+  MOUNT: 'mount',
+  UPDATE: 'update',
+  UNMOUNT: 'unmount',
+} as const;
+
+const shouldRender = (cycle: number, type: BenchmarkType): boolean => {
+  switch (type) {
+    // Render every odd iteration (first, third, etc)
+    // Mounts and unmounts the component
+    case BenchmarkTypes.MOUNT:
+    case BenchmarkTypes.UNMOUNT:
+      return !((cycle + 1) % 2);
+    // Render every iteration (updates previously rendered module)
+    case BenchmarkTypes.UPDATE:
+      return true;
+    default:
+      return false;
+  }
+};
+
+const shouldRecord = (cycle: number, type: BenchmarkType): boolean => {
+  switch (type) {
+    // Record every odd iteration (when mounted: first, third, etc)
+    case BenchmarkTypes.MOUNT:
+      return !((cycle + 1) % 2);
+    // Record every iteration
+    case BenchmarkTypes.UPDATE:
+      return true;
+    // Record every even iteration (when unmounted)
+    case BenchmarkTypes.UNMOUNT:
+      return !(cycle % 2);
+    default:
+      return false;
+  }
+};
+
+const isDone = (cycle: number, sampleCount: number, type: BenchmarkType): boolean => {
+  switch (type) {
+    case BenchmarkTypes.MOUNT:
+      return cycle >= sampleCount * 2 - 1;
+    case BenchmarkTypes.UPDATE:
+      return cycle >= sampleCount - 1;
+    case BenchmarkTypes.UNMOUNT:
+      return cycle >= sampleCount * 2;
+    default:
+      return true;
+  }
+};
+
+const sortNumbers = (a: number, b: number): number => a - b;
+
+interface BenchmarkProps extends BenchmarkParameters {
+  forceLayout?: boolean;
+  timeout: number;
+  onComplete: (x: BenchResultsType) => void;
+}
+
+type BenchmarkState = {
+  componentProps: object;
+  cycle: number;
+  running: boolean;
+};
+
+/**
+ * Benchmark
+ * TODO: documentation
+ */
+export class Benchmark extends Component<BenchmarkProps, BenchmarkState> {
+  _raf?: number;
+  _startTime: number;
+  _samples: Array<SampleTimingType>;
+
+  static displayName = 'Benchmark';
+
+  static defaultProps: {
+    sampleCount: number;
+    timeout: number;
+    type: BenchmarkProps['type'];
+  } = {
+    sampleCount: 50,
+    timeout: 10000, // 10 seconds
+    type: BenchmarkTypes.MOUNT,
+  };
+
+  constructor(props: BenchmarkProps, context?: object) {
+    super(props, context);
+    const cycle = 0;
+    const componentProps = props.getComponentProps({ cycle });
+    this.state = {
+      componentProps,
+      cycle,
+      running: false,
+    };
+    this._startTime = 0;
+    this._samples = [];
+  }
+
+  override componentWillReceiveProps(nextProps: BenchmarkProps) {
+    if (nextProps) {
+      this.setState(state => ({ componentProps: nextProps.getComponentProps(state.cycle) }));
+    }
+  }
+
+  override componentWillUpdate(nextProps: BenchmarkProps, nextState: BenchmarkState) {
+    if (nextState.running && !this.state.running) {
+      this._startTime = Timing.now();
+    }
+  }
+
+  override componentDidUpdate() {
+    const { forceLayout, sampleCount, timeout, type } = this.props;
+    const { cycle, running } = this.state;
+
+    if (running && shouldRecord(cycle, type)) {
+      this._samples[cycle].scriptingEnd = Timing.now();
+
+      // force style recalc that would otherwise happen before the next frame
+      if (forceLayout) {
+        this._samples[cycle].layoutStart = Timing.now();
+        if (document.body) {
+          document.body.offsetWidth;
+        }
+        this._samples[cycle].layoutEnd = Timing.now();
+      }
+    }
+
+    if (running) {
+      const now = Timing.now();
+      if (!isDone(cycle, sampleCount, type) && now - this._startTime < timeout) {
+        this._handleCycleComplete();
+      } else {
+        this._handleComplete(now);
+      }
+    }
+  }
+
+  override componentWillUnmount() {
+    if (this._raf) {
+      window.cancelAnimationFrame(this._raf);
+    }
+  }
+
+  override render() {
+    const { Component, type } = this.props;
+    const { componentProps, cycle, running } = this.state;
+    if (running && shouldRecord(cycle, type)) {
+      this._samples[cycle] = { scriptingStart: Timing.now() };
+    }
+
+    return running && shouldRender(cycle, type) ? <Component {...componentProps} /> : null;
+  }
+
+  start() {
+    this._samples = [];
+    this.setState(() => ({ running: true, cycle: 0 }));
+  }
+
+  _handleCycleComplete() {
+    const { getComponentProps, type } = this.props;
+    const { cycle } = this.state;
+
+    let componentProps: Record<string, string | number>;
+    if (getComponentProps) {
+      // Calculate the component props outside of the time recording (render)
+      // so that it doesn't skew results
+      componentProps = getComponentProps({ cycle });
+      // make sure props always change for update tests
+      if (type === BenchmarkTypes.UPDATE) {
+        componentProps['data-test'] = cycle;
+      }
+    }
+
+    this._raf = window.requestAnimationFrame(() => {
+      this.setState((state: BenchmarkState) => ({
+        cycle: state.cycle + 1,
+        componentProps,
+      }));
+    });
+  }
+
+  getSamples(): FullSampleTimingType[] {
+    return this._samples.reduce(
+      (
+        memo: FullSampleTimingType[],
+        { scriptingStart, scriptingEnd, layoutStart, layoutEnd }: SampleTimingType,
+      ): FullSampleTimingType[] => {
+        memo.push({
+          start: scriptingStart,
+          end: layoutEnd || scriptingEnd || 0,
+          scriptingStart,
+          scriptingEnd: scriptingEnd || 0,
+          layoutStart,
+          layoutEnd,
+        });
+        return memo;
+      },
+      [],
+    );
+  }
+
+  _handleComplete(endTime: number) {
+    const { onComplete } = this.props;
+    const samples = this.getSamples();
+
+    this.setState(() => ({ running: false, cycle: 0 }));
+
+    const runTime = endTime - this._startTime;
+    const sortedElapsedTimes = samples.map(({ start, end }) => end - start).sort(sortNumbers);
+    const sortedScriptingElapsedTimes = samples
+      .map(({ scriptingStart, scriptingEnd }) => scriptingEnd - scriptingStart)
+      .sort(sortNumbers);
+    const sortedLayoutElapsedTimes = samples
+      .map(({ layoutStart, layoutEnd }) => (layoutEnd || 0) - (layoutStart || 0))
+      .sort(sortNumbers);
+
+    onComplete({
+      startTime: this._startTime,
+      endTime,
+      runTime,
+      sampleCount: samples.length,
+      samples: samples,
+      max: sortedElapsedTimes[sortedElapsedTimes.length - 1],
+      min: sortedElapsedTimes[0],
+      median: getMedian(sortedElapsedTimes),
+      mean: getMean(sortedElapsedTimes),
+      stdDev: getStdDev(sortedElapsedTimes),
+      meanLayout: getMean(sortedLayoutElapsedTimes),
+      meanScripting: getMean(sortedScriptingElapsedTimes),
+    });
+  }
+}
diff --git a/apps/benchmark/src/Benchmark/utils/math.ts b/apps/benchmark/src/Benchmark/utils/math.ts
new file mode 100644
index 0000000..0186776
--- /dev/null
+++ b/apps/benchmark/src/Benchmark/utils/math.ts
@@ -0,0 +1,26 @@
+type ValuesType = Array<number>;
+
+export const getStdDev = (values: ValuesType): number => {
+  const avg = getMean(values);
+
+  const squareDiffs = values.map((value: number) => {
+    const diff = value - avg;
+    return diff * diff;
+  });
+
+  return Math.sqrt(getMean(squareDiffs));
+};
+
+export const getMean = (values: ValuesType): number => {
+  const sum = values.reduce((sum: number, value: number) => sum + value, 0);
+  return sum / values.length;
+};
+
+export const getMedian = (values: ValuesType): number => {
+  if (values.length === 1) {
+    return values[0];
+  }
+
+  const numbers = values.sort((a: number, b: number) => a - b);
+  return (numbers[(numbers.length - 1) >> 1] + numbers[numbers.length >> 1]) / 2;
+};
diff --git a/apps/benchmark/src/Benchmark/utils/timing.ts b/apps/benchmark/src/Benchmark/utils/timing.ts
new file mode 100644
index 0000000..3b4ef50
--- /dev/null
+++ b/apps/benchmark/src/Benchmark/utils/timing.ts
@@ -0,0 +1,16 @@
+const NS_PER_MS = 1e6;
+const MS_PER_S = 1e3;
+
+// Returns a high resolution time (if possible) in milliseconds
+export function now(): number {
+  if (window && window.performance) {
+    return window.performance.now();
+  } else if (process && process.hrtime) {
+    const [seconds, nanoseconds] = process.hrtime();
+    const secInMS = seconds * MS_PER_S;
+    const nSecInMS = nanoseconds / NS_PER_MS;
+    return secInMS + nSecInMS;
+  } else {
+    return Date.now();
+  }
+}
diff --git a/apps/benchmark/src/components/ReportCard.tsx b/apps/benchmark/src/components/ReportCard.tsx
new file mode 100644
index 0000000..478f6d6
--- /dev/null
+++ b/apps/benchmark/src/components/ReportCard.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import { BenchDisplayResults } from '../types';
+
+interface ReportCardProps extends Partial<BenchDisplayResults> {
+  version?: string;
+}
+
+const fmt = (time: number) => {
+  const milliSecs = Number(Math.round(time * 100) / 100);
+  const str = milliSecs.toFixed(2);
+  return 10 / milliSecs > 1 ? `0${str}` : str;
+};
+
+const ReportCard: React.FC<ReportCardProps> = ({ library, version, benchmark, analysis }) => {
+  const sampleCountText = analysis ? `(${analysis.sampleCount})` : '';
+  const versionText = version ? `@${version}` : '';
+
+  return (
+    <div className="report-card">
+      <div className="column">
+        <div className="main">{`${library}${versionText}`}</div>
+        <div>
+          {benchmark} {sampleCountText}
+        </div>
+      </div>
+      <div className="column">
+        {analysis ? (
+          <>
+            <div className="main">
+              {fmt(analysis.mean)} ±{fmt(analysis.stdDev)} ms
+            </div>
+            <div>
+              (S/L) {fmt(analysis.meanScripting)}/{fmt(analysis.meanLayout)} ms
+            </div>
+          </>
+        ) : (
+          <div className="main">In progress...</div>
+        )}
+      </div>
+    </div>
+  );
+};
+
+export default ReportCard;
diff --git a/apps/benchmark/src/components/Select.tsx b/apps/benchmark/src/components/Select.tsx
new file mode 100644
index 0000000..84b1527
--- /dev/null
+++ b/apps/benchmark/src/components/Select.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+interface SelectProps {
+  label: string;
+}
+
+const Select: React.FC<SelectProps & React.SelectHTMLAttributes<HTMLSelectElement>> = props => {
+  const { children, label, ...rest } = props;
+  return (
+    <div className="select">
+      <label>{label}</label>
+      <select {...rest}>{children}</select>
+    </div>
+  );
+};
+
+export default Select;
diff --git a/apps/benchmark/src/components/index.ts b/apps/benchmark/src/components/index.ts
new file mode 100644
index 0000000..4b81c7c
--- /dev/null
+++ b/apps/benchmark/src/components/index.ts
@@ -0,0 +1,2 @@
+export { default as Select } from './Select';
+export { default as ReportCard } from './ReportCard';
diff --git a/apps/benchmark/src/implementations/emotion/Box.tsx b/apps/benchmark/src/implementations/emotion/Box.tsx
new file mode 100644
index 0000000..de466ec
--- /dev/null
+++ b/apps/benchmark/src/implementations/emotion/Box.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import { BoxProps } from '../types';
+import View from './View';
+
+const Box: React.FC<BoxProps> = ({ color, fixed = false, layout = 'column', outer = false, ...other }) => (
+  <View
+    {...other}
+    style={[styles[`color${color}`], fixed && styles.fixed, layout === 'row' && styles.row, outer && styles.outer]}
+  />
+);
+
+const styles = {
+  outer: {
+    alignSelf: 'flex-start',
+    padding: 4,
+  },
+  row: {
+    flexDirection: 'row',
+  },
+  color0: {
+    backgroundColor: '#14171A',
+  },
+  color1: {
+    backgroundColor: '#AAB8C2',
+  },
+  color2: {
+    backgroundColor: '#E6ECF0',
+  },
+  color3: {
+    backgroundColor: '#FFAD1F',
+  },
+  color4: {
+    backgroundColor: '#F45D22',
+  },
+  color5: {
+    backgroundColor: '#E0245E',
+  },
+  fixed: {
+    width: 6,
+    height: 6,
+  },
+};
+
+export default Box;
diff --git a/apps/benchmark/src/implementations/emotion/Dot.tsx b/apps/benchmark/src/implementations/emotion/Dot.tsx
new file mode 100644
index 0000000..6302795
--- /dev/null
+++ b/apps/benchmark/src/implementations/emotion/Dot.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { css } from '@emotion/css';
+import { DotProps } from '../types';
+
+const Dot: React.FC<DotProps> = ({ size, x, y, children, color }) => (
+  <div
+    className={css([
+      styles.root,
+      {
+        borderBottomColor: color,
+        borderRightWidth: `${size / 2}px`,
+        borderBottomWidth: `${size / 2}px`,
+        borderLeftWidth: `${size / 2}px`,
+        marginLeft: `${x}px`,
+        marginTop: `${y}px`,
+      },
+    ])}
+  >
+    {children}
+  </div>
+);
+
+const styles = {
+  root: {
+    position: 'absolute',
+    cursor: 'pointer',
+    width: 0,
+    height: 0,
+    borderColor: 'transparent',
+    borderStyle: 'solid',
+    borderTopWidth: 0,
+    transform: 'translate(50%, 50%)',
+  },
+} as const;
+
+export default Dot;
diff --git a/apps/benchmark/src/implementations/emotion/Provider.ts b/apps/benchmark/src/implementations/emotion/Provider.ts
new file mode 100644
index 0000000..864fc43
--- /dev/null
+++ b/apps/benchmark/src/implementations/emotion/Provider.ts
@@ -0,0 +1,2 @@
+import View from './View';
+export default View;
diff --git a/apps/benchmark/src/implementations/emotion/View.tsx b/apps/benchmark/src/implementations/emotion/View.tsx
new file mode 100644
index 0000000..f01e10f
--- /dev/null
+++ b/apps/benchmark/src/implementations/emotion/View.tsx
@@ -0,0 +1,25 @@
+import { css } from '@emotion/css';
+import React from 'react';
+
+const View: React.FC<{ style: any }> = ({ style = [], ...rest }) => {
+  return <div {...rest} className={css([viewStyle, ...style])} />;
+};
+
+const viewStyle = {
+  alignItems: 'stretch',
+  borderWidth: 0,
+  borderStyle: 'solid',
+  boxSizing: 'border-box',
+  display: 'flex',
+  flexBasis: 'auto',
+  flexDirection: 'column',
+  flexShrink: 0,
+  margin: 0,
+  padding: 0,
+  position: 'relative',
+  // fix flexbox bugs
+  minHeight: 0,
+  minWidth: 0,
+};
+
+export default View;
diff --git a/apps/benchmark/src/implementations/emotion/index.ts b/apps/benchmark/src/implementations/emotion/index.ts
new file mode 100644
index 0000000..2fb2f66
--- /dev/null
+++ b/apps/benchmark/src/implementations/emotion/index.ts
@@ -0,0 +1,11 @@
+import Box from './Box';
+import Dot from './Dot';
+import Provider from './Provider';
+import View from './View';
+
+export default {
+  Box,
+  Dot,
+  Provider,
+  View,
+};
diff --git a/apps/benchmark/src/implementations/griffel/Box.tsx b/apps/benchmark/src/implementations/griffel/Box.tsx
new file mode 100644
index 0000000..37d1fdf
--- /dev/null
+++ b/apps/benchmark/src/implementations/griffel/Box.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { BoxProps } from '../types';
+import View from './View';
+import { makeStyles, shorthands, mergeClasses } from '@griffel/react';
+
+const useStyles = makeStyles({
+  outer: {
+    alignSelf: 'flex-start',
+    ...shorthands.padding('4px'),
+  },
+  row: {
+    flexDirection: 'row',
+  },
+  color0: {
+    backgroundColor: '#14171A',
+  },
+  color1: {
+    backgroundColor: '#AAB8C2',
+  },
+  color2: {
+    backgroundColor: '#E6ECF0',
+  },
+  color3: {
+    backgroundColor: '#FFAD1F',
+  },
+  color4: {
+    backgroundColor: '#F45D22',
+  },
+  color5: {
+    backgroundColor: '#E0245E',
+  },
+  fixed: {
+    width: '6px',
+    height: '6px',
+  },
+});
+
+const Box: React.FC<BoxProps> = ({ color, fixed = false, layout = 'column', outer = false, ...other }) => {
+  const styles = useStyles();
+
+  return (
+    <View
+      {...other}
+      className={mergeClasses(
+        styles[`color${color}`],
+        fixed && styles.fixed,
+        layout === 'row' && styles.row,
+        outer && styles.outer,
+      )}
+    />
+  );
+};
+
+export default Box;
diff --git a/apps/benchmark/src/implementations/griffel/Dot.tsx b/apps/benchmark/src/implementations/griffel/Dot.tsx
new file mode 100644
index 0000000..69d9e1c
--- /dev/null
+++ b/apps/benchmark/src/implementations/griffel/Dot.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import { DotProps } from '../types';
+import { makeStyles, shorthands } from '@griffel/react';
+
+const useStyles = makeStyles({
+  root: {
+    position: 'absolute',
+    cursor: 'pointer',
+    width: 0,
+    height: 0,
+    ...shorthands.borderColor('transparent', 'transparent', 'var(--border-color)'),
+    ...shorthands.borderStyle('solid'),
+    ...shorthands.borderWidth('0px', 'var(--border-width)', 'var(--border-width)', 'var(--border-width)'),
+    marginLeft: 'var(--margin-left)',
+    marginTop: 'var(--margin-top)',
+    transform: 'translate(50%, 50%)',
+  },
+});
+
+const Dot: React.FC<DotProps> = ({ size, x, y, children, color }) => {
+  const styles = useStyles();
+  return (
+    <div
+      className={styles.root}
+      style={
+        {
+          '--border-width': `${size / 2}px`,
+          '--border-color': color,
+          '--margin-left': `${x}px`,
+          '--margin-top': `${y}px`,
+        } as any
+      }
+    >
+      {children}
+    </div>
+  );
+};
+
+const styles = {
+  root: {
+    position: 'absolute',
+    cursor: 'pointer',
+    width: 0,
+    height: 0,
+    borderColor: 'transparent',
+    borderStyle: 'solid',
+    borderTopWidth: 0,
+    transform: 'translate(50%, 50%)',
+  },
+} as const;
+
+export default Dot;
diff --git a/apps/benchmark/src/implementations/griffel/Provider.tsx b/apps/benchmark/src/implementations/griffel/Provider.tsx
new file mode 100644
index 0000000..bb3db34
--- /dev/null
+++ b/apps/benchmark/src/implementations/griffel/Provider.tsx
@@ -0,0 +1,14 @@
+import { createDOMRenderer } from '@griffel/core';
+import { RendererProvider } from '@griffel/react';
+import React from 'react';
+
+const renderer = createDOMRenderer(document);
+
+/**
+ * The benchmarking app already uses Griffel, no need for an extra provider here.
+ */
+const GriffelProvider: React.FC = ({ children }) => {
+  return <RendererProvider renderer={renderer}>{children}</RendererProvider>;
+};
+
+export default GriffelProvider;
diff --git a/apps/benchmark/src/implementations/griffel/View.tsx b/apps/benchmark/src/implementations/griffel/View.tsx
new file mode 100644
index 0000000..cc8c4e3
--- /dev/null
+++ b/apps/benchmark/src/implementations/griffel/View.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import { makeStyles, shorthands, mergeClasses } from '@griffel/react';
+
+const useStyles = makeStyles({
+  root: {
+    alignItems: 'stretch',
+    ...shorthands.borderWidth('0px'),
+    ...shorthands.borderStyle('solid'),
+    boxSizing: 'border-box',
+    display: 'flex',
+    flexBasis: 'auto',
+    flexDirection: 'column',
+    flexShrink: 0,
+    ...shorthands.margin('0px'),
+    ...shorthands.padding('0px'),
+    position: 'relative',
+    // fix flexbox bugs
+    minHeight: 0,
+    minWidth: 0,
+  },
+});
+
+const View: React.FC<{ className: string }> = ({ className, ...other }) => {
+  const styles = useStyles();
+  return <div {...other} className={mergeClasses(styles.root, className)} />;
+};
+
+export default View;
diff --git a/apps/benchmark/src/implementations/griffel/index.ts b/apps/benchmark/src/implementations/griffel/index.ts
new file mode 100644
index 0000000..c9a332f
--- /dev/null
+++ b/apps/benchmark/src/implementations/griffel/index.ts
@@ -0,0 +1,11 @@
+import Box from './Box';
+import View from './View';
+import Dot from './Dot';
+import Provider from './Provider';
+
+export default {
+  Box,
+  View,
+  Dot,
+  Provider,
+};
diff --git a/apps/benchmark/src/implementations/index.ts b/apps/benchmark/src/implementations/index.ts
new file mode 100644
index 0000000..1281f76
--- /dev/null
+++ b/apps/benchmark/src/implementations/index.ts
@@ -0,0 +1,33 @@
+// @ts-expect-error
+import packageJson from '../../package.json';
+// Glob imports handled by esbuild plugin
+// @ts-expect-error
+import * as implementations from '../implementations/**/index.ts?(x)';
+import type { ComponentsType } from '../types';
+
+interface ImplementationsImport {
+  default: { default: any }[];
+  filenames: string[];
+}
+
+interface ImplementationType {
+  components: ComponentsType;
+  name: string;
+  version: string;
+}
+
+const implementationsObject = (implementations as ImplementationsImport).filenames.reduce((obj, filename, i) => {
+  // skip this file
+  if (filename.includes('implementations/index.ts')) {
+    return obj;
+  }
+
+  const components = implementations.default[i].default;
+  // paths are normalized '../implementations/<implem>/index.ts'
+  const name = filename.split('/')[2];
+  const version = packageJson.dependencies[name] ?? '';
+  obj[name] = { components, name, version };
+  return obj;
+}, {} as Record<string, ImplementationType>);
+
+export default implementationsObject;
diff --git a/apps/benchmark/src/implementations/inline-styles/Box.tsx b/apps/benchmark/src/implementations/inline-styles/Box.tsx
new file mode 100644
index 0000000..ea7f56c
--- /dev/null
+++ b/apps/benchmark/src/implementations/inline-styles/Box.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import { BoxProps } from '../types';
+import View from './View';
+
+const Box: React.FC<BoxProps> = ({ color, fixed = false, layout = 'column', outer = false, ...other }) => (
+  <View
+    {...other}
+    style={{
+      ...styles[`color${color}`],
+      ...(fixed && styles.fixed),
+      ...(layout === 'row' && styles.row),
+      ...(outer && styles.outer),
+    }}
+  />
+);
+
+const styles = {
+  outer: {
+    alignSelf: 'flex-start',
+    padding: 4,
+  },
+  row: {
+    flexDirection: 'row',
+  },
+  color0: {
+    backgroundColor: '#14171A',
+  },
+  color1: {
+    backgroundColor: '#AAB8C2',
+  },
+  color2: {
+    backgroundColor: '#E6ECF0',
+  },
+  color3: {
+    backgroundColor: '#FFAD1F',
+  },
+  color4: {
+    backgroundColor: '#F45D22',
+  },
+  color5: {
+    backgroundColor: '#E0245E',
+  },
+  fixed: {
+    width: 6,
+    height: 6,
+  },
+} as const;
+
+export default Box;
diff --git a/apps/benchmark/src/implementations/inline-styles/Dot.tsx b/apps/benchmark/src/implementations/inline-styles/Dot.tsx
new file mode 100644
index 0000000..008b55a
--- /dev/null
+++ b/apps/benchmark/src/implementations/inline-styles/Dot.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { DotProps } from '../types';
+
+const Dot: React.FC<DotProps> = ({ size, x, y, children, color }) => (
+  <div
+    style={{
+      ...styles.root,
+      borderBottomColor: color,
+      borderRightWidth: `${size / 2}px`,
+      borderBottomWidth: `${size / 2}px`,
+      borderLeftWidth: `${size / 2}px`,
+      marginLeft: `${x}px`,
+      marginTop: `${y}px`,
+    }}
+  >
+    {children}
+  </div>
+);
+
+const styles = {
+  root: {
+    position: 'absolute',
+    cursor: 'pointer',
+    width: 0,
+    height: 0,
+    borderColor: 'transparent',
+    borderStyle: 'solid',
+    borderTopWidth: 0,
+    transform: 'translate(50%, 50%)',
+  },
+} as const;
+
+export default Dot;
diff --git a/apps/benchmark/src/implementations/inline-styles/Provider.tsx b/apps/benchmark/src/implementations/inline-styles/Provider.tsx
new file mode 100644
index 0000000..67a2a4a
--- /dev/null
+++ b/apps/benchmark/src/implementations/inline-styles/Provider.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+const InlineStylesProvider: React.FC = ({ children }) => {
+  return <>{children}</>;
+};
+
+export default InlineStylesProvider;
diff --git a/apps/benchmark/src/implementations/inline-styles/View.tsx b/apps/benchmark/src/implementations/inline-styles/View.tsx
new file mode 100644
index 0000000..9d37cae
--- /dev/null
+++ b/apps/benchmark/src/implementations/inline-styles/View.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+
+const compose = (s1: React.CSSProperties, s2: React.CSSProperties) => {
+  if (s1 && s2) {
+    return { ...s1, ...s2 };
+  } else {
+    return s1 || s2;
+  }
+};
+
+const View: React.FC<{ style: React.CSSProperties }> = ({ style, ...other }) => {
+  return <div {...other} style={compose(viewStyle, style)} />;
+};
+
+const viewStyle = {
+  alignItems: 'stretch',
+  borderWidth: 0,
+  borderStyle: 'solid',
+  boxSizing: 'border-box',
+  display: 'flex',
+  flexBasis: 'auto',
+  flexDirection: 'column',
+  flexShrink: 0,
+  margin: 0,
+  padding: 0,
+  position: 'relative',
+  // fix flexbox bugs
+  minHeight: 0,
+  minWidth: 0,
+} as React.CSSProperties;
+
+export default View;
diff --git a/apps/benchmark/src/implementations/inline-styles/index.ts b/apps/benchmark/src/implementations/inline-styles/index.ts
new file mode 100644
index 0000000..c9a332f
--- /dev/null
+++ b/apps/benchmark/src/implementations/inline-styles/index.ts
@@ -0,0 +1,11 @@
+import Box from './Box';
+import View from './View';
+import Dot from './Dot';
+import Provider from './Provider';
+
+export default {
+  Box,
+  View,
+  Dot,
+  Provider,
+};
diff --git a/apps/benchmark/src/implementations/types.ts b/apps/benchmark/src/implementations/types.ts
new file mode 100644
index 0000000..5733752
--- /dev/null
+++ b/apps/benchmark/src/implementations/types.ts
@@ -0,0 +1,13 @@
+export interface BoxProps {
+  color: 0 | 1 | 2 | 3 | 4 | 5;
+  fixed: boolean;
+  layout: 'column' | 'row';
+  outer: boolean;
+}
+
+export interface DotProps {
+  size: number;
+  x: number;
+  y: number;
+  color: string;
+}
diff --git a/apps/benchmark/src/index.tsx b/apps/benchmark/src/index.tsx
new file mode 100644
index 0000000..1891626
--- /dev/null
+++ b/apps/benchmark/src/index.tsx
@@ -0,0 +1,6 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { App } from './App';
+import './App.css';
+
+ReactDOM.render(<App />, document.getElementById('root'));
diff --git a/apps/benchmark/src/scenarios/SierpinskiTriangle/index.tsx b/apps/benchmark/src/scenarios/SierpinskiTriangle/index.tsx
new file mode 100644
index 0000000..8c85836
--- /dev/null
+++ b/apps/benchmark/src/scenarios/SierpinskiTriangle/index.tsx
@@ -0,0 +1,82 @@
+import { BenchmarkType } from '../../types';
+import React from 'react';
+import { interpolatePurples, interpolateBuPu, interpolateRdPu } from 'd3-scale-chromatic';
+
+const targetSize = 10;
+
+interface SierpinskiTriangleProps {
+  x: number;
+  y: number;
+  depth: number;
+  renderCount: number;
+  components: {
+    Dot: React.ElementType;
+  };
+  s: number;
+}
+
+class SierpinskiTriangle extends React.Component<SierpinskiTriangleProps> {
+  static displayName = 'SierpinskiTriangle';
+
+  static benchmarkType: BenchmarkType = 'update';
+
+  static defaultProps = {
+    depth: 0,
+    renderCount: 0,
+  };
+
+  override render() {
+    const { components, x, y, depth, renderCount } = this.props;
+    let { s } = this.props;
+    const { Dot } = components;
+
+    if (Dot) {
+      if (s <= targetSize) {
+        let fn;
+        switch (depth) {
+          case 1:
+            fn = interpolatePurples;
+            break;
+          case 2:
+            fn = interpolateBuPu;
+            break;
+          case 3:
+          default:
+            fn = interpolateRdPu;
+        }
+
+        // introduce randomness to ensure that repeated runs don't produce the same colors
+        const color = fn((renderCount * Math.random()) / 20);
+        return <Dot color={color} size={targetSize} x={x - targetSize / 2} y={y - targetSize / 2} />;
+      }
+
+      s /= 2;
+
+      return (
+        <React.Fragment>
+          <SierpinskiTriangle components={components} depth={1} renderCount={renderCount} s={s} x={x} y={y - s / 2} />
+          <SierpinskiTriangle
+            components={components}
+            depth={2}
+            renderCount={renderCount}
+            s={s}
+            x={x - s}
+            y={y + s / 2}
+          />
+          <SierpinskiTriangle
+            components={components}
+            depth={3}
+            renderCount={renderCount}
+            s={s}
+            x={x + s}
+            y={y + s / 2}
+          />
+        </React.Fragment>
+      );
+    } else {
+      return <span style={{ color: 'white' }}>No implementation available</span>;
+    }
+  }
+}
+
+export default SierpinskiTriangle;
diff --git a/apps/benchmark/src/scenarios/Tree/index.tsx b/apps/benchmark/src/scenarios/Tree/index.tsx
new file mode 100644
index 0000000..6fbcc16
--- /dev/null
+++ b/apps/benchmark/src/scenarios/Tree/index.tsx
@@ -0,0 +1,38 @@
+import type { BenchmarkType } from '../../types';
+import React, { Component, ElementType } from 'react';
+
+interface TreeProps {
+  breadth: number;
+  depth: number;
+  id: number;
+  wrap: number;
+  components: {
+    Box: ElementType;
+  };
+}
+
+class Tree extends Component<TreeProps> {
+  static displayName = 'Tree';
+  static benchmarkType: BenchmarkType = 'mount';
+
+  override render() {
+    const { breadth, components, depth, id, wrap } = this.props;
+    const { Box } = components;
+
+    let result = (
+      <Box color={id % 3} layout={depth % 2 === 0 ? 'column' : 'row'} outer>
+        {depth === 0 && <Box color={(id % 3) + 3} fixed />}
+        {depth !== 0 &&
+          Array.from({ length: breadth }).map((el, i) => (
+            <Tree breadth={breadth} components={components} depth={depth - 1} id={i} key={i} wrap={wrap} />
+          ))}
+      </Box>
+    );
+    for (let i = 0; i < wrap; i++) {
+      result = <Box>{result}</Box>;
+    }
+    return result;
+  }
+}
+
+export default Tree;
diff --git a/apps/benchmark/src/types.ts b/apps/benchmark/src/types.ts
new file mode 100644
index 0000000..bc68131
--- /dev/null
+++ b/apps/benchmark/src/types.ts
@@ -0,0 +1,61 @@
+import React from 'react';
+import type implementations from './implementations';
+import type tests from './tests';
+
+export type BenchResultsType = {
+  startTime: number;
+  endTime: number;
+  runTime: number;
+  sampleCount: number;
+  samples: Array<FullSampleTimingType>;
+  max: number;
+  min: number;
+  median: number;
+  mean: number;
+  meanLayout: number;
+  meanScripting: number;
+  stdDev: number;
+};
+
+export type BenchmarkName = keyof typeof tests;
+export type LibraryName = keyof typeof implementations;
+
+export interface BenchDisplayResults {
+  version?: string;
+  benchmark: BenchmarkName;
+  library: LibraryName;
+  analysis?: BenchResultsType;
+}
+
+export type SampleTimingType = {
+  scriptingStart: number;
+  scriptingEnd?: number;
+  layoutStart?: number;
+  layoutEnd?: number;
+};
+
+export type FullSampleTimingType = {
+  start: number;
+  end: number;
+  scriptingStart: number;
+  scriptingEnd: number;
+  layoutStart?: number;
+  layoutEnd?: number;
+};
+
+export interface ComponentsType {
+  Box: React.ElementType;
+  Dot: React.ElementType;
+  Provider: React.ElementType;
+  View: React.ElementType;
+}
+
+export type BenchmarkType = 'mount' | 'update' | 'unmount';
+
+export interface BenchmarkParameters {
+  Component: React.ElementType;
+  // eslint-disable-next-line @typescript-eslint/ban-types
+  getComponentProps: Function;
+  sampleCount: number;
+  type: BenchmarkType;
+}
diff --git a/apps/benchmark/tsconfig.json b/apps/benchmark/tsconfig.json
new file mode 100644
index 0000000..b0c2a4c
--- /dev/null
+++ b/apps/benchmark/tsconfig.json
@@ -0,0 +1,18 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "allowJs": true,
+    "checkJs": true,
+    "allowSyntheticDefaultImports": true,
+    "forceConsistentCasingInFileNames": true,
+    "jsx": "react",
+    "outDir": "../../dist/out-tsc",
+    "strict": true,
+    "noImplicitOverride": true,
+    "noPropertyAccessFromIndexSignature": true,
+    "noImplicitReturns": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "types": ["environment", "static-assets"],
+  "include": ["./src/**/*.ts", "./src/**/*.tsx", "./*.js"]
+}
diff --git a/package.json b/package.json
index 16dc2f5..07dc0e3 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
     "@docusaurus/core": "2.0.0-beta.15",
     "@docusaurus/module-type-aliases": "^2.0.0-beta.15",
     "@docusaurus/preset-classic": "2.0.0-beta.15",
+    "@emotion/css": "^11.9.0",
     "@nrwl/cli": "13.4.5",
     "@nrwl/eslint-plugin-nx": "13.4.5",
     "@nrwl/jest": "13.4.5",
@@ -52,6 +53,7 @@
     "@tsconfig/docusaurus": "^1.0.4",
     "@types/babel__helper-plugin-utils": "7.10.0",
     "@types/chrome": "0.0.180",
+    "@types/d3-scale-chromatic": "^3.0.0",
     "@types/jest": "27.0.2",
     "@types/js-beautify": "^1.13.3",
     "@types/loader-utils": "2.0.3",
@@ -61,6 +63,7 @@
     "@types/stylis": "4.0.2",
     "@types/tmp": "0.2.3",
     "@types/webpack-env": "^1.17.0",
+    "@types/yargs": "^17.0.10",
     "@typescript-eslint/eslint-plugin": "~5.3.0",
     "@typescript-eslint/parser": "~5.3.0",
     "babel-jest": "27.2.3",
@@ -69,6 +72,10 @@
     "babel-plugin-tester": "10.0.0",
     "beachball": "^2.20.0",
     "bestzip": "2.2.0",
+    "d3-scale-chromatic": "^3.0.0",
+    "esbuild": "^0.14.43",
+    "esbuild-plugin-alias": "^0.2.1",
+    "esbuild-plugin-import-glob": "^0.1.1",
     "eslint": "8.2.0",
     "eslint-config-prettier": "8.1.0",
     "eslint-import-resolver-typescript": "2.5.0",
@@ -93,7 +100,8 @@
     "tmp": "0.2.1",
     "ts-jest": "27.0.5",
     "typescript": "~4.5.2",
-    "url-loader": "^3.0.0"
+    "url-loader": "^3.0.0",
+    "yargs": "^17.5.1"
   },
   "dependencies": {
     "@babel/core": "^7.12.13",
diff --git a/packages/webpack-extraction-plugin/__fixtures__/babel/basic/code.ts b/packages/webpack-extraction-plugin/__fixtures__/babel/basic/code.ts
new file mode 100644
index 0000000..19dad50
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/babel/basic/code.ts
@@ -0,0 +1,22 @@
+import { __styles } from '@griffel/react';
+
+export const styles = __styles(
+  {
+    root: {
+      sj55zd: 'fe3e8s9',
+      uwmqm3: ['fycuoez', 'f8wuabp'],
+    },
+    icon: {
+      De3pzq: 'fcnqdeg',
+      Bi91k9c: 'faf35ka',
+    },
+  },
+  {
+    d: [
+      '.fe3e8s9{color:red;}',
+      '.fycuoez{padding-left:4px;}',
+      '.f8wuabp{padding-right:4px;}',
+      '.fcnqdeg{background-color:green;}',
+    ],
+  },
+);
diff --git a/packages/webpack-extraction-plugin/__fixtures__/babel/basic/output.ts b/packages/webpack-extraction-plugin/__fixtures__/babel/basic/output.ts
new file mode 100644
index 0000000..2a6fcfd
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/babel/basic/output.ts
@@ -0,0 +1,11 @@
+import { __styles, __css } from '@griffel/react';
+export const styles = __css({
+  root: {
+    sj55zd: 'fe3e8s9',
+    uwmqm3: ['fycuoez', 'f8wuabp'],
+  },
+  icon: {
+    De3pzq: 'fcnqdeg',
+    Bi91k9c: 'faf35ka',
+  },
+});
diff --git a/packages/webpack-extraction-plugin/__fixtures__/babel/multiple/code.ts b/packages/webpack-extraction-plugin/__fixtures__/babel/multiple/code.ts
new file mode 100644
index 0000000..0b4f067
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/babel/multiple/code.ts
@@ -0,0 +1,23 @@
+import { __styles } from '@griffel/react';
+
+export const stylesA = __styles(
+  {
+    root: {
+      sj55zd: 'fe3e8s9',
+    },
+  },
+  {
+    d: ['.fe3e8s9{color:red;}'],
+  },
+);
+
+export const stylesB = __styles(
+  {
+    root: {
+      De3pzq: 'fcnqdeg',
+    },
+  },
+  {
+    d: ['.fcnqdeg{background-color:green;}'],
+  },
+);
diff --git a/packages/webpack-extraction-plugin/__fixtures__/babel/multiple/output.ts b/packages/webpack-extraction-plugin/__fixtures__/babel/multiple/output.ts
new file mode 100644
index 0000000..9669953
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/babel/multiple/output.ts
@@ -0,0 +1,11 @@
+import { __styles, __css } from '@griffel/react';
+export const stylesA = __css({
+  root: {
+    sj55zd: 'fe3e8s9',
+  },
+});
+export const stylesB = __css({
+  root: {
+    De3pzq: 'fcnqdeg',
+  },
+});
diff --git a/packages/webpack-extraction-plugin/package.json b/packages/webpack-extraction-plugin/package.json
index ca3c0ae..918e103 100644
--- a/packages/webpack-extraction-plugin/package.json
+++ b/packages/webpack-extraction-plugin/package.json
@@ -7,5 +7,13 @@
   "repository": {
     "type": "git",
     "url": "https://github.com/microsoft/griffel"
+  },
+  "dependencies": {
+    "@babel/core": "^7.12.13",
+    "@babel/helper-plugin-utils": "^7.12.13",
+    "tslib": "^2.1.0"
+  },
+  "peerDependencies": {
+    "@griffel/core": "^1.3.1"
   }
 }
diff --git a/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts
new file mode 100644
index 0000000..b50d184
--- /dev/null
+++ b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts
@@ -0,0 +1,107 @@
+import { NodePath, PluginObj, PluginPass, types as t } from '@babel/core';
+import { declare } from '@babel/helper-plugin-utils';
+import type { CSSRulesByBucket } from '@griffel/core';
+
+type StripRuntimeBabelPluginOptions = never;
+
+type StripRuntimeBabelPluginState = PluginPass & {
+  cssRules?: string[];
+};
+
+export type StripRuntimeBabelPluginMetadata = {
+  cssRules: string[];
+};
+
+export const babelPluginStripGriffelRuntime = declare<
+  Partial<StripRuntimeBabelPluginOptions>,
+  PluginObj<StripRuntimeBabelPluginState>
+>(api => {
+  api.assertVersion(7);
+
+  return {
+    name: '@griffel/webpack-extraction-plugin/babel',
+    pre() {
+      this.cssRules = [];
+    },
+    post() {
+      (this.file.metadata as unknown as StripRuntimeBabelPluginMetadata).cssRules = this.cssRules!;
+    },
+
+    visitor: {
+      Program: {
+        exit(path, state) {
+          path.traverse({
+            ImportSpecifier(path) {
+              const importedPath = path.get('imported');
+
+              if (!importedPath.isIdentifier({ name: '__styles' })) {
+                return;
+              }
+
+              const declarationPath = path.findParent(p =>
+                p.isImportDeclaration(),
+              ) as NodePath<t.ImportDeclaration> | null;
+
+              if (declarationPath === null) {
+                throw path.buildCodeFrameError(
+                  [
+                    'Failed to find "ImportDeclaration" path for an "ImportSpecifier".',
+                    'Please report a bug (https://github.com/microsoft/griffel/issues) if this error happens',
+                  ].join(' '),
+                );
+              }
+
+              declarationPath.pushContainer('specifiers', t.identifier('__css'));
+            },
+
+            /**
+             * Visits all call expressions (__styles function calls):
+             * - replaces "__styles" calls "__css"
+             * - removes CSS rules from "__styles" calls
+             */
+            CallExpression(path) {
+              const calleePath = path.get('callee');
+
+              if (!calleePath.isIdentifier({ name: '__styles' })) {
+                return;
+              }
+
+              calleePath.replaceWith(t.identifier('__css'));
+
+              const argumentPaths = path.get('arguments');
+
+              if (argumentPaths.length !== 2) {
+                throw calleePath.buildCodeFrameError(
+                  [
+                    '"__styles" function call should have exactly two arguments.',
+                    'Please report a bug (https://github.com/microsoft/griffel/issues) if this error happens',
+                  ].join(' '),
+                );
+              }
+
+              // Returns the styles as a JavaScript object
+              const evaluationResult = argumentPaths[1].evaluate();
+
+              if (!evaluationResult.confident) {
+                throw argumentPaths[1].buildCodeFrameError(
+                  [
+                    'Failed to evaluate CSS rules from "__styles" call.',
+                    'Please report a bug (https://github.com/microsoft/griffel/issues) if this error happens',
+                  ].join(' '),
+                );
+              }
+
+              const cssRulesByBucket = evaluationResult.value as CSSRulesByBucket;
+
+              Object.values(cssRulesByBucket).forEach(cssRules => {
+                state.cssRules!.push(...cssRules);
+              });
+
+              argumentPaths[1].remove();
+            },
+          });
+        },
+      },
+    },
+  };
+});
diff --git a/packages/webpack-extraction-plugin/tsconfig.lib.json b/packages/webpack-extraction-plugin/tsconfig.lib.json
index 475cca8..278cb18 100644
--- a/packages/webpack-extraction-plugin/tsconfig.lib.json
+++ b/packages/webpack-extraction-plugin/tsconfig.lib.json
@@ -6,6 +6,6 @@
     "declaration": true,
     "types": ["node", "environment"]
   },
-  "exclude": ["__fixtures__/*/*.ts", "**/*.spec.ts", "**/*.test.ts"],
+  "exclude": ["__fixtures__/**/*/*.ts", "**/*.spec.ts", "**/*.test.ts"],
   "include": ["**/*.ts"]
 }
diff --git a/packages/webpack-extraction-plugin/tsconfig.spec.json b/packages/webpack-extraction-plugin/tsconfig.spec.json
index 76b0900..c8d8748 100644
--- a/packages/webpack-extraction-plugin/tsconfig.spec.json
+++ b/packages/webpack-extraction-plugin/tsconfig.spec.json
@@ -7,7 +7,7 @@
     "types": ["jest", "node", "environment"]
   },
   "include": [
-    "__fixtures__/*/code.ts",
+    "__fixtures__/**/*/code.ts",
     "**/*.test.ts",
     "**/*.spec.ts",
     "**/*.test.tsx",
diff --git a/workspace.json b/workspace.json
index f2f5e0a..59a619b 100644
--- a/workspace.json
+++ b/workspace.json
@@ -1,6 +1,7 @@
 {
   "version": 2,
   "projects": {
+    "@griffel/benchmark": "apps/benchmark",
     "@griffel/babel-preset": "packages/babel-preset",
     "@griffel/core": "packages/core",
     "@griffel/devtools": "packages/devtools",
diff --git a/yarn.lock b/yarn.lock
index 46a253d..0b14664 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -457,6 +457,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@babel/helper-plugin-utils@npm:^7.17.12":
+  version: 7.17.12
+  resolution: "@babel/helper-plugin-utils@npm:7.17.12"
+  checksum: 4813cf0ddb0f143de032cb88d4207024a2334951db330f8216d6fa253ea320c02c9b2667429ef1a34b5e95d4cfbd085f6cb72d418999751c31d0baf2422cc61d
+  languageName: node
+  linkType: hard
+
 "@babel/helper-remap-async-to-generator@npm:^7.16.8":
   version: 7.16.8
   resolution: "@babel/helper-remap-async-to-generator@npm:7.16.8"
@@ -949,6 +956,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@babel/plugin-syntax-jsx@npm:^7.12.13":
+  version: 7.17.12
+  resolution: "@babel/plugin-syntax-jsx@npm:7.17.12"
+  dependencies:
+    "@babel/helper-plugin-utils": ^7.17.12
+  peerDependencies:
+    "@babel/core": ^7.0.0-0
+  checksum: 6acd0bbca8c3e0100ad61f3b7d0b0111cd241a0710b120b298c4aa0e07be02eccbcca61ede1e7678ade1783a0979f20305b62263df6767fa3fbf658670d82af5
+  languageName: node
+  linkType: hard
+
 "@babel/plugin-syntax-jsx@npm:^7.16.7":
   version: 7.16.7
   resolution: "@babel/plugin-syntax-jsx@npm:7.16.7"
@@ -1726,6 +1744,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@babel/runtime@npm:^7.13.10":
+  version: 7.18.3
+  resolution: "@babel/runtime@npm:7.18.3"
+  dependencies:
+    regenerator-runtime: ^0.13.4
+  checksum: db8526226aa02cfa35a5a7ac1a34b5f303c62a1f000c7db48cb06c6290e616483e5036ab3c4e7a84d0f3be6d4e2148d5fe5cec9564bf955f505c3e764b83d7f1
+  languageName: node
+  linkType: hard
+
 "@babel/template@npm:>=7, @babel/template@npm:^7.12.13, @babel/template@npm:^7.12.7, @babel/template@npm:^7.16.7, @babel/template@npm:^7.3.3":
   version: 7.16.7
   resolution: "@babel/template@npm:7.16.7"
@@ -2601,6 +2628,28 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@emotion/babel-plugin@npm:^11.7.1":
+  version: 11.9.2
+  resolution: "@emotion/babel-plugin@npm:11.9.2"
+  dependencies:
+    "@babel/helper-module-imports": ^7.12.13
+    "@babel/plugin-syntax-jsx": ^7.12.13
+    "@babel/runtime": ^7.13.10
+    "@emotion/hash": ^0.8.0
+    "@emotion/memoize": ^0.7.5
+    "@emotion/serialize": ^1.0.2
+    babel-plugin-macros: ^2.6.1
+    convert-source-map: ^1.5.0
+    escape-string-regexp: ^4.0.0
+    find-root: ^1.1.0
+    source-map: ^0.5.7
+    stylis: 4.0.13
+  peerDependencies:
+    "@babel/core": ^7.0.0
+  checksum: 2d2c4fadd389862896bcbc5f42c9b9c1a199810173fcf14e5520506c7179c2ddb991b8832fd273f42104cf0dae98886ad8e767b5e38ad235b652d903c3b8a328
+  languageName: node
+  linkType: hard
+
 "@emotion/cache@npm:^10.0.27":
   version: 10.0.29
   resolution: "@emotion/cache@npm:10.0.29"
@@ -2613,6 +2662,19 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@emotion/cache@npm:^11.7.1":
+  version: 11.9.3
+  resolution: "@emotion/cache@npm:11.9.3"
+  dependencies:
+    "@emotion/memoize": ^0.7.4
+    "@emotion/sheet": ^1.1.1
+    "@emotion/utils": ^1.0.0
+    "@emotion/weak-memoize": ^0.2.5
+    stylis: 4.0.13
+  checksum: 6e0aab2fa5b9b6b0b9bf66b5328ed44265c23ced16b46c13d2602c3497fabd95299f6cf2c87cbc02b630452aa3cff599c194c538125d744aa135824129698ccc
+  languageName: node
+  linkType: hard
+
 "@emotion/core@npm:^10.1.1":
   version: 10.3.1
   resolution: "@emotion/core@npm:10.3.1"
@@ -2640,6 +2702,24 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@emotion/css@npm:^11.9.0":
+  version: 11.9.0
+  resolution: "@emotion/css@npm:11.9.0"
+  dependencies:
+    "@emotion/babel-plugin": ^11.7.1
+    "@emotion/cache": ^11.7.1
+    "@emotion/serialize": ^1.0.3
+    "@emotion/sheet": ^1.0.3
+    "@emotion/utils": ^1.0.0
+  peerDependencies:
+    "@babel/core": ^7.0.0
+  peerDependenciesMeta:
+    "@babel/core":
+      optional: true
+  checksum: bd83d9af5cf87b82992dea030b5c7db9cc26f8fee508fc8c20257bc040d35b0cc595143c5c8e3f5d6719351f3ae1c53b152899c22b60a2a5ff37d3439304401e
+  languageName: node
+  linkType: hard
+
 "@emotion/hash@npm:0.8.0, @emotion/hash@npm:^0.8.0":
   version: 0.8.0
   resolution: "@emotion/hash@npm:0.8.0"
@@ -2663,6 +2743,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@emotion/memoize@npm:^0.7.4, @emotion/memoize@npm:^0.7.5":
+  version: 0.7.5
+  resolution: "@emotion/memoize@npm:0.7.5"
+  checksum: 83da8d4a7649a92c72f960817692bc6be13cc13e107b9f7e878d63766525ed4402881bfeb3cda61145c050281e7e260f114a0a2870515527346f2ef896b915b3
+  languageName: node
+  linkType: hard
+
 "@emotion/serialize@npm:^0.11.15, @emotion/serialize@npm:^0.11.16":
   version: 0.11.16
   resolution: "@emotion/serialize@npm:0.11.16"
@@ -2676,6 +2763,19 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@emotion/serialize@npm:^1.0.2, @emotion/serialize@npm:^1.0.3":
+  version: 1.0.4
+  resolution: "@emotion/serialize@npm:1.0.4"
+  dependencies:
+    "@emotion/hash": ^0.8.0
+    "@emotion/memoize": ^0.7.4
+    "@emotion/unitless": ^0.7.5
+    "@emotion/utils": ^1.0.0
+    csstype: ^3.0.2
+  checksum: e8cc342056734e176ea837fe44035126dea174962db40852a7ced499d258c0056b0fd3c298743c446f9ba0f2647cb42dfb623b8e5783c265deb9eb20138d68e7
+  languageName: node
+  linkType: hard
+
 "@emotion/sheet@npm:0.9.4":
   version: 0.9.4
   resolution: "@emotion/sheet@npm:0.9.4"
@@ -2683,6 +2783,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@emotion/sheet@npm:^1.0.3, @emotion/sheet@npm:^1.1.1":
+  version: 1.1.1
+  resolution: "@emotion/sheet@npm:1.1.1"
+  checksum: b916ac665735ef6dfda26b09f2d3493789d432d649733db9da18c4db0115e7fdadeb8d45f6490320248916bb13d978bba74c914b711ac96f659b76a5e52d5cd2
+  languageName: node
+  linkType: hard
+
 "@emotion/styled-base@npm:^10.3.0":
   version: 10.3.0
   resolution: "@emotion/styled-base@npm:10.3.0"
@@ -2718,7 +2825,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@emotion/unitless@npm:0.7.5":
+"@emotion/unitless@npm:0.7.5, @emotion/unitless@npm:^0.7.5":
   version: 0.7.5
   resolution: "@emotion/unitless@npm:0.7.5"
   checksum: f976e5345b53fae9414a7b2e7a949aa6b52f8bdbcc84458b1ddc0729e77ba1d1dfdff9960e0da60183877873d3a631fa24d9695dd714ed94bcd3ba5196586a6b
@@ -2732,7 +2839,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@emotion/weak-memoize@npm:0.2.5":
+"@emotion/utils@npm:^1.0.0":
+  version: 1.1.0
+  resolution: "@emotion/utils@npm:1.1.0"
+  checksum: d3b681ca3a23b07033ac6c6937e71010a5549ac8ccec325eb6c91a7e48d9a73db83fa5dadc58be981bb125d7c00fedca868ea4362b1da9e02866615f96be4df1
+  languageName: node
+  linkType: hard
+
+"@emotion/weak-memoize@npm:0.2.5, @emotion/weak-memoize@npm:^0.2.5":
   version: 0.2.5
   resolution: "@emotion/weak-memoize@npm:0.2.5"
   checksum: 27d402b0c683b94658220b6d47840346ee582329ca2a15ec9c233492e0f1a27687ccb233b76eedc922f2e185e444cc89f7b97a81a1d3e5ae9f075bab08e965ea
@@ -5886,6 +6000,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/d3-scale-chromatic@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "@types/d3-scale-chromatic@npm:3.0.0"
+  checksum: e06afffd2725570aa90cb3050eb96a94727264948d9256e56807ab582aba379168d84d1d98bcaa275bf38375148b35dfe13697e06fc7565dd17ac7e2acb11980
+  languageName: node
+  linkType: hard
+
 "@types/eslint-scope@npm:^3.7.0":
   version: 3.7.3
   resolution: "@types/eslint-scope@npm:3.7.3"
@@ -6575,6 +6696,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/yargs@npm:^17.0.10":
+  version: 17.0.10
+  resolution: "@types/yargs@npm:17.0.10"
+  dependencies:
+    "@types/yargs-parser": "*"
+  checksum: f0673cbfc08e17239dc58952a88350d6c4db04a027a28a06fbad27d87b670e909f9cd9e66f9c64cebdd5071d1096261e33454a55868395f125297e5c50992ca8
+  languageName: node
+  linkType: hard
+
 "@typescript-eslint/eslint-plugin@npm:~5.3.0":
   version: 5.3.1
   resolution: "@typescript-eslint/eslint-plugin@npm:5.3.1"
@@ -8097,7 +8227,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"babel-plugin-macros@npm:^2.0.0, babel-plugin-macros@npm:^2.8.0":
+"babel-plugin-macros@npm:^2.0.0, babel-plugin-macros@npm:^2.6.1, babel-plugin-macros@npm:^2.8.0":
   version: 2.8.0
   resolution: "babel-plugin-macros@npm:2.8.0"
   dependencies:
@@ -10442,6 +10572,32 @@ __metadata:
   languageName: node
   linkType: hard
 
+"d3-color@npm:1 - 3":
+  version: 3.1.0
+  resolution: "d3-color@npm:3.1.0"
+  checksum: 4931fbfda5d7c4b5cfa283a13c91a954f86e3b69d75ce588d06cde6c3628cebfc3af2069ccf225e982e8987c612aa7948b3932163ce15eb3c11cd7c003f3ee3b
+  languageName: node
+  linkType: hard
+
+"d3-interpolate@npm:1 - 3":
+  version: 3.0.1
+  resolution: "d3-interpolate@npm:3.0.1"
+  dependencies:
+    d3-color: 1 - 3
+  checksum: a42ba314e295e95e5365eff0f604834e67e4a3b3c7102458781c477bd67e9b24b6bb9d8e41ff5521050a3f2c7c0c4bbbb6e187fd586daa3980943095b267e78b
+  languageName: node
+  linkType: hard
+
+"d3-scale-chromatic@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "d3-scale-chromatic@npm:3.0.0"
+  dependencies:
+    d3-color: 1 - 3
+    d3-interpolate: 1 - 3
+  checksum: a8ce4cb0267a17b28ebbb929f5e3071d985908a9c13b6fcaa2a198e1e018f275804d691c5794b970df0049725b7944f32297b31603d235af6414004f0c7f82c0
+  languageName: node
+  linkType: hard
+
 "d@npm:1, d@npm:^1.0.1":
   version: 1.0.1
   resolution: "d@npm:1.0.1"
@@ -11447,6 +11603,233 @@ __metadata:
   languageName: node
   linkType: hard
 
+"esbuild-android-64@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-android-64@npm:0.14.43"
+  conditions: os=android & cpu=x64
+  languageName: node
+  linkType: hard
+
+"esbuild-android-arm64@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-android-arm64@npm:0.14.43"
+  conditions: os=android & cpu=arm64
+  languageName: node
+  linkType: hard
+
+"esbuild-darwin-64@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-darwin-64@npm:0.14.43"
+  conditions: os=darwin & cpu=x64
+  languageName: node
+  linkType: hard
+
+"esbuild-darwin-arm64@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-darwin-arm64@npm:0.14.43"
+  conditions: os=darwin & cpu=arm64
+  languageName: node
+  linkType: hard
+
+"esbuild-freebsd-64@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-freebsd-64@npm:0.14.43"
+  conditions: os=freebsd & cpu=x64
+  languageName: node
+  linkType: hard
+
+"esbuild-freebsd-arm64@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-freebsd-arm64@npm:0.14.43"
+  conditions: os=freebsd & cpu=arm64
+  languageName: node
+  linkType: hard
+
+"esbuild-linux-32@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-linux-32@npm:0.14.43"
+  conditions: os=linux & cpu=ia32
+  languageName: node
+  linkType: hard
+
+"esbuild-linux-64@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-linux-64@npm:0.14.43"
+  conditions: os=linux & cpu=x64
+  languageName: node
+  linkType: hard
+
+"esbuild-linux-arm64@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-linux-arm64@npm:0.14.43"
+  conditions: os=linux & cpu=arm64
+  languageName: node
+  linkType: hard
+
+"esbuild-linux-arm@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-linux-arm@npm:0.14.43"
+  conditions: os=linux & cpu=arm
+  languageName: node
+  linkType: hard
+
+"esbuild-linux-mips64le@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-linux-mips64le@npm:0.14.43"
+  conditions: os=linux & cpu=mips64el
+  languageName: node
+  linkType: hard
+
+"esbuild-linux-ppc64le@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-linux-ppc64le@npm:0.14.43"
+  conditions: os=linux & cpu=ppc64
+  languageName: node
+  linkType: hard
+
+"esbuild-linux-riscv64@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-linux-riscv64@npm:0.14.43"
+  conditions: os=linux & cpu=riscv64
+  languageName: node
+  linkType: hard
+
+"esbuild-linux-s390x@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-linux-s390x@npm:0.14.43"
+  conditions: os=linux & cpu=s390x
+  languageName: node
+  linkType: hard
+
+"esbuild-netbsd-64@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-netbsd-64@npm:0.14.43"
+  conditions: os=netbsd & cpu=x64
+  languageName: node
+  linkType: hard
+
+"esbuild-openbsd-64@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-openbsd-64@npm:0.14.43"
+  conditions: os=openbsd & cpu=x64
+  languageName: node
+  linkType: hard
+
+"esbuild-plugin-alias@npm:^0.2.1":
+  version: 0.2.1
+  resolution: "esbuild-plugin-alias@npm:0.2.1"
+  checksum: afe2d2c8b5f09d5321cb8d9c0825e8a9f6e03c2d50df92f953a291d4620cc29eddb3da9e33b238f6d8f77738e0277bdcb831f127399449fecf78fb84c04e5da9
+  languageName: node
+  linkType: hard
+
+"esbuild-plugin-import-glob@npm:^0.1.1":
+  version: 0.1.1
+  resolution: "esbuild-plugin-import-glob@npm:0.1.1"
+  dependencies:
+    fast-glob: ^3.2.5
+  checksum: d6a350e13e22bca2183b939040a4f7d3d775ea41fe215f1487aaac17e0477e9bea4afb507c89a17ddaf9c91d8791f567ec98c4a787d3176df9206d67d671a58c
+  languageName: node
+  linkType: hard
+
+"esbuild-sunos-64@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-sunos-64@npm:0.14.43"
+  conditions: os=sunos & cpu=x64
+  languageName: node
+  linkType: hard
+
+"esbuild-windows-32@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-windows-32@npm:0.14.43"
+  conditions: os=win32 & cpu=ia32
+  languageName: node
+  linkType: hard
+
+"esbuild-windows-64@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-windows-64@npm:0.14.43"
+  conditions: os=win32 & cpu=x64
+  languageName: node
+  linkType: hard
+
+"esbuild-windows-arm64@npm:0.14.43":
+  version: 0.14.43
+  resolution: "esbuild-windows-arm64@npm:0.14.43"
+  conditions: os=win32 & cpu=arm64
+  languageName: node
+  linkType: hard
+
+"esbuild@npm:^0.14.43":
+  version: 0.14.43
+  resolution: "esbuild@npm:0.14.43"
+  dependencies:
+    esbuild-android-64: 0.14.43
+    esbuild-android-arm64: 0.14.43
+    esbuild-darwin-64: 0.14.43
+    esbuild-darwin-arm64: 0.14.43
+    esbuild-freebsd-64: 0.14.43
+    esbuild-freebsd-arm64: 0.14.43
+    esbuild-linux-32: 0.14.43
+    esbuild-linux-64: 0.14.43
+    esbuild-linux-arm: 0.14.43
+    esbuild-linux-arm64: 0.14.43
+    esbuild-linux-mips64le: 0.14.43
+    esbuild-linux-ppc64le: 0.14.43
+    esbuild-linux-riscv64: 0.14.43
+    esbuild-linux-s390x: 0.14.43
+    esbuild-netbsd-64: 0.14.43
+    esbuild-openbsd-64: 0.14.43
+    esbuild-sunos-64: 0.14.43
+    esbuild-windows-32: 0.14.43
+    esbuild-windows-64: 0.14.43
+    esbuild-windows-arm64: 0.14.43
+  dependenciesMeta:
+    esbuild-android-64:
+      optional: true
+    esbuild-android-arm64:
+      optional: true
+    esbuild-darwin-64:
+      optional: true
+    esbuild-darwin-arm64:
+      optional: true
+    esbuild-freebsd-64:
+      optional: true
+    esbuild-freebsd-arm64:
+      optional: true
+    esbuild-linux-32:
+      optional: true
+    esbuild-linux-64:
+      optional: true
+    esbuild-linux-arm:
+      optional: true
+    esbuild-linux-arm64:
+      optional: true
+    esbuild-linux-mips64le:
+      optional: true
+    esbuild-linux-ppc64le:
+      optional: true
+    esbuild-linux-riscv64:
+      optional: true
+    esbuild-linux-s390x:
+      optional: true
+    esbuild-netbsd-64:
+      optional: true
+    esbuild-openbsd-64:
+      optional: true
+    esbuild-sunos-64:
+      optional: true
+    esbuild-windows-32:
+      optional: true
+    esbuild-windows-64:
+      optional: true
+    esbuild-windows-arm64:
+      optional: true
+  bin:
+    esbuild: bin/esbuild
+  checksum: c5988ba9d3b62c794aaf6752ca4cf0f009d0dc127a19d21c1bbeb2fe6d983c7fe39f3d330e2c00d5a9b6de4a71bfb0f5f347e69c37eb545ab61dc4824b5b2bb3
+  languageName: node
+  linkType: hard
+
 "escalade@npm:^3.1.1":
   version: 3.1.1
   resolution: "escalade@npm:3.1.1"
@@ -12143,7 +12526,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.7, fast-glob@npm:^3.2.9":
+"fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.5, fast-glob@npm:^3.2.7, fast-glob@npm:^3.2.9":
   version: 3.2.11
   resolution: "fast-glob@npm:3.2.11"
   dependencies:
@@ -13263,6 +13646,7 @@ __metadata:
     "@docusaurus/core": 2.0.0-beta.15
     "@docusaurus/module-type-aliases": ^2.0.0-beta.15
     "@docusaurus/preset-classic": 2.0.0-beta.15
+    "@emotion/css": ^11.9.0
     "@emotion/hash": ^0.8.0
     "@linaria/babel-preset": ^3.0.0-beta.14
     "@linaria/shaker": ^3.0.0-beta.14
@@ -13288,6 +13672,7 @@ __metadata:
     "@tsconfig/docusaurus": ^1.0.4
     "@types/babel__helper-plugin-utils": 7.10.0
     "@types/chrome": 0.0.180
+    "@types/d3-scale-chromatic": ^3.0.0
     "@types/jest": 27.0.2
     "@types/js-beautify": ^1.13.3
     "@types/loader-utils": 2.0.3
@@ -13297,6 +13682,7 @@ __metadata:
     "@types/stylis": 4.0.2
     "@types/tmp": 0.2.3
     "@types/webpack-env": ^1.17.0
+    "@types/yargs": ^17.0.10
     "@typescript-eslint/eslint-plugin": ~5.3.0
     "@typescript-eslint/parser": ~5.3.0
     "@typescript-eslint/utils": ^5.20.0
@@ -13308,7 +13694,11 @@ __metadata:
     beachball: ^2.20.0
     bestzip: 2.2.0
     csstype: ^3.0.10
+    d3-scale-chromatic: ^3.0.0
     enhanced-resolve: ^5.8.2
+    esbuild: ^0.14.43
+    esbuild-plugin-alias: ^0.2.1
+    esbuild-plugin-import-glob: ^0.1.1
     eslint: 8.2.0
     eslint-config-prettier: 8.1.0
     eslint-import-resolver-typescript: 2.5.0
@@ -13339,6 +13729,7 @@ __metadata:
     tslib: ^2.1.0
     typescript: ~4.5.2
     url-loader: ^3.0.0
+    yargs: ^17.5.1
   languageName: unknown
   linkType: soft
 
@@ -22287,6 +22678,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"stylis@npm:4.0.13, stylis@npm:^4.0.13":
+  version: 4.0.13
+  resolution: "stylis@npm:4.0.13"
+  checksum: 8ea7a87028b6383c6a982231c4b5b6150031ce028e0fdaf7b2ace82253d28a8af50cc5a9da8a421d3c7c4441592f393086e332795add672aa4a825f0fe3713a3
+  languageName: node
+  linkType: hard
+
 "stylis@npm:^3.5.4":
   version: 3.5.4
   resolution: "stylis@npm:3.5.4"
@@ -22294,13 +22692,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"stylis@npm:^4.0.13":
-  version: 4.0.13
-  resolution: "stylis@npm:4.0.13"
-  checksum: 8ea7a87028b6383c6a982231c4b5b6150031ce028e0fdaf7b2ace82253d28a8af50cc5a9da8a421d3c7c4441592f393086e332795add672aa4a825f0fe3713a3
-  languageName: node
-  linkType: hard
-
 "stylus-loader@npm:^6.2.0":
   version: 6.2.0
   resolution: "stylus-loader@npm:6.2.0"
@@ -24609,6 +25000,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"yargs-parser@npm:^21.0.0":
+  version: 21.0.1
+  resolution: "yargs-parser@npm:21.0.1"
+  checksum: c3ea2ed12cad0377ce3096b3f138df8267edf7b1aa7d710cd502fe16af417bafe4443dd71b28158c22fcd1be5dfd0e86319597e47badf42ff83815485887323a
+  languageName: node
+  linkType: hard
+
 "yargs@npm:15.4.1":
   version: 15.4.1
   resolution: "yargs@npm:15.4.1"
@@ -24661,6 +25059,21 @@ __metadata:
   languageName: node
   linkType: hard
 
+"yargs@npm:^17.5.1":
+  version: 17.5.1
+  resolution: "yargs@npm:17.5.1"
+  dependencies:
+    cliui: ^7.0.2
+    escalade: ^3.1.1
+    get-caller-file: ^2.0.5
+    require-directory: ^2.1.1
+    string-width: ^4.2.3
+    y18n: ^5.0.5
+    yargs-parser: ^21.0.0
+  checksum: 00d58a2c052937fa044834313f07910fd0a115dec5ee35919e857eeee3736b21a4eafa8264535800ba8bac312991ce785ecb8a51f4d2cc8c4676d865af1cfbde
+  languageName: node
+  linkType: hard
+
 "yn@npm:3.1.1":
   version: 3.1.1
   resolution: "yn@npm:3.1.1"


Test patch:
diff --git a/apps/benchmark/src/tests.ts b/apps/benchmark/src/tests.ts
new file mode 100644
index 0000000..ccbefbf
--- /dev/null
+++ b/apps/benchmark/src/tests.ts
@@ -0,0 +1,58 @@
+import implementations from './implementations';
+import Tree from './scenarios/Tree';
+import SierpinskiTriangle from './scenarios/SierpinskiTriangle';
+import { BenchmarkParameters, ComponentsType } from './types';
+
+const packageNames = Object.keys(implementations);
+
+interface BenchmarkTest extends BenchmarkParameters {
+  version: string;
+  name: string;
+}
+
+const createTestBlock = (fn: (components: ComponentsType) => BenchmarkParameters) => {
+  return packageNames.reduce((testSetups, packageName) => {
+    const { name, components, version } = implementations[packageName];
+    // TODO handle Provider
+    const { Component, getComponentProps, sampleCount, type } = fn(components);
+
+    testSetups[packageName] = {
+      Component,
+      getComponentProps,
+      sampleCount,
+      // Provider,
+      type,
+      version,
+      name,
+    };
+    return testSetups;
+  }, {} as Record<string, BenchmarkTest>);
+};
+
+const tests = {
+  'Mount deep tree': createTestBlock(components => ({
+    type: 'mount' as const,
+    Component: Tree,
+    getComponentProps: () => ({ breadth: 2, components, depth: 8, id: 0, wrap: 1 }),
+    Provider: components.Provider,
+    sampleCount: 50,
+  })),
+  'Mount wide tree': createTestBlock(components => ({
+    type: 'mount',
+    Component: Tree,
+    getComponentProps: () => ({ breadth: 6, components, depth: 3, id: 0, wrap: 2 }),
+    Provider: components.Provider,
+    sampleCount: 50,
+  })),
+  'Update dynamic styles': createTestBlock(components => ({
+    type: 'update',
+    Component: SierpinskiTriangle,
+    getComponentProps: ({ cycle }: { cycle: number }) => {
+      return { components, s: 200, renderCount: cycle, x: 0, y: 0 };
+    },
+    Provider: components.Provider,
+    sampleCount: 100,
+  })),
+};
+
+export default tests;
diff --git a/packages/react/src/renderToStyleElements.test.tsx b/packages/react/src/renderToStyleElements.test.tsx
new file mode 100644
index 0000000..cf85399
--- /dev/null
+++ b/packages/react/src/renderToStyleElements.test.tsx
@@ -0,0 +1,55 @@
+import { createDOMRenderer } from '@griffel/core';
+import * as prettier from 'prettier';
+import * as React from 'react';
+import * as ReactDOM from 'react-dom/server';
+
+import { makeStyles } from './makeStyles';
+import { RendererProvider } from './RendererContext';
+import { renderToStyleElements } from './renderToStyleElements';
+
+expect.addSnapshotSerializer({
+  test(value) {
+    return typeof value === 'string';
+  },
+  print(value) {
+    /**
+     * test function makes sure that value is the guarded type
+     */
+    const _value = value as string;
+
+    return prettier.format(_value, { parser: 'html' }).trim();
+  },
+});
+
+describe('renderToStyleElements', () => {
+  it('renders elements in DOM env', () => {
+    const useExampleStyles = makeStyles({
+      root: { color: 'red', ':hover': { color: 'green' } },
+    });
+    const ExampleComponent: React.FC = () => {
+      const classes = useExampleStyles();
+
+      return <div className={classes.root} />;
+    };
+
+    const renderer = createDOMRenderer();
+
+    ReactDOM.renderToStaticMarkup(
+      <RendererProvider renderer={renderer}>
+        <ExampleComponent />
+      </RendererProvider>,
+    );
+
+    expect(ReactDOM.renderToStaticMarkup(<>{renderToStyleElements(renderer)}</>)).toMatchInlineSnapshot(`
+      <style data-make-styles-bucket="d" data-make-styles-rehydration="true">
+        .fe3e8s9 {
+          color: red;
+        }</style
+      ><style data-make-styles-bucket="h" data-make-styles-rehydration="true">
+        .f1ej289o:hover {
+          color: green;
+        }
+      </style>
+    `);
+  });
+});
diff --git a/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.test.ts b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.test.ts
new file mode 100644
index 0000000..c29bb93
--- /dev/null
+++ b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.test.ts
@@ -0,0 +1,36 @@
+import pluginTester, { prettierFormatter } from 'babel-plugin-tester';
+import * as fs from 'fs';
+import * as path from 'path';
+
+import { babelPluginStripGriffelRuntime } from './babelPluginStripGriffelRuntime';
+
+const prettierConfig = JSON.parse(
+  fs.readFileSync(path.resolve(__dirname, '../../../.prettierrc'), { encoding: 'utf-8' }),
+);
+const fixturesDir = path.join(__dirname, '..', '__fixtures__', 'babel');
+
+pluginTester({
+  babelOptions: {
+    parserOpts: {
+      plugins: ['typescript'],
+    },
+  },
+  pluginOptions: {
+    babelOptions: {
+      presets: ['@babel/typescript'],
+    },
+  },
+  formatResult: code =>
+    prettierFormatter(code, {
+      config: {
+        ...prettierConfig,
+        parser: 'typescript',
+      },
+    }),
+
+  fixtures: fixturesDir,
+  tests: [],
+
+  plugin: babelPluginStripGriffelRuntime,
+  pluginName: '@griffel/webpack-extraction-plugin/babel',
+});